昨天我发现了一个 Christoph Nahr 撰写的题为“ .NET 结构性能”的文章,它对几种语言(C + + ,C # ,Java,JavaScript)进行了基准测试,为一个增加了两个结构点(double
元组)的方法。
事实证明,C + + 版本需要大约1000ms 才能执行(1e9次迭代) ,而 C # 在同一台机器上不能低于3000ms (在 x64中表现更差)。
为了自己测试它,我使用了 C # 代码(稍微简化了一下,只调用参数通过值传递的方法) ,并在 i7-3610QM 机器上运行它(单核3.1 Ghz 升压) ,8 GB RAM,Win8.1,使用。NET 4.5.2,释放构建32位(x86 WoW64,因为我的操作系统是64位)。这是一个简化版本:
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
这是糊状物。您需要将它作为32位版本运行在。NET 4.x (在代码中有几个检查来确保这一点)。
(更新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);
}