1. はじめに
オペレーティングシステムにおいて、プロセス情報はシステム監視やパフォーマンス分析にとって極めて重要です。ここで、現在のOSのプロセス情報をキャプチャし、それをサーバーや監視端末などの他端末に効率的に転送する監視プログラムを開発する必要があると仮定しましょう。この過程で、キャプチャしたプロセスオブジェクトをバイナリデータに変換し、さらにパケットサイズを最小化するための最適化が重要な課題となります。本稿では、ステップバイステップの分析を通じて、C#オブジェクトのバイナリシリアル化をビットフィールド技術を用いて最適化する方法について考察します。

まず、プロセスオブジェクトのフィールド定義例を示します。このオブジェクトをネットワーク(TCP/UDP)で転送するためには、バイナリ形式に変換する必要があります。その際、いかにして最小のパケットサイズを実現するかが課題です。
| フィールド名 | 説明 | 例 |
|---|---|---|
| PID | プロセスID | 10565 |
| Name | プロセス名 | 码坊 |
| Publisher | 発行者 | 沙漠尽头的狼 |
| CommandLine | コマンドライン | dotnet CodeWF.Tools.dll |
| CPU | CPU(全コアの合計処理利用率) | 2.3% |
| Memory | メモリ(プロセスが占有する物理メモリ) | 0.1% |
| Disk | ディスク(全物理ドライブの合計利用率) | 0.1 MB/秒 |
| Network | ネットワーク(現在の主要ネットワークの利用率) | 0 Mbps |
| GPU | GPU(全GPUエンジンの最高利用率) | 2.2% |
| GPUEngine | GPUエンジン | GPU 0 - 3D |
| PowerUsage | 電力使用状況(CPU、ディスク、GPUの消費電力への影響) | 低 |
| PowerUsageTrend | 電力使用状況の傾向(一定期間のCPU、ディスク、GPUの消費電力への影響) | 非常に低 |
| Type | プロセスタイプ | アプリ |
| Status | プロセスステータス | 効率モード |
2. 最適化プロセス
2.1. プロセスオブジェクトの定義と初期分析
各フィールドのサンプル値に基づいてデータ型を決定しました。
| フィールド名 | データ型 | 説明 | 例 |
|---|---|---|---|
| PID | int | プロセスID | 10565 |
| Name | string? | プロセス名 | 码坊 |
| Publisher | string? | 発行者 | 沙漠尽头的狼 |
| CommandLine | string? | コマンドライン | dotnet CodeWF.Tools.dll |
| CPU | string? | CPU(全コアの合計処理利用率) | 2.3% |
| Memory | string? | メモリ(プロセスが占有する物理メモリ) | 0.1% |
| Disk | string? | ディスク(全物理ドライブの合計利用率) | 0.1 MB/秒 |
| Network | string? | ネットワーク(現在の主要ネットワークの利用率) | 0 Mbps |
| GPU | string? | GPU(全GPUエンジンの最高利用率) | 2.2% |
| GPUEngine | string? | GPUエンジン | GPU 0 - 3D |
| PowerUsage | string? | 電力使用状況(CPU、ディスク、GPUの消費電力への影響) | 低 |
| PowerUsageTrend | string? | 電力使用状況の傾向(一定期間のCPU、ディスク、GPUの消費電力への影響) | 非常に低 |
| Type | string? | プロセスタイプ | アプリ |
| Status | string? | プロセスステータス | 効率モード |
C# クラス SystemProcess を作成してプロセス情報を表現します。
public class SystemProcess
{
public int PID { get; set; }
public string? Name { get; set; }
public string? Publisher { get; set; }
public string? CommandLine { get; set; }
public string? CPU { get; set; }
public string? Memory { get; set; }
public string? Disk { get; set; }
public string? Network { get; set; }
public string? GPU { get; set; }
public string? GPUEngine { get; set; }
public string? PowerUsage { get; set; }
public string? PowerUsageTrend { get; set; }
public string? Type { get; set; }
public string? Status { get; set; }
}
テストデータを定義します。
private SystemProcess _codeWFObject = new SystemProcess()
{
PID = 10565,
Name = "码坊",
Publisher = "沙漠尽头的狼",
CommandLine = "dotnet CodeWF.Tools.dll",
CPU = "2.3%",
Memory = "0.1%",
Disk = "0.1 MB/秒",
Network = "0 Mbps",
GPU = "2.2%",
GPUEngine = "GPU 0 - 3D",
PowerUsage = "低",
PowerUsageTrend = "非常低",
Type = "应用",
Status = "效率模式"
};
2.2. JSONシリアル化の除外
オブジェクトをJSON文字列に変換するのは、Web開発では最も一般的です。簡潔で、フロントエンドとバックエンドの両方で処理しやすいためです。
public class SysteProcessUnitTest
{
private readonly ITestOutputHelper _testOutputHelper;
private SystemProcess _codeWFObject // 前述の定義、ここでは省略
public SysteProcessUnitTest(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
}
/// <summary>
/// Jsonシリアル化サイズテスト
/// </summary>
[Fact]
public void Test_SerializeJsonData_Success()
{
var jsonData = JsonSerializer.Serialize(_codeWFObject);
_testOutputHelper.WriteLine($"Json長さ:{jsonData.Length}");
var jsonDataBytes = Encoding.UTF8.GetBytes(jsonData);
_testOutputHelper.WriteLine($"jsonバイナリ長さ:{jsonDataBytes.Length}");
}
}
標準出力:
Json長さ:366
jsonバイナリ長さ:366
JSONシリアル化はWeb開発で非常に普及しており、簡潔で処理が容易ですが、TCP/UDPネットワーク転送では、不要なパケットサイズの増加(冗長なフィールド名の宣言)を引き起こす可能性があります。したがって、JSONシリアル化を除外し、より効率的なバイナリシリアル化手法を模索します。
{
"PID": 10565,
"Name": "\u7801\u754C\u5DE5\u574A",
"Publisher": "\u6C99\u6F20\u5C3D\u5934\u7684\u72FC",
"CommandLine": "dotnet CodeWF.Tools.dll",
"CPU": "2.3%",
"Memory": "0.1%",
"Disk": "0.1 MB/\u79D2",
"Network": "0 Mbps",
"GPU": "2.2%",
"GPUEngine": "GPU 0 - 3D",
"PowerUsage": "\u4F4E",
"PowerUsageTrend": "\u975E\u5E38\u4F4E",
"Type": "\u5E94\u7528",
"Status": "\u6548\u7387\u6A21\u5F0F"
}
2.3. BinaryWriter を用いたバイナリシリアル化
以前の記事で書いたバイナリシリアル化ヘルパークラス SerializeHelper を使用します。このクラスは BinaryWriter を使用してオブジェクトをバイナリデータに変換します(逆シリアル化には BinaryReader を使用)。
まず、SystemProcess クラスに空インターフェース INetObject を実装させ、クラスに NetHeadAttribute 属性を追加します(パケットヘッダ定義を追加し、複数のネットワークオブジェクトの逆シリアル化識別を容易にします。シリアル化後、システムID、ネットワークオブジェクトID、オブジェクトバージョン番号などのシリアル化補助フィールドが追加されるため、数バイト増加します)。
/// <summary>
/// ネットワークオブジェクトシリアル化インターフェース
/// </summary>
public interface INetObject
{
}
[NetHead(1, 1)]
public class SystemProcess : INetObject
{
// フィールド定義は省略
}
次に、シリアル化と逆シリアル化の正確性を検証し、シリアル化後のバイナリデータ長を出力するテストメソッドを作成します。
/// <summary>
/// バイナリシリアル化テスト
/// </summary>
[Fact]
public void Test_SerializeToBytes_Success()
{
var buffer = SerializeHelper.SerializeByNative(_codeWFObject, 1);
_testOutputHelper.WriteLine($"シリアル化後のバイナリ長さ:{buffer.Length}");
var deserializeObj = SerializeHelper.DeserializeByNative<SystemProcess>(buffer);
Assert.Equal("码坊", deserializeObj.Name);
}
標準出力:
シリアル化後のバイナリ長さ:152
JSON の 366 バイトから半分以下のサイズになりました(366→152、しかもいくつかフィールドが増えています)。上の単体テストではデータの逆シリアル化後の正しさも確認しています。この状態をベースにさらに最適化を進めます。
2.4. データ型の調整
バイナリデータサイズをさらに最適化するため、データ型の調整を行いました。プロセスデータのサンプル分析により、一部のフィールドはよりコンパクトに表現できることがわかりました。例えば、CPU利用率はパーセント記号なしの数値部分(例:2.3)のみ渡せばよく、プロセスタイプはカスタム文字列ではなく列挙値のみ渡せば十分です。このような調整により、パケットサイズを削減できます。
| フィールド名 | データ型 | 説明 | 例 |
|---|---|---|---|
| PID | int | プロセスID | 10565 |
| Name | string? | プロセス名 | 码坊 |
| Publisher | string? | 発行者 | 沙漠尽头的狼 |
| CommandLine | string? | コマンドライン | dotnet CodeWF.Tools.dll |
| CPU | float | CPU(全コアの合計処理利用率) | 2.3 |
| Memory | float | メモリ(プロセスが占有する物理メモリ) | 0.1 |
| Disk | float | ディスク(全物理ドライブの合計利用率) | 0.1 |
| Network | float | ネットワーク(現在の主要ネットワークの利用率) | 0 |
| GPU | float | GPU(全GPUエンジンの最高利用率) | 2.2 |
| GPUEngine | byte | GPUエンジン、0:なし、1:GPU 0 - 3D | 1 |
| PowerUsage | byte | 電力使用状況(CPU、ディスク、GPUの消費電力への影響)、0:非常に低、1:低、2:中、3:高、4:非常に高 | 1 |
| PowerUsageTrend | byte | 電力使用状況の傾向(一定期間のCPU、ディスク、GPUの消費電力への影響)、0:非常に低、1:低、2:中、3:高、4:非常に高 | 0 |
| Type | byte | プロセスタイプ、0:アプリ、1:バックグラウンドプロセス | 0 |
| Status | byte | プロセスステータス、0:正常実行、1:効率モード、2:一時停止 | 1 |
テストデータ定義を変更します。
[NetHead(1, 2)]
public class SystemProcess2 : INetObject
{
public int PID { get; set; }
public string? Name { get; set; }
public string? Publisher { get; set; }
public string? CommandLine { get; set; }
public float CPU { get; set; }
public float Memory { get; set; }
public float Disk { get; set; }
public float Network { get; set; }
public float GPU { get; set; }
public byte GPUEngine { get; set; }
public byte PowerUsage { get; set; }
public byte PowerUsageTrend { get; set; }
public byte Type { get; set; }
public byte Status { get; set; }
}
/// <summary>
/// 通常最適化(フィールドデータ型)
/// </summary>
private SystemProcess2 _codeWFObject2 = new SystemProcess2()
{
PID = 10565,
Name = "码坊",
Publisher = "沙漠尽头的狼",
CommandLine = "dotnet CodeWF.Tools.dll",
CPU = 2.3f,
Memory = 0.1f,
Disk = 0.1f,
Network = 0,
GPU = 2.2f,
GPUEngine = 1,
PowerUsage = 1,
PowerUsageTrend = 0,
Type = 0,
Status = 1
};
単体テストを追加します。
/// <summary>
/// バイナリシリアル化テスト
/// </summary>
[Fact]
public void Test_SerializeToBytes2_Success()
{
var buffer = SerializeHelper.SerializeByNative(_codeWFObject2, 1);
_testOutputHelper.WriteLine($"シリアル化後のバイナリ長さ:{buffer.Length}");
var deserializeObj = SerializeHelper.DeserializeByNative<SystemProcess2>(buffer);
Assert.Equal("码坊", deserializeObj.Name);
Assert.Equal(2.2f, deserializeObj.GPU);
}
テスト結果:
標準出力:
シリアル化後のバイナリ長さ:99
パケットサイズはさらに1/3削減され、152バイトから99バイトになりました。これは一部のフィールドのデータ型を string? から float または byte に変更した成果です。
2.5. 再びデータ型調整とビットフィールド最適化
さらに進んで、ビットフィールド技術を導入します。ビットフィールドを使用すると、メモリ内のフィールドレイアウトをより細かく制御でき、バイナリデータのサイズをさらに削減できます。フィールドルールを再定義し、いくつかの列挙値フィールドにビットフィールドを使用します。これにより、パケットサイズを大幅に削減できます。
前の表と下の表を比較すると、主に2種類のデータ型調整があります。ルールは以下のとおりです。
1つ目:一部のフィールドは単なる列挙値であり、
byte(8ビット)で表現されていますが、例えばプロセスタイプは2つの状態(0:アプリ、1:バックグラウンドプロセス)しかなく、1ビット(0、1)で表現できます。電力使用状況は5状態なので、3ビットで十分です(6状態まで表現可能)。2つ目:一部のfloatデータ型は、実際には小数点以下1桁までしか要求しません。パーセンテージを表す値は100.0%を超えないため、整数化を検討します。例えば23.3%の場合、23.3を送信する代わりに10倍して233を送信し、最大値は1000(100.0%)を超えません。別のプロセスはデータを解析した後、10で割って使用します。これにより、floatの4バイト(32ビット)から10ビット(最大値1024)に最適化できます。
このルールに従ってフィールドルールを再定義します。
| フィールド名 | データ型 | 説明 | 例 |
|---|---|---|---|
| PID | int | プロセスID | 10565 |
| Name | string? | プロセス名 | 码坊 |
| Publisher | string? | 発行者 | 沙漠尽头的狼 |
| CommandLine | string? | コマンドライン | dotnet CodeWF.Tools.dll |
| Data | byte[8] | 固定サイズのいくつかのフィールド。なぜ8バイト長なのか?(注:逆シリアル化時にはbyte[]長さを表す4バイトが追加されるため、Dataフィールドは合計12バイトを占める) |
固定フィールド (Data) の詳細説明:
| フィールド名 | Offset | Size | 説明 | 例 |
|---|---|---|---|---|
| CPU | 0 | 10 | CPU(全コアの合計処理利用率)、最後の桁が小数部を表す。例:23 は 2.3% を意味する | 23 |
| Memory | 10 | 10 | メモリ(プロセスが占有する物理メモリ)、最後の桁が小数部を表す。例:1 は 0.1% | 1 |
| Disk | 20 | 10 | ディスク(全物理ドライブの合計利用率)、最後の桁が小数部を表す。例:1 は 0.1% | 1 |
| Network | 30 | 10 | ネットワーク(現在の主要ネットワークの利用率)、最後の桁が小数部を表す。例:253 は 25.3% | 0 |
| GPU | 40 | 10 | GPU(全GPUエンジンの最高利用率)、最後の桁が小数部を表す。例:22 は 2.2% | 22 |
| GPUEngine | 50 | 1 | GPUエンジン、0:なし、1:GPU 0 - 3D | 1 |
| PowerUsage | 51 | 3 | 電力使用状況(CPU、ディスク、GPUの消費電力への影響)、0:非常に低、1:低、2:中、3:高、4:非常に高 | 1 |
| PowerUsageTrend | 54 | 3 | 電力使用状況の傾向(一定期間のCPU、ディスク、GPUの消費電力への影響)、0:非常に低、1:低、2:中、3:高、4:非常に高 | 0 |
| Type | 57 | 1 | プロセスタイプ、0:アプリ、1:バックグラウンドプロセス | 0 |
| Status | 58 | 2 | プロセスステータス、0:正常実行、1:効率モード、2:一時停止 | 1 |
上の表は、固定フィールドの一部のビットフィールドルールです。Offset は Data バイト配列内でのフィールドの開始位置(ビット単位)、Size は占有サイズ(ビット単位)を示します。例えば Memory フィールドは、Data 配列内で 10 ビット目から 20 ビット目までを占有します。
これにより、固定サイズで元々25バイト長だった10個のフィールドを8バイトに最適化できました(5つのfloat:4バイト(32ビット)→10ビット、単一バイト(8ビット)→2ビット、4ビット、6ビットなど。つまり 200ビット(25*8)→64ビット(実際は60ビットですが、ネットワーク転送の最小単位はバイトのため、切り上げて8バイト(64ビット))。
クラス定義を以下のように変更します。コード内のコメントに注意してください。
[NetHead(1, 3)]
public class SystemProcess3 : INetObject
{
public int PID { get; set; }
public string? Name { get; set; }
public string? Publisher { get; set; }
public string? CommandLine { get; set; }
private byte[]? _data;
/// <summary>
/// シリアル化対象。実際にシリアル化するデータ
/// </summary>
public byte[]? Data
{
get => _data;
set
{
_data = value;
// 重要:逆シリアル化時にbyteをオブジェクトに変換する(ビットフィールド操作)
_processData = _data?.ToFieldObject<SystemProcessData>();
}
}
private SystemProcessData? _processData;
/// <summary>
/// プロセスデータ。NetIgnoreMember属性を付与するとシリアル化で無視される
/// </summary>
[NetIgnoreMember]
public SystemProcessData? ProcessData
{
get => _processData;
set
{
_processData = value;
// 重要:オブジェクトをbyte[]に変換する(ビットフィールドシリアル化操作)
_data = _processData?.FieldObjectBuffer();
}
}
}
public record SystemProcessData
{
[NetFieldOffset(0, 10)] public short CPU { get; set; }
[NetFieldOffset(10, 10)] public short Memory { get; set; }
[NetFieldOffset(20, 10)] public short Disk { get; set; }
[NetFieldOffset(30, 10)] public short Network { get; set; }
[NetFieldOffset(40, 10)] public short GPU { get; set; }
[NetFieldOffset(50, 1)] public byte GPUEngine { get; set; }
[NetFieldOffset(51, 3)] public byte PowerUsage { get; set; }
[NetFieldOffset(54, 3)] public byte PowerUsageTrend { get; set; }
[NetFieldOffset(57, 1)] public byte Type { get; set; }
[NetFieldOffset(58, 2)] public byte Status { get; set; }
}
単体テストを追加します。
/// <summary>
/// 限界最適化(フィールドデータ型)
/// </summary>
private SystemProcess3 _codeWFObject3 = new SystemProcess3()
{
PID = 10565,
Name = "码坊",
Publisher = "沙漠尽头的狼",
CommandLine = "dotnet CodeWF.Tools.dll",
ProcessData = new SystemProcessData()
{
CPU = 23,
Memory = 1,
Disk = 1,
Network = 0,
GPU = 22,
GPUEngine = 1,
PowerUsage = 1,
PowerUsageTrend = 0,
Type = 0,
Status = 1
}
};
/// <summary>
/// バイナリ限界シリアル化テスト
/// </summary>
[Fact]
public void Test_SerializeToBytes3_Success()
{
var buffer = SerializeHelper.SerializeByNative(_codeWFObject3, 1);
_testOutputHelper.WriteLine($"シリアル化後のバイナリ長さ:{buffer.Length}");
var deserializeObj = SerializeHelper.DeserializeByNative<SystemProcess3>(buffer);
Assert.Equal("码坊", deserializeObj.Name);
Assert.Equal(23, deserializeObj.ProcessData.CPU);
Assert.Equal(1, deserializeObj.ProcessData.PowerUsage);
}
テスト出力:
標準出力:
シリアル化後のバイナリ長さ:86
99 バイトから 86 バイトへ、13 バイトの削減です。ネットワークが制限された環境では非常に大きな効果で、例えば 100 万件のデータでは 12.4MB もの差になります。ビットフィールドのシリアル化と逆シリアル化のコードについては、ここでは詳細を述べません(退屈で、説明が難しいかもしれません)。コードは以下のようになります。
public partial class SerializeHelper
{
public static byte[] FieldObjectBuffer<T>(this T obj) where T : class
{
var properties = typeof(T).GetProperties();
var totalSize = 0;
// 合計ビット長を計算
foreach (var property in properties)
{
if (!Attribute.IsDefined(property, typeof(NetFieldOffsetAttribute)))
{
continue;
}
var offsetAttribute =
(NetFieldOffsetAttribute)property.GetCustomAttribute(typeof(NetFieldOffsetAttribute))!;
totalSize = Math.Max(totalSize, offsetAttribute.Offset + offsetAttribute.Size);
}
var bufferLength = (int)Math.Ceiling((double)totalSize / 8);
var buffer = new byte[bufferLength];
foreach (var property in properties)
{
if (!Attribute.IsDefined(property, typeof(NetFieldOffsetAttribute)))
{
continue;
}
var offsetAttribute =
(NetFieldOffsetAttribute)property.GetCustomAttribute(typeof(NetFieldOffsetAttribute))!;
dynamic value = property.GetValue(obj)!; // dynamic型で動的にプロパティ値を取得
SetBitValue(ref buffer, value, offsetAttribute.Offset, offsetAttribute.Size);
}
return buffer;
}
public static T ToFieldObject<T>(this byte[] buffer) where T : class, new()
{
var obj = new T();
var properties = typeof(T).GetProperties();
foreach (var property in properties)
{
if (!Attribute.IsDefined(property, typeof(NetFieldOffsetAttribute)))
{
continue;
}
var offsetAttribute =
(NetFieldOffsetAttribute)property.GetCustomAttribute(typeof(NetFieldOffsetAttribute))!;
dynamic value = GetValueFromBit(buffer, offsetAttribute.Offset, offsetAttribute.Size,
property.PropertyType);
property.SetValue(obj, value);
}
return obj;
}
/// <summary>
/// 値をビット単位でbufferに書き込む
/// </summary>
/// <param name="buffer"></param>
/// <param name="value"></param>
/// <param name="offset"></param>
/// <param name="size"></param>
private static void SetBitValue(ref byte[] buffer, int value, int offset, int size)
{
var mask = (1 << size) - 1;
buffer[offset / 8] |= (byte)((value & mask) << (offset % 8));
if (offset % 8 + size > 8)
{
buffer[offset / 8 + 1] |= (byte)((value & mask) >> (8 - offset % 8));
}
}
/// <summary>
/// bufferからビット単位で値を読み出す
/// </summary>
/// <param name="buffer"></param>
/// <param name="offset"></param>
/// <param name="size"></param>
/// <param name="propertyType"></param>
/// <returns></returns>
private static dynamic GetValueFromBit(byte[] buffer, int offset, int size, Type propertyType)
{
var mask = (1 << size) - 1;
var bitValue = (buffer[offset / 8] >> (offset % 8)) & mask;
if (offset % 8 + size > 8)
{
bitValue |= (buffer[offset / 8 + 1] << (8 - offset % 8)) & mask;
}
dynamic result = Convert.ChangeType(bitValue, propertyType); // プロパティの型に応じて変換
return result;
}
}
3. 最適化の効果とまとめ
段階的な最適化により、初期のJSONシリアル化(366バイト)から、通常のバイナリシリアル化(152バイト)、そしてビットフィールド技術を用いた86バイトへとサイズを削減できました。この最適化はネットワーク転送、特に大量のデータを転送する必要がある場合に非常に効果的です。
本稿では、サンプルケースを通じて、C#オブジェクトのバイナリシリアル化最適化手法を考察しました。ビットフィールド技術を使用することで、パケットサイズを極限まで圧縮し、ネットワーク転送の効率を向上させることができました。これはC/Sプログラム開発における楽しみの一つであり、究極のパフォーマンスを追求する姿勢の表れです。
最後に、本稿のテストソースコードのGitHubリンクを提供します。読者の参考と学習に役立ててください。
おまけ:このリポジトリには、前回の記事『C#百万オブジェクトシリアル化深層解析:ネットワーク転送で速度とサイズの完璧なバランスを実現する方法 (dotnet9.com)』のサンプルコードも含まれており、TCP、UDPのサーバー・クライアント結合テストプログラムも同梱されています。