ラベル class の投稿を表示しています。 すべての投稿を表示
ラベル class の投稿を表示しています。 すべての投稿を表示

2015年6月20日

クラスと構造体のインターフェースメソッド呼び出し

IComparer<T>IEqualityComparer<T> などのインターフェースを実装する際、クラスと構造体のどちらがいいのか迷ったことがあったので、インターフェースにキャストしたクラス・構造体からの、メソッド呼び出し速度を測定してみた。

[測定コード]

static class MeasureMethodCall
{
    static void Main(string[] args)
    {
        var size = 10000000;
        var cmpc = new ComparerClass();  // クラスベース
        var cmps = new ComparerStruct(); // 構造体ベース

        for( var i=0; i<11; i++ ){
            Console.WriteLine("[{0}]", i);

            // インターフェースにキャストした状態からのメソッド呼び出し
            InterfaceCall(size, cmpc);
            InterfaceCall(size, cmps);

            // おまけ:直接のメソッド呼び出し
            DirectCall(size, cmpc);
            DirectCall(size, cmps);
        }
    }

    static void InterfaceCall(int size, IComparer<int> comparer)
    {
        var sw = Stopwatch.StartNew();
        for( var i=0; i<size; i++ ) comparer.Compare(0, i);
        sw.Stop();
        Console.WriteLine("[InterfaceCall][{0}][{1:struct;0;class }] {2}",
            size, comparer.GetType().IsValueType.GetHashCode(), sw.Elapsed);
    }

    static void DirectCall<T>(int size, T comparer) where T : IComparer<int>
    {
        var sw = Stopwatch.StartNew();
        for( var i=0; i<size; i++ ) comparer.Compare(0, i);
        sw.Stop();
        Console.WriteLine("[DirectCall   ][{0}][{1:struct;0;class }] {2}",
            size, comparer.GetType().IsValueType.GetHashCode(), sw.Elapsed);
    }
}

class ComparerClass : IComparer<int>
{
    public int Compare(int x, int y){ return x.CompareTo(y); }
}

struct ComparerStruct : IComparer<int>
{
    public int Compare(int x, int y){ return x.CompareTo(y); }
}
[結果]
インターフェース(クラス) 0.0758722
インターフェース(構造体) 0.1012821
クラス 0.0674410
構造体 0.0083828
※コマンドプロンプトで実施(csc /optimize)
※2 回目以降は類似の結果だったため、2 回目の結果を採択
※単位は[s]

まとめると、
構造体 ≫ クラス > インターフェース(クラス) > インターフェース(構造体)
※左の方が速い

当たり前だが、インターフェースにキャストした状態からのメソッド呼び出しは、直接呼び出しよりも遅い。
そして、インターフェースにキャストした状態では、クラスより構造体の方が遅い。
理由はたぶん、インターフェース(構造体)のメソッド呼び出しは、自身(構造体インスタンス)のボックス化解除が必要だから?

