C#で書いていて、繰り返し実行されるコードでnewするのはイヤだな~と思うわけですよ。
ヒープから小さなオブジェクトをたくさん取ってくるとオーバーヘッドがでかいから。
かといって、構造体にすると、引数や戻り値で移動するだけで丸ごとコピーになってオーバーヘッドがでかいわけです。
いったい何バイトなら構造体にして許容なのだろう……。
なんてことを今頃考えているなんて時代錯誤です (汗。
方針 §
変な最適化が入ると困るので、別のプロジェクトのメソッド呼び出しの引数と戻り値に様々なサイズの構造体を渡して処理時間を見ることにしました。
環境 §
Pentium D 3.2GHz+メモリ 2GB
Windows XP SP2
C# 2.0+.NET Framework 2.0
Viusal Studio 2005上でデバッグ実行
検証プログラム (呼び出され側、クラスライブラリ) §
using System;
using System.Collections.Generic;
using System.Text;
namespace ClassLibrary1
{
public class c4
{
public int test1;
}
public class c16
{
public int test1;
public int test2;
public int test3;
public int test4;
}
public class c64
{
public int test11, test12, test13, test14;
public int test21, test22, test23, test24;
public int test31, test32, test33, test34;
public int test41, test42, test43, test44;
}
public struct s4
{
public int test1;
}
public struct s16
{
public int test1;
public int test2;
public int test3;
public int test4;
}
public struct s64
{
public int test11, test12, test13, test14;
public int test21, test22, test23, test24;
public int test31, test32, test33, test34;
public int test41, test42, test43, test44;
}
public class passClass
{
public static c4 pass( c4 t ) { return t; }
public static c16 pass(c16 t) { return t; }
public static c64 pass(c64 t) { return t; }
public static s4 pass(s4 t) { return t; }
public static s16 pass(s16 t) { return t; }
public static s64 pass(s64 t) { return t; }
}
}
検証プログラム (呼び出す側、コンソールアプリケーション) §
using System;
using System.Collections.Generic;
using System.Text;
using ClassLibrary1;
namespace ConsoleApplication89
{
class Program
{
private const int MAX_COUNT = 100000000;
static void Main(string[] args)
{
DateTime start1 = DateTime.Now;
for (int i = 0; i < MAX_COUNT; i++)
{
c4 t = passClass.pass( new c4() );
}
DateTime end1 = DateTime.Now;
Console.WriteLine(end1 - start1);
DateTime start1a = DateTime.Now;
for (int i = 0; i < MAX_COUNT; i++)
{
c16 t = passClass.pass(new c16());
}
DateTime end1a = DateTime.Now;
Console.WriteLine(end1a - start1a);
DateTime start1b = DateTime.Now;
for (int i = 0; i < MAX_COUNT; i++)
{
c64 t = passClass.pass(new c64());
}
DateTime end1b = DateTime.Now;
Console.WriteLine(end1b - start1b);
DateTime start2 = DateTime.Now;
for (int i = 0; i < MAX_COUNT; i++)
{
s4 t = passClass.pass(new s4());
}
DateTime end2 = DateTime.Now;
Console.WriteLine(end2 - start2);
DateTime start3 = DateTime.Now;
for (int i = 0; i < MAX_COUNT; i++)
{
s16 t = passClass.pass(new s16());
}
DateTime end3 = DateTime.Now;
Console.WriteLine(end3 - start3);
DateTime start4 = DateTime.Now;
for (int i = 0; i < MAX_COUNT; i++)
{
s64 t = passClass.pass(new s64());
}
DateTime end4 = DateTime.Now;
Console.WriteLine(end4 - start4);
}
}
}
結果 §
種類 | タイム |
---|
クラス4バイト | 00:00:02.9531439 |
クラス16バイト | 00:00:03.7343989 |
クラス64バイト | 00:00:06.1875396 |
構造体4バイト | 00:00:01.1718825 |
構造体16バイト | 00:00:04.3437778 |
構造体64バイト | 00:00:14.2344661 |
クラスは、サイズによって引き渡しに要する時間に差はないと考えられますが、newのオーバーヘッドがサイズで変わるので3種類用意しました。
クラス、構造体のバイト数は、もしかしたら正しくないかもしれません。
ちなみに、newのオーバーヘッドは生成時の1回きりですが、引数や戻り値に渡すオーバーヘッドは、渡す回数だけ発生します。その点で、構造体は不利になることに注意が必要です。
考察 §
クラスはnewによってヒープからメモリを確保していると考えられます。
一方、構造体の引数と戻り値はスタック上に確保され、動的なメモリ確保は発生していないと考えられます。
つまり、上記の数字は、クラスの場合のみメモリの確保というオーバーヘッドが含まれていると考えられます。
さて、これを見て分かるのは、サイズごとに有利不利が変わっている状況です。
4バイトのデータを扱う場合はオーバーヘッドが大きく響くためか、構造体を使う方が有利となります。
しかし、16バイトではその差は小さくなり、僅かに構造体が不利となります。
そして、64バイトでは圧倒的に構造体が不利になります。
この問題を解決するためには、構造体を参照渡しでメソッドに渡す方法があり得ますが、単に速度を稼ぐために参照渡しにするぐらいなら、おとなしくクラスにしておく方が良いような気もします。
更に考察 §
構造体の損益分岐点が16バイトあたりに存在する……という結論は、確実なものではありません。環境や使い方によって変わりうるものです。
しかし、仮に16バイトだとすると、rectangleF構造体の存在意義が何となく見えてきます。
float4つの数値から構成されるrectangleF構造体のサイズは16バイトです。
整数4つの数値から構成されるrectangle構造体のサイズも同じく16バイトです。
しかし、double4つの数値から構成されるrectangleD構造体はありません。それを作るとすればサイズは32バイトになりますが、これは構造体で扱うには効率の良くないサイズということになります。
つまり、rectangleF構造体は、あくまでfloat4つの16バイトの構造体として作られる必然性があったと言うことです。
……という話が事実かどうかは分からないので、眉に唾を付けるように!