我想知道是否有人可以向我解释编译器到底做了什么让我观察到一个简单方法的性能差异如此之大。
public static uint CalculateCheckSum(string str) {
char[] charArray = str.ToCharArray();
uint checkSum = 0;
foreach (char c in charArray) {
checkSum += c;
}
return checkSum % 256;
}
我正在与一位同事合作,为消息处理应用程序做一些基准测试/优化。在 Visual Studio 2012 中使用相同的输入字符串对此函数进行 1000 万次迭代大约需要 25 秒,但是当使用“优化代码”选项构建项目时,相同的代码在 7 秒内执行相同的 1000 万次迭代。
我非常有兴趣了解编译器在幕后做了什么,以便我们能够看到像这样看似无辜的代码块的性能提高超过 3 倍。
根据要求,这是一个完整的控制台应用程序,它演示了我所看到的内容。
class Program
{
public static uint CalculateCheckSum(string str)
{
char[] charArray = str.ToCharArray();
uint checkSum = 0;
foreach (char c in charArray)
{
checkSum += c;
}
return checkSum % 256;
}
static void Main(string[] args)
{
string stringToCount = "8=FIX.4.29=15135=D49=SFS56=TOMW34=11752=20101201-03:03:03.2321=DEMO=DG00121=155=IBM54=138=10040=160=20101201-03:03:03.23244=10.059=0100=ARCA10=246";
Stopwatch stopwatch = Stopwatch.StartNew();
for (int i = 0; i < 10000000; i++)
{
CalculateCheckSum(stringToCount);
}
stopwatch.Stop();
Console.WriteLine(stopwatch.Elapsed);
}
}
在关闭优化的情况下调试运行我看到 13 秒,我得到 2 秒。
在优化后的发布中运行 3.1 秒和 2.3 秒。
最佳答案
要查看C# 编译器 为您做了什么,您需要查看 IL。如果您想了解这如何影响 JIT 代码,您需要查看 Scott Chamberlain 所描述的 native 代码。请注意,JITted 代码会因处理器架构、CLR 版本、进程的启动方式以及可能的其他因素而异。
我通常会从 IL 开始,然后可能查看 JIT 代码。
使用 ildasm
比较 IL 可能会有些棘手,因为它包含每条指令的标签。以下是经过优化和未经优化(使用 C# 5 编译器)编译的方法的两个版本,删除了无关的标签(和 nop
指令)以使它们尽可能易于比较:
优化
.method public hidebysig static uint32
CalculateCheckSum(string str) cil managed
{
// Code size 46 (0x2e)
.maxstack 2
.locals init (char[] V_0,
uint32 V_1,
char V_2,
char[] V_3,
int32 V_4)
ldarg.0
callvirt instance char[] [mscorlib]System.String::ToCharArray()
stloc.0
ldc.i4.0
stloc.1
ldloc.0
stloc.3
ldc.i4.0
stloc.s V_4
br.s loopcheck
loopstart:
ldloc.3
ldloc.s V_4
ldelem.u2
stloc.2
ldloc.1
ldloc.2
add
stloc.1
ldloc.s V_4
ldc.i4.1
add
stloc.s V_4
loopcheck:
ldloc.s V_4
ldloc.3
ldlen
conv.i4
blt.s loopstart
ldloc.1
ldc.i4 0x100
rem.un
ret
} // end of method Program::CalculateCheckSum
未优化
.method public hidebysig static uint32
CalculateCheckSum(string str) cil managed
{
// Code size 63 (0x3f)
.maxstack 2
.locals init (char[] V_0,
uint32 V_1,
char V_2,
uint32 V_3,
char[] V_4,
int32 V_5,
bool V_6)
ldarg.0
callvirt instance char[] [mscorlib]System.String::ToCharArray()
stloc.0
ldc.i4.0
stloc.1
ldloc.0
stloc.s V_4
ldc.i4.0
stloc.s V_5
br.s loopcheck
loopstart:
ldloc.s V_4
ldloc.s V_5
ldelem.u2
stloc.2
ldloc.1
ldloc.2
add
stloc.1
ldloc.s V_5
ldc.i4.1
add
stloc.s V_5
loopcheck:
ldloc.s V_5
ldloc.s V_4
ldlen
conv.i4
clt
stloc.s V_6
ldloc.s V_6
brtrue.s loopstart
ldloc.1
ldc.i4 0x100
rem.un
stloc.3
br.s methodend
methodend:
ldloc.3
ret
}
注意事项:
- 优化后的版本使用了更少的本地人。这可能允许 JIT 更有效地使用寄存器。
- 优化版本在检查是否再次循环时使用
blt.s
而不是clt
后跟brtrue.s
(这是一个额外的本地人的原因)。 - 未优化的版本在返回之前使用额外的本地来存储返回值,大概是为了使调试更容易。
- 未优化的版本在返回之前有一个无条件分支。
- 优化后的版本更短,但我怀疑它是否足够短以内联,所以我怀疑这无关紧要。
关于C# 编译器优化,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21438751/