インターフェースを引数に取るメソッドが、上記のように where 制約で実装されていれば構造体がベストだが、.NET Framework の標準メソッドの多くはインタフェース引数に where 制約を使っていない。
where 制約を使うと、型引数が増えて呼び出し側での指定が必要になり、コードが汚くなるからだと思われる。(参考:メソッドの型推論で型パラメータの制約は使われない

結論としては、一般的な使い方をするなら、インターフェースはクラスで実装した方がいい。
※プロパティも実質メソッドなので、プロパティを実装するインターフェースについても同様

実際に Array.SortEnumerable.OrderBy で測定したところ、インターフェース(クラス)の方がいい結果になった。

検証環境

Windows 7 64bit/.NET 4.5.2
Intel(R) Celeron(R) CPU G530 @ 2.40GHz/DDR3 8GB(4GB x 2)

2014年6月16日

クラス(class) vs 構造体(struct)

.NET の型は、クラス(class/Class)構造体(struct/Structure)の2つに大きく分けられる。

クラスは参照型とも呼ばれ、その変数はデータがあるメモリへのアドレス(参照)を保持している。
構造体は値型とも呼ばれ、その変数はデータを直接保持している。
クラスのデータが置かれる場所をヒープ、構造体のデータが置かれる場所をスタックと呼ぶ。

クラスと構造体は両方ともデータとメソッドを持てるので、慣れないうちはどちらを使えばいいか迷うと思う。
とりあえず、使い分けの基準になりそうな違いを挙げる。
  1. 代入(引数への値渡し)時と、ボックス化&ボックス化解除(キャスト)時、構造体はデータをコピーする
    クラスはアドレスのコピーのみ
    ※ 構造体のサイズが大きいほど、代入のコストは大きくなる
  2. 構造体は null にならない
    ※ null とはクラス変数がデータ(参照先)を持ってないことを表すアドレス
  3. インスタンス作成(new)の速度は、クラスより構造体の方が速い
  4. 構造体は GC の対象にならない (スコープから外れた時点で、自動的に消える)
  5. 構造体は他の構造体やクラスから継承できず、継承元にもならない (ただし、インターフェースは実装可能)

補足

  1. についてコードで説明

class ElementC
{
    public ElementC(int value) { Value = value; }
    public int Value;
}
struct ElementS
{
    public ElementS(int value) { Value = value; }
    public int Value;
}
static class Program
{
    static void Main(string[] args)
    {
        /* クラス変数の代入 */
        var ec = new ElementC(1);
        Console.WriteLine(ec.Value);  // 1
        var ec_ = ec; /* クラスの代入はアドレスのコピー */
        ec_.Value = 2; /* 同じデータを指しているので ec_ を変更すると ec も変わる */
        /* 結果 */
        Console.WriteLine(ec_.Value); // 2
        Console.WriteLine(ec.Value);  // 2

        /* 構造体変数の代入 */
        var es = new ElementS(10);
        Console.WriteLine(es.Value);  // 10
        var es_ = es; /* 構造体の代入はデータのコピー */
        es_.Value = 20; /* コピーなので es_ を変更しても es には変わらない */
        /* 結果 */
        Console.WriteLine(es_.Value); // 20
        Console.WriteLine(es.Value);  // 10
    }
}
引数の値渡しも代入と同様。「=」が無いため、意識しにくいかも。
List<T> 使用時の間違い易いパターン。

/* ElementC, ElementS の定義は上と同じ */
static class Program
{
    static void Main(string[] args)
    {
        /* クラス変数の値渡し */
        var ecs = new List<ElementC>();
        var ec = new ElementC(1);
        ecs.Add(ec); /* アドレスのコピー */
        Console.WriteLine(ecs[0].Value); // 1
        ec.Value = 2; /* ec を変更すると ecs[0] も変わる */
        /* 結果 */
        Console.WriteLine(ec.Value);     // 2
        Console.WriteLine(ecs[0].Value); // 2

        /* 構造体変数の値渡し */
        var ess = new List<ElementS>();
        var es = new ElementS(10);
        ess.Add(es); /* データのコピー */
        Console.WriteLine(ess[0].Value); // 10
        es.Value = 20; /* es を変更しても ess[0] は変わらない*/
        /* 結果 */
        Console.WriteLine(es.Value);     // 20
        Console.WriteLine(ess[0].Value); // 10
    }
}

  1. について
例外的な構造体として、Nullable<T> が存在するが、これは本当に null になるわけではない。
Nullable<T> は下記のように定義されてる。

public struct Nullable<T> where T : struct
{
    private bool hasValue;
    internal T value;
    /* 以降、省略 */
}
Nullable<T> が null を代入できたり、null と比較できたりするのは、コンパイラが該当する処理に置き換えているためである。
つまり、シンタックスシュガーになっている。

その他の違い

下記が分かり易くまとめられてる。

おまけ

unsafe を使えば、構造体のアドレスを取得できる。
ちょっと反則っぽいが・・・

/* ElementS の定義は上と同じ */
static class Program
{
    static void Main(string[] args)
    {
        var es = new ElementS(10);
        Console.WriteLine(es.Value);  // 10
        unsafe {
            var esp = &es;   /* es のアドレス取得 */
            esp->Value = 30; /* esp の参照先を変更すると、es も変わる */
            Console.WriteLine(esp->Value); // 30
        }
        Console.WriteLine(es.Value); // 30
    }
}

ちなみに、es を object 変数に代入し、その変数を ElementS でキャストして Value を変更するとかはできない。
コンパイルエラーになる。(コンパイラ エラー CS0445