昨天,我找到了Christoph Nahr撰写的名为“ .NET Struct Performance”的文章,该文章 对几种语言(C ++,C#,Java,JavaScript)的基准测试了一种添加两个点结构(double
元组)的方法。
事实证明,C ++版本执行大约需要1000毫秒(1e9迭代),而C#在同一台机器上的时间不能低于3000毫秒(并且在x64中的表现甚至更差)。
为了自己进行测试,我使用了C#代码(并略微简化以仅调用按值传递参数的方法),并在i7-3610QM机器(单核3.1Ghz Boost),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
算术(在其他所有东西都被优化之后)。
奇怪的东西
现在来了奇怪的部分。如果我只是在循环外添加另一个秒表(是的,经过几次重试,我将其范围缩小到了这一疯狂的步骤),则代码运行速度快了三倍:
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
给我错误的结果,因为我可以清楚地看到它在一秒钟后结束。
谁能告诉我这里会发生什么?
(更新)
这是同一程序中的两个方法,表明原因不是JITting:
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
这是一个pastebin。您需要在.NET 4.x上将其作为32位版本运行(在代码中进行了两次检查以确保做到这一点)。
(更新4)
在@usr对@Hans答案的评论之后,我检查了两种方法的优化反汇编,它们有很大不同:
这似乎表明,差异可能是由于编译器在第一种情况下表现出了好笑而不是双字段对齐?
另外,如果我添加两个变量(总偏移量为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,因此我排除了结构布局/方法调用效率低下的问题。