閉じる

【C#】sizeof演算子の使い方|構造体サイズ取得の注意点とMarshalとの違い

C#において、データのメモリサイズを把握することは、パフォーマンスの最適化や低レイヤーなシステムプログラミング、あるいは外部のC++ライブラリとの相互運用(Interop)において非常に重要です。

その中心となるのがsizeof演算子です。

この演算子は、指定した型のインスタンスがメモリ上で占有するバイト数を返します。

しかし、C#のsizeofは、単にサイズを測るだけのツールではありません。

対象となる型が「アンマネージ型」である必要があるほか、構造体のパディング(詰め物)による影響や、実行時の振る舞いが異なるMarshal.SizeOfとの使い分けなど、正しく理解しておくべき注意点がいくつか存在します。

本記事では、sizeof演算子の基本的な使い方から、構造体におけるメモリレイアウトの仕組み、そして実務で迷いやすいMarshalクラスとの違いまでを詳しく解説します。

sizeof演算子の基本と定義済みの型

sizeof演算子は、C#の組み込み型(プリミティブ型)のサイズを取得する際に最も頻繁に利用されます。

まずは、どのような型に対して使用でき、どのような結果が得られるのかを確認しましょう。

組み込み型に対するsizeofの挙動

C#のプリミティブ型に対してsizeofを使用する場合、その結果はコンパイル時に定数として決定されます。

これは、実行時のオーバーヘッドが全くないことを意味します。

以下のサンプルコードで、代表的な型のサイズを確認してみましょう。

C#
using System;

public class Program
{
    public static void Main()
    {
        // 組み込み型のサイズを表示
        // これらは unsafe キーワードなしで使用可能
        Console.WriteLine($"bool:    {sizeof(bool)} byte");
        Console.WriteLine($"char:    {sizeof(char)} bytes");
        Console.WriteLine($"byte:    {sizeof(byte)} byte");
        Console.WriteLine($"int:     {sizeof(int)} bytes");
        Console.WriteLine($"long:    {sizeof(long)} bytes");
        Console.WriteLine($"float:   {sizeof(float)} bytes");
        Console.WriteLine($"double:  {sizeof(double)} bytes");
        Console.WriteLine($"decimal: {sizeof(decimal)} bytes");
    }
}
実行結果
bool:    1 byte
char:    2 bytes
byte:    1 byte
int:     4 bytes
long:    8 bytes
float:   4 bytes
double:  8 bytes
decimal: 16 bytes

C#のchar型は、内部的にUnicode(UTF-16)を使用しているため、1バイトではなく2バイトである点に注意してください。

また、bool型は概念的には1ビットで十分ですが、メモリの最小単位である1バイトを占有します。

sizeofが使用できる「アンマネージ型」の条件

sizeof演算子は、あらゆる型に使えるわけではありません。

対象はアンマネージ型(Unmanaged types)に限定されています。

アンマネージ型とは、以下のいずれかに該当する型を指します。

  • sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool
  • 列挙型(enum)
  • ポインタ型
  • フィールドにアンマネージ型のみを持つユーザー定義の構造体(struct)

逆に、クラス(class)や、参照型をフィールドに含む構造体、配列、文字列(string)に対しては、sizeofを使用することはできません。

これらはガベージコレクション(GC)の管理対象となるため、メモリ上のサイズが固定されない、あるいは参照という間接的な構造を持っているためです。

unsafeコンテキストと構造体での利用

組み込み型以外のユーザー定義構造体でsizeofを使用する場合、少し特殊なルールが適用されます。

それは、unsafeコードブロック内での実行が必要になるという点です。

なぜunsafeが必要なのか

C#の設計思想として、ポインタ操作や直接的なメモリサイズの参照は、安全性を損なう可能性があると考えられています。

そのため、プリミティブ型以外の構造体に対してsizeofを適用する場合、開発者が明示的に「安全ではないコードを書いている」と宣言する必要があります。

構造体での実装例

