【发布时间】:2017-07-18 17:54:47
【问题描述】:
我的一位同事一直在阅读 Robert C Martin 的 Clean Code,并阅读了关于使用许多小函数而不是更少的大函数的部分。这引发了关于这种方法的性能后果的争论。所以我们编写了一个快速程序来测试性能,结果被结果弄糊涂了。
对于初学者来说,这里是该函数的普通版本。
static double NormalFunction()
{
double a = 0;
for (int j = 0; j < s_OuterLoopCount; ++j)
{
for (int i = 0; i < s_InnerLoopCount; ++i)
{
double b = i * 2;
a = a + b + 1;
}
}
return a;
}
这是我制作的将功能分解为小功能的版本。
static double TinyFunctions()
{
double a = 0;
for (int i = 0; i < s_OuterLoopCount; i++)
{
a = Loop(a);
}
return a;
}
static double Loop(double a)
{
for (int i = 0; i < s_InnerLoopCount; i++)
{
double b = Double(i);
a = Add(a, Add(b, 1));
}
return a;
}
static double Double(double a)
{
return a * 2;
}
static double Add(double a, double b)
{
return a + b;
}
我使用秒表类对函数计时,当我在调试中运行它时,我得到了以下结果。
s_OuterLoopCount = 10000;
s_InnerLoopCount = 10000;
NormalFunction Time = 377 ms;
TinyFunctions Time = 1322 ms;
这些结果对我来说很有意义,尤其是在调试中,因为函数调用会产生额外的开销。当我在 release 中运行它时,我得到了以下结果。
s_OuterLoopCount = 10000;
s_InnerLoopCount = 10000;
NormalFunction Time = 173 ms;
TinyFunctions Time = 98 ms;
这些结果让我感到困惑,即使编译器通过内联所有函数调用来优化 TinyFunctions,怎么可能让它快 57%?
我们已经尝试在 NormalFunctions 中移动变量声明,它基本上对运行时间没有影响。
我希望有人知道发生了什么,如果编译器可以很好地优化 TinyFunctions,为什么不能对 NormalFunction 应用类似的优化。
在环顾四周时,我们发现有人提到分解函数可以让 JIT 更好地优化放入寄存器的内容,但 NormalFunctions 只有 4 个变量,所以我很难相信这可以解释巨大的性能差异。
如果有人能提供任何见解,我将不胜感激。
更新 1 正如下面 Kyle 所指出的,改变操作的顺序对 NormalFunction 的性能产生了巨大的影响。
static double NormalFunction()
{
double a = 0;
for (int j = 0; j < s_OuterLoopCount; ++j)
{
for (int i = 0; i < s_InnerLoopCount; ++i)
{
double b = i * 2;
a = b + 1 + a;
}
}
return a;
}
以下是此配置的结果。
s_OuterLoopCount = 10000;
s_InnerLoopCount = 10000;
NormalFunction Time = 91 ms;
TinyFunctions Time = 102 ms;
这超出了我的预期,但仍然留下了一个问题,即为什么操作顺序会对性能造成约 56% 的影响。
此外,我随后尝试了整数运算,但我们又回到了没有任何意义的状态。
s_OuterLoopCount = 10000;
s_InnerLoopCount = 10000;
NormalFunction Time = 87 ms;
TinyFunctions Time = 52 ms;
无论操作顺序如何,这都不会改变。
【问题讨论】:
-
可能是因为在发布模式下,小函数可以通过内联进行优化。此外,tail call optimization 可能是差异的一部分。优化“NormalFunction”更加困难,因为编译器更难识别可能的优化。
-
这些可以解释为什么 TinyFunctions 可以像 NormalFunction 一样快地执行,但它并没有向我解释它是如何执行得这么快的。因为 NormalFunctions 几乎是 TinyFunctions 的预内嵌版本
-
我猜这可能是双重加法得到不同的处理尝试使用正常 a = a + (b + 1);
-
了解发生了什么的一种方法是查看使用 ILSPY 生成的 IL 代码
-
寻呼 eric lippert 或 jon skeet
标签: c# performance compilation roslyn jit