昨日 、2つのポイント構造体(タプル)を追加するメソッドのいくつかの言語(C ++、C#、Java、JavaScript)のベンチマークとなった「.NET Struct Performance」というタイトルのChristoph Nahrの記事を見つけましたdouble
。
結局のところ、C ++バージョンは実行に約1000ミリ秒(1e9反復)かかりますが、C#は同じマシン上で〜3000ミリ秒を下回ることはできません(x64ではパフォーマンスがさらに低下します)。
自分でテストするために、C#コード(およびパラメーターが値で渡されるメソッドのみを呼び出すように少し簡略化)を取り、i7-3610QMマシン(シングルコアの場合は3.1Ghzブースト)、8GB RAM、Win8で実行しました。 1、.NET 4.5.2を使用して、RELEASEビルド32ビット(私のOSは64ビットであるためx86 WoW64)。これは簡略版です:
public static class CSharpTest
{
private const int ITERATIONS = 1000000000;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Point AddByVal(Point a, Point b)
{
return new Point(a.X + b.Y, a.Y + b.X);
}
public static void Main()
{
Point a = new Point(1, 1), b = new Point(1, 1);
Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
a = AddByVal(a, b);
sw.Stop();
Console.WriteLine("Result: x={0} y={1}, Time elapsed: {2} ms",
a.X, a.Y, sw.ElapsedMilliseconds);
}
}
Point
単純に次のように定義されます。
public struct Point
{
private readonly double _x, _y;
public Point(double x, double y) { _x = x; _y = y; }
public double X { get { return _x; } }
public double Y { get { return _y; } }
}
これを実行すると、記事と同様の結果が得られます。
Result: x=1000000001 y=1000000001, Time elapsed: 3159 ms
最初の奇妙な観察
メソッドをインライン化する必要があるので、構造体を完全に削除し、全体を単純にインライン化すると、コードがどのように機能するのか疑問に思いました。
public static class CSharpTest
{
private const int ITERATIONS = 1000000000;
public static void Main()
{
// not using structs at all here
double ax = 1, ay = 1, bx = 1, by = 1;
Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
{
ax = ax + by;
ay = ay + bx;
}
sw.Stop();
Console.WriteLine("Result: x={0} y={1}, Time elapsed: {2} ms",
ax, ay, sw.ElapsedMilliseconds);
}
}
そして、実質的に同じ結果が得られました(数回の再試行後、実際には1%遅くなります)。つまり、JIT-terはすべての関数呼び出しを最適化しているようです。
Result: x=1000000001 y=1000000001, Time elapsed: 3200 ms
また、ベンチマークはstruct
パフォーマンスを測定していないようであり、実際には基本的なdouble
計算のみを測定しているように見えます(他のすべてが最適化された後)。
奇妙なもの
ここで奇妙な部分が来ます。ループの外に別のストップウォッチを追加しただけの場合(はい、数回の再試行後、このクレイジーなステップに絞り込みました)、コードは3倍速く実行されます。
public static void Main()
{
var outerSw = Stopwatch.StartNew(); // <-- added
{
Point a = new Point(1, 1), b = new Point(1, 1);
var sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
a = AddByVal(a, b);
sw.Stop();
Console.WriteLine("Result: x={0} y={1}, Time elapsed: {2} ms",
a.X, a.Y, sw.ElapsedMilliseconds);
}
outerSw.Stop(); // <-- added
}
Result: x=1000000001 y=1000000001, Time elapsed: 961 ms
ばかげている!そしてStopwatch
、1秒後に終了することがはっきりとわかるので、間違った結果が出ているようには見えません。
誰かここで何が起こっているのか教えてもらえますか?
(更新)
同じプログラムの2つの方法を以下に示します。これは理由がJITでないことを示しています。
public static class CSharpTest
{
private const int ITERATIONS = 1000000000;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Point AddByVal(Point a, Point b)
{
return new Point(a.X + b.Y, a.Y + b.X);
}
public static void Main()
{
Test1();
Test2();
Console.WriteLine();
Test1();
Test2();
}
private static void Test1()
{
Point a = new Point(1, 1), b = new Point(1, 1);
var sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
a = AddByVal(a, b);
sw.Stop();
Console.WriteLine("Test1: x={0} y={1}, Time elapsed: {2} ms",
a.X, a.Y, sw.ElapsedMilliseconds);
}
private static void Test2()
{
var swOuter = Stopwatch.StartNew();
Point a = new Point(1, 1), b = new Point(1, 1);
var sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
a = AddByVal(a, b);
sw.Stop();
Console.WriteLine("Test2: x={0} y={1}, Time elapsed: {2} ms",
a.X, a.Y, sw.ElapsedMilliseconds);
swOuter.Stop();
}
}
出力:
Test1: x=1000000001 y=1000000001, Time elapsed: 3242 ms
Test2: x=1000000001 y=1000000001, Time elapsed: 974 ms
Test1: x=1000000001 y=1000000001, Time elapsed: 3251 ms
Test2: x=1000000001 y=1000000001, Time elapsed: 972 ms
これがペーストビンです。.NET 4.xで32ビットリリースとして実行する必要があります(これを確認するためにコードにいくつかのチェックがあります)。
(更新4)
@Hansの回答に対する@usrのコメントに続いて、両方のメソッドの最適化された逆アセンブリを確認しましたが、どちらもかなり異なります。
これは、違いが、ダブルフィールドアライメントではなく、コンパイラが最初のケースでおかしな動作をしていることが原因である可能性があることを示しているようです。
また、2つの変数(合計8バイトのオフセット)を追加しても、速度は同じですが、Hans Passantによるフィールドアライメントの言及とは関係がないようです。
// this is still fast?
private static void Test3()
{
var magical_speed_booster_1 = "whatever";
var magical_speed_booster_2 = "whatever";
{
Point a = new Point(1, 1), b = new Point(1, 1);
var sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
a = AddByVal(a, b);
sw.Stop();
Console.WriteLine("Test2: x={0} y={1}, Time elapsed: {2} ms",
a.X, a.Y, sw.ElapsedMilliseconds);
}
GC.KeepAlive(magical_speed_booster_1);
GC.KeepAlive(magical_speed_booster_2);
}
double
変数のみを使用してテストを行い、struct
sは使用しないため、構造体のレイアウト/メソッド呼び出しの非効率性を排除しました。