以下のコードは、独自の構造体を作成し、そのサイズを取得する例です。

C#
using System;

// 単純な構造体
struct Point
{
    public int X;
    public int Y;
}

public class Program
{
    public static void Main()
    {
        // ユーザー定義構造体のサイズ取得には unsafe が必要
        unsafe
        {
            Console.WriteLine($"Point struct size: {sizeof(Point)} bytes");
        }
    }
}
実行結果
Point struct size: 8 bytes

このコードを実行するためには、プロジェクトのプロパティ(または .csproj ファイル)で「アンセーフコードの許可(AllowUnsafeBlocks)」を有効にする必要があります。

unsafeを使わない代替案:Unsafe.SizeOf

最新のC#(.NET Core以降)では、System.Runtime.CompilerServices名前空間にあるUnsafe.SizeOf<T>()メソッドを使用することで、unsafeキーワードを使わずに構造体のサイズを取得することも可能です。

C#
using System;
using System.Runtime.CompilerServices;

struct Data
{
    public int Id;
    public double Value;
}

public class Program
{
    public static void Main()
    {
        // Unsafe.SizeOf なら unsafe ブロックが不要
        int size = Unsafe.SizeOf<Data>();
        Console.WriteLine($"Data struct size: {size} bytes");
    }
}
実行結果
Data struct size: 16 bytes

このメソッドは、ジェネリック型引数を受け取り、実行時にそのサイズを返します。

内部的にはsizeof命令にコンパイルされるため、パフォーマンスも非常に高いです。

構造体サイズとメモリパディングの注意点

sizeofを使用する際に最も注意が必要なのが、メモリパディング(Padding)です。

構造体のサイズは、単純にフィールドのサイズの合計になるとは限りません。

パディングが発生する理由

CPUがメモリにアクセスする際、特定の境界(4バイト境界や8バイト境界など)に合わせてデータを配置した方が、アクセス効率が良くなります。

これを「メモリのアライメント(整列)」と呼びます。

この調整のために、フィールドの間に空のデータ(パディング)が挿入されることがあります。

パディングの具体例

以下のコードで、パディングの影響を確認してみましょう。

C#
using System;
using System.Runtime.InteropServices;

struct SampleStruct
{
    public byte A;   // 1バイト
    // ここに3バイトのパディングが挿入される
    public int B;    // 4バイト
}

public class Program
{
    public static void Main()
    {
        unsafe
        {
            Console.WriteLine($"Size of byte: {sizeof(byte)}");
            Console.WriteLine($"Size of int:  {sizeof(int)}");
            Console.WriteLine($"Size of SampleStruct: {sizeof(SampleStruct)}");
        }
    }
}
実行結果
Size of byte: 1
Size of int:  4
Size of SampleStruct: 8

byte(1)とint(4)を合わせると5バイトですが、sizeofの結果は8バイトとなります。

これは、int型のフィールドを4バイトの境界に配置するために、byte型の後ろに3バイトの隙間が作られたためです。

StructLayoutによる制御

この挙動を制御したい場合(例えば、通信プロトコルやバイナリフォーマットに厳密に合わせる必要がある場合)、[StructLayout]属性を使用します。

C#
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct PackedStruct
{
    public byte A;
    public int B;
}

このようにPack = 1を指定すると、パディングが最小限(1バイト単位)になり、この構造体のsizeof5バイトになります。

ただし、アライメントが崩れるため、特定のCPUアーキテクチャではパフォーマンスが低下する可能性がある点に注意してください。

sizeof と Marshal.SizeOf の違い

C#には、型のサイズを取得する方法がもう一つあります。

それがSystem.Runtime.InteropServices.Marshal.SizeOfメソッドです。

この2つは似て非なるものであり、状況に応じて使い分ける必要があります。

両者の主な違い

最も大きな違いは、sizeofが「マネージドメモリ上のサイズ」を返すのに対し、Marshal.SizeOfは「アンマネージド(ネイティブ)メモリに転送(マーシャリング)された時のサイズ」を返す点です。

