yosolaさんの指摘の通り調査用のコードにミスがありました。
このエントリの結果は間違いです。調査用のコードを修正したエントリをご覧ください。
--
メソッドの引数として構造体を渡す場合にちょっと気になったので調査用のコードを書いた。
比較したのは次の3つのケース。
- 構造体を値渡し
- 構造体を参照渡し(ref修飾子)
- 構造体が実装するインターフェースで渡す
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())) .ToListksksts / junk / source — bitbucket.org(); // 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); } } }
結果は次の通り。
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修飾子を使いまくるか、素直にコピーを発生させるか迷う。
だいぶ前に書かれた記事へのコメント失礼します。
返信削除こちらの記事を参考にさせて頂いていたのですが、
参照渡しのTick数をカウントする箇所のみ
'+=' ではなく '=' になっているため
目的の比較が出来ていないのではないでしょうか?
yosola の コメントに助かりました。。
返信削除非常に興味深い記事だけに。
yosolaさん
返信削除ミスのご指摘ありがとうございます。せっかくコメントくださったのに気づくのが遅くなってすみません。
参照渡し(AddRefStruct)が異常に速度的なパフォーマンスがいい結果になってしまったのはご指摘いただいた通りのミスが原因です。あとでミスがあったことが分かる形で更新します。
匿名さん
ミスリードしそうになってすみません。
C#はあまり詳しくないのですが、基本refはCのポインタ渡しと同様だと思われます。値渡しの場合、スタック上でのコピーが発生するので、処理にかかる時間は構造体のサイズに比例します。一方で、ポインタ(オブジェクトのアドレスを格納するための領域。4 byteか8 byteだと思われます)のサイズは一定なので、構造体のサイズがどうであれ、コピーにかかるコストは一定です。C#のコンパイラがどうなっているか不明ですが、inlineなどで最適化されている場合については、参照渡しはしばしばアドレスの直接参照に置き換わるため、実質ゼロコストになるケースもあると思われます(そうなるかどうかは、コンパイラ依存です)。インターフェイス経由で時間がかかるのは当然で、インターフェイスはポインタを間接参照することで値を引っ張るので、メソッド呼び出しのところで時間を食っているのだと思われます(アクセスせずに渡すだけなら、refと同じなのでは?)。
返信削除