2010/09/29

C#の構造体の引き渡し方によるパフォーマンスの違い

2016/02/20追記--
yosolaさんの指摘の通り調査用のコードにミスがありました。
このエントリの結果は間違いです。調査用のコードを修正したエントリをご覧ください。
--

メソッドの引数として構造体を渡す場合にちょっと気になったので調査用のコードを書いた。
比較したのは次の3つのケース。
  • 構造体を値渡し
  • 構造体を参照渡し(ref修飾子)
  • 構造体が実装するインターフェースで渡す
コードは次の通り。struct Pointがinterface ITupleを実装している。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;

namespace StructInterface
{
    interface ITuple
    {
        int X { get; set; }
        int Y { get; set; }
        int Z { get; set; }
    }

    struct Point : ITuple
    {
        public int X { get; set; }
        public int Y { get; set; }
        public int Z { get; set; }
        public Point(int x, int y, int z) : this() { X = x; Y = y; Z = z; }
        public void AddStruct(Point point)
        {
            X += point.X;
            Y += point.Y;
            Z += point.Z;
        }
        public void AddRefStruct(ref Point point)
        {
            X += point.X;
            Y += point.Y;
            Z += point.Z;
        }
        public void AddInterface(ITuple tuple)
        {
            X += tuple.X;
            Y += tuple.Y;
            Z += tuple.Z;
        }
        public override bool Equals(object obj)
        {
            if (!(obj is Point))
                return false;
            var p = (Point)obj;
            var result = X == p.X && Y == p.Y && Z == p.Z;
            return result;
        }
        public override int GetHashCode()
        {
            var result = X ^ Y ^ Z;
            return result;
        }
        public override string ToString()
        {
            var str = string.Format("{0}, {1}, {2}, ", X, Y, Z);
            return str;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            for (var size = 1000; size <= 1000000; size *= 10)
                Run(size);
        }

        static void Run(int size)
        {
            Console.WriteLine("size: {0}", size);

            var count = 10;
            var sw = new Stopwatch();
            var structTicks = 0L;
            var refStructTicks = 0L;
            var interfaceTicks = 0L;

            for (var c = 0; c <= count; c++)
            {
                var random = new Random(count);
                var points = Enumerable.Range(0, size)
                                       .Select(_ => new Point(random.Next(), random.Next(), random.Next()))
                                       .ToList();

                // Point.AddStruct
                sw.Reset();
                sw.Start();
                var sp = new Point();
                foreach (var point in points)
                    sp.AddStruct(point);
                sw.Stop();
                structTicks += sw.ElapsedTicks;

                // Point.AddRefStruct
                sw.Reset();
                sw.Start();
                var rsp = new Point();
                foreach (var point in points)
                {
                    var p = new Point(point.X, point.Y, point.Z);
                    rsp.AddRefStruct(ref p);
                }
                sw.Stop();
                refStructTicks = sw.ElapsedTicks;

                // Point.AddInterface
                sw.Reset();
                sw.Start();
                var ip = new Point();
                foreach (var point in points)
                    ip.AddInterface(point);
                sw.Stop();
                interfaceTicks += sw.ElapsedTicks;


                // correctness
                if (!sp.Equals(rsp))
                    Console.WriteLine("!sp.Equals(rsp)");
                if (!sp.Equals(ip))
                    Console.WriteLine("!sp.Equals(ip)");
            }

            Console.WriteLine("AddStruct: {0} ({1:F})", structTicks / count, structTicks / (double)structTicks);
            Console.WriteLine("AddRefStruct: {0} ({1:F})", refStructTicks / count, refStructTicks / (double)structTicks);
            Console.WriteLine("AddInterface: {0} ({1:F})", interfaceTicks / count, interfaceTicks / (double)structTicks);
        }
    }
}
ksksts / junk / source — bitbucket.org

結果は次の通り。
size: 1000
AddStruct: 832 (1.00)
AddRefStruct: 31 (0.04)
AddInterface: 789 (0.95)
size: 10000
AddStruct: 1023 (1.00)
AddRefStruct: 119 (0.12)
AddInterface: 1803 (1.76)
size: 100000
AddStruct: 9916 (1.00)
AddRefStruct: 1100 (0.11)
AddInterface: 16277 (1.64)
size: 1000000
AddStruct: 102814 (1.00)
AddRefStruct: 11699 (0.11)
AddInterface: 159081 (1.55)
感想としては、はじめ参照渡しが早いことを意外に感じた(値型と参照型のインスタンス作成の速度差から値型のインスタンスのコピーはそれほどの負荷にならないと思っていた)けど、コピーを発生させるよりは参照渡しにした方が軽いということは納得できた。
インターフェース経由が遅いのは疑問。型変換的な処理が入ってしまうのかな。

メソッドの引数に構造体を渡す場合に(変更しないのに)ref修飾子を使いまくるか、素直にコピーを発生させるか迷う。

4 件のコメント:

yosola さんのコメント...

だいぶ前に書かれた記事へのコメント失礼します。

こちらの記事を参考にさせて頂いていたのですが、
参照渡しのTick数をカウントする箇所のみ
'+=' ではなく '=' になっているため
目的の比較が出来ていないのではないでしょうか?

匿名 さんのコメント...

yosola の コメントに助かりました。。

非常に興味深い記事だけに。

ksksts さんのコメント...

yosolaさん
ミスのご指摘ありがとうございます。せっかくコメントくださったのに気づくのが遅くなってすみません。
参照渡し(AddRefStruct)が異常に速度的なパフォーマンスがいい結果になってしまったのはご指摘いただいた通りのミスが原因です。あとでミスがあったことが分かる形で更新します。

匿名さん
ミスリードしそうになってすみません。

通りすがり さんのコメント...

C#はあまり詳しくないのですが、基本refはCのポインタ渡しと同様だと思われます。値渡しの場合、スタック上でのコピーが発生するので、処理にかかる時間は構造体のサイズに比例します。一方で、ポインタ(オブジェクトのアドレスを格納するための領域。4 byteか8 byteだと思われます)のサイズは一定なので、構造体のサイズがどうであれ、コピーにかかるコストは一定です。C#のコンパイラがどうなっているか不明ですが、inlineなどで最適化されている場合については、参照渡しはしばしばアドレスの直接参照に置き換わるため、実質ゼロコストになるケースもあると思われます(そうなるかどうかは、コンパイラ依存です)。インターフェイス経由で時間がかかるのは当然で、インターフェイスはポインタを間接参照することで値を引っ張るので、メソッド呼び出しのところで時間を食っているのだと思われます(アクセスせずに渡すだけなら、refと同じなのでは?)。