特徴sizeof 演算子Marshal.SizeOf メソッド
評価タイミングコンパイル時(またはJIT時)実行時(ランタイム)
対象アンマネージ型のみクラスや構造体(条件あり)
unsafe必要(ユーザー定義構造体の場合)不要
戻り値の意味C#内部でのメモリ占有量ネイティブ転送時のメモリ占有量
パフォーマンス非常に高速(定数)やや低速(リフレクション等を使用)

Marshal.SizeOf が必要なケース

例えば、C#のbool型は1バイトですが、Win32 APIなどのネイティブ環境ではBOOL型(4バイトの整数)として扱われることが一般的です。

C#
using System;
using System.Runtime.InteropServices;

struct NativeCompatible
{
    public bool Flag;
}

public class Program
{
    public static void Main()
    {
        unsafe
        {
            // C#内部でのサイズ
            Console.WriteLine($"sizeof: {sizeof(NativeCompatible)}");
        }

        // ネイティブ転送時のサイズ
        Console.WriteLine($"Marshal.SizeOf: {Marshal.SizeOf(typeof(NativeCompatible))}");
    }
}

実行結果(環境により異なる場合があります):

実行結果
sizeof: 1
Marshal.SizeOf: 4

このように、外部ライブラリとのデータのやり取りを行う場合は Marshal.SizeOf を使用し、純粋にC#の内部処理やメモリ効率を考える場合は sizeof を使用するのが鉄則です。

実践的な活用:ジェネリック型とsizeof

C#のsizeofは、ジェネリック型引数に対して直接使用することはできません。

例えば、sizeof(T)のような記述はコンパイルエラーとなります。

なぜジェネリックで使えないのか

sizeofはコンパイル時にサイズが確定している必要がありますが、ジェネリック型Tのサイズは、実際にどのような型が当てはめられるかまで分からないためです。

解決策:Unsafe.SizeOf<T> の活用

前述したUnsafe.SizeOf<T>は、この問題を解決します。

これは実行時にその型のサイズを解決できるため、ジェネリックなバッファ処理などで非常に重宝します。

C#
using System;
using System.Runtime.CompilerServices;

public class BufferManager<T> where T : struct
{
    public void PrintTypeSize()
    {
        // sizeof(T) は不可だが、Unsafe.SizeOf<T> なら可能
        int size = Unsafe.SizeOf<T>();
        Console.WriteLine($"The size of type {typeof(T).Name} is {size} bytes.");
    }
}

public class Program
{
    public static void Main()
    {
        var intManager = new BufferManager<int>();
        intManager.PrintTypeSize();

        var doubleManager = new BufferManager<double>();
        doubleManager.PrintTypeSize();
    }
}
実行結果
The size of type Int32 is 4 bytes.
The size of type Double is 8 bytes.

このように、where T : struct(あるいはより厳密なwhere T : unmanaged)の制約と組み合わせることで、汎用的なメモリ操作ライブラリを作成することができます。

まとめ

C#のsizeof演算子は、メモリ上のデータサイズを効率的に取得するための強力なツールです。

最後に、本記事の内容を整理します。

  • 組み込み型に対しては常に使用可能で、コンパイル時に定数として処理される。
  • ユーザー定義構造体で使用する場合は、unsafeコンテキストが必要。
  • 構造体にはメモリパディングが発生するため、単純な合計サイズよりも大きくなることがある。
  • Marshal.SizeOfはネイティブ連携用、sizeofはマネージド内のメモリ管理用と使い分ける。
  • モダンなC#では、Unsafe.SizeOf<T>を使うことで、より柔軟にサイズ取得が可能。

メモリレイアウトを意識したプログラミングは、大規模なデータ処理や高速なレスポンスが求められるアプリケーションにおいて、大きな武器となります。

ぜひsizeofを正しく使いこなし、ワンランク上のC#プログラミングを目指してください。

変数・データ型・演算子

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!