返回 if 块内部或写入 if/else 块之间是否存在性能差异
本文关键字:if 存在 是否 性能 之间 内部 返回 else | 更新日期: 2023-09-27 18:30:58
我正在通过我的项目来重构一些代码,并意识到我写了这样的东西:
if(errorCode > 0)
{
DisplayError(errorCode);
return;
}
// Continue to do stuff otherwise.
您可能已经猜到了,该函数的返回类型为 void。当我开始查看此内容时,我思考将其放在 if/else 块中是否有任何真正的区别:
if(errorCode > 0)
{
DisplayError(errorCode);
}
else
{
// Do other stuff
}
else 块将持续到函数结束,因此控制流本质上是相同的。这里是否存在性能差异,或者应该使用的约定,或者这两者真的完全相同吗?
两种情况下生成的代码完全相同。
(在第一个示例中,代码周围缺少括号,但我只是假设这是一个错字,您实际上是在询问使用 return
和 else
之间的区别。
如果您查看为这两种方法生成的代码:
public static void Test1(int errorCode) {
if (errorCode > 0) {
Console.WriteLine(errorCode);
return;
}
Console.WriteLine("ok");
}
public static void Test2(int errorCode) {
if (errorCode > 0) {
Console.WriteLine(errorCode);
} else {
Console.WriteLine("ok");
}
}
它将看起来像这样:
if (errorCode > 0) {
011A00DA in al,dx
011A00DB push eax
011A00DC mov dword ptr [ebp-4],ecx
011A00DF cmp dword ptr ds:[10F3178h],0
011A00E6 je 011A00ED
011A00E8 call 7470C310
011A00ED cmp dword ptr [ebp-4],0
011A00F1 jle 011A0100
Console.WriteLine(errorCode);
011A00F3 mov ecx,dword ptr [ebp-4]
011A00F6 call 73C5A920
return;
011A00FB nop
011A00FC mov esp,ebp
011A00FE pop ebp
011A00FF ret
}
Console.WriteLine("ok");
011A0100 mov ecx,dword ptr ds:[3E92190h]
011A0106 call 7359023C
}
011A010B nop
011A010C mov esp,ebp
011A010E pop ebp
011A010F ret
和:
if (errorCode > 0) {
011A0122 in al,dx
011A0123 push eax
011A0124 mov dword ptr [ebp-4],ecx
011A0127 cmp dword ptr ds:[10F3178h],0
011A012E je 011A0135
011A0130 call 7470C310
011A0135 cmp dword ptr [ebp-4],0
011A0139 jle 011A0148
Console.WriteLine(errorCode);
011A013B mov ecx,dword ptr [ebp-4]
011A013E call 73C5A920
011A0143 nop
011A0144 mov esp,ebp
011A0146 pop ebp
011A0147 ret
} else {
Console.WriteLine("ok");
011A0148 mov ecx,dword ptr ds:[3E92190h]
011A014E call 7359023C
}
}
011A0153 nop
011A0154 mov esp,ebp
011A0156 pop ebp
011A0157 ret
生成的代码完全相同,直到最后一条指令。
题外话,但太大而无法评论,您还可以使用出色的 LinqPad 进行 IL 反汇编。
有趣的是,在调试模式下仅对IL进行反汇编,Guffa的Test2 IL中有几个额外的nop,根据Guffa的答案在x86/x64 JIT中编译出来:
调试 IL:
Test1:
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldc.i4.0
IL_0003: cgt
IL_0005: ldc.i4.0
IL_0006: ceq
IL_0008: stloc.0 // CS$4$0000
IL_0009: ldloc.0 // CS$4$0000
IL_000A: brtrue.s IL_0016
IL_000C: nop
IL_000D: ldarg.0
IL_000E: call System.Console.WriteLine
IL_0013: nop
IL_0014: br.s IL_0021
IL_0016: ldstr "ok"
IL_001B: call System.Console.WriteLine
IL_0020: nop
IL_0021: ret
Test2:
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldc.i4.0
IL_0003: cgt
IL_0005: ldc.i4.0
IL_0006: ceq
IL_0008: stloc.0 // CS$4$0000
IL_0009: ldloc.0 // CS$4$0000
IL_000A: brtrue.s IL_0017
IL_000C: nop
IL_000D: ldarg.0
IL_000E: call System.Console.WriteLine
IL_0013: nop
IL_0014: nop <-- Extra
IL_0015: br.s IL_0024
IL_0017: nop <-- Extra
IL_0018: ldstr "ok"
IL_001D: call System.Console.WriteLine
IL_0022: nop
IL_0023: nop <-- Extra
IL_0024: ret
这很有趣 IMO,因为从内存来看,处理器nop's
如果实际执行,它们是浪费的,但如果它们填充以允许跳转到后续对齐边界,则有益。这就引出了一个问题,即为什么 IL 对nops
发表意见,因为填充优化应该只是 JIT 编译器关注的问题。答案似乎无关,即大括号,可能是出于调试原因
对于发布模式,Guffa 是正确的 - 在发布模式下编译 IL 会从 IL 中删除所有 nops - IL nops 纯粹是为了允许在大括号上调试中断/逐步通过。抖动重新引入 x86 nops 以实现均匀的指令对齐,并且这两种方法是相同的。
发布 IL(测试 1 和测试 2):
IL_0000: ldarg.0
IL_0001: ldc.i4.0
IL_0002: ble.s IL_000B
IL_0004: ldarg.0
IL_0005: call System.Console.WriteLine
IL_000A: ret
IL_000B: ldstr "ok"
IL_0010: call System.Console.WriteLine
IL_0015: ret