在 C 中,当编译到x86机器时,我通常会用逻辑表达式替换分支,当速度是最重要的方面时,即使条件很复杂,例如,而不是< / p>
char isSomething() {
if (complexExpression01) {
if (complexExpression02) {
if(!complexExpression03) {
return 1;
}
}
}
return 0;
}
我会写:
char isSomething() {
return complexExpression01 &&
complexExpression02 &&
!complexExpression03 ;
}
现在显然,这可能更难维护,代码可读性更低,但实际上可能更快。
在使用托管代码(例如C#)时,是否有任何理由采取相同的方式?托管代码中的“跳转”是否昂贵,因为它们处于非托管代码中(至少在x86上)?
答案 0 :(得分:4)
在常规编译器中,生成的代码通常是相同的,至少在假设您使用常规
时csc.exe /optimize+
cl.exe /O2
g++ -O2
和相关的默认优化模式。
一般的口头禅是:个人资料,个人资料,个人资料(并且在您的探查者告诉您之前不要进行微观优化)。您始终可以查看生成的代码 2 ,看看是否还有改进的余地。
以这种方式思考,例如C#代码:
每个 complexExpressions 都是一个事实上的函数调用调用(call,calli,callvirt opcode 3 ),它要求将其参数压入堆栈。返回值将被推入堆栈而不是退出时的参数。
现在,CLR是一个基于堆栈的虚拟机(即无寄存器),这与堆栈上的匿名临时变量完全相同。唯一的区别是代码中使用的标识符数量。
现在JIT引擎对此做了什么是另一回事:JIT引擎必须将这些调用转换为本机程序集,并且可能通过调整寄存器分配,指令排序,分支预测和类似之类的东西来进行优化。 1
<子>
1 (虽然在实践中,对于此示例,不允许进行更有趣的优化,因为complex function calls
可能有副作用,C#规范非常清楚评估顺序和所谓的序列点。 注意但是,JIT引擎允许内联函数调用,以减少调用开销。
不仅当它们是非虚拟的,而且(IIRC)也可以在编译时静态地知道某些.NET框架内部的运行时类型。我必须为此查找一个参考,但事实上我认为.NET framework 4.0中引入了一些属性来明确阻止框架函数的内联;这样,Microsoft就可以在服务包/更新中修补库代码,即使用户程序集已经提前编译(ngen.exe)到本机映像中也是如此。
在C / C ++中,内存模型更加宽松(即至少在C ++ 11之前),并且代码通常在编译时直接编译为本机指令。补充说C / C ++编译器通常会进行积极的内联,即使在这样的编译器中代码通常都是相同的,除非你在没有启用优化的情况下进行编译
<子> 2 我用
ildasm
或monodis
查看生成的IL代码mono -aot=full,static
或mkbundle
生成本机对象模块,objdump -CdS
查看带注释的本机程序集说明。请注意,这纯粹是好奇心,因为我很少发现这种有趣的瓶颈。但是,请参阅J on Skeet's blog posts on performance optimizing Noda.NET
,了解可能潜伏在生成的通用类IL代码中的惊喜示例。
3 编辑对编译器内在函数的运算符不准确,但即使它们只是将结果留在堆栈上。
答案 1 :(得分:2)
这取决于托管语言的CLR和编译器的实现。对于C#,以下测试用例证明嵌套if语句和组合if语句的指令没有区别:
// case 1
if (value1 < value2)
00000089 mov eax,dword ptr [ebp-0Ch]
0000008c cmp eax,dword ptr [ebp-10h]
0000008f jge 000000A6
{
if (value2 < value3)
00000091 mov eax,dword ptr [ebp-10h]
00000094 cmp eax,dword ptr [ebp-14h]
00000097 jge 000000A6
{
result1 = true;
00000099 mov eax,1
0000009e and eax,0FFh
000000a3 mov dword ptr [ebp-4],eax
}
}
// case 2
if (value1 < value2 && value2 < value3)
000000a6 mov eax,dword ptr [ebp-0Ch]
000000a9 cmp eax,dword ptr [ebp-10h]
000000ac jge 000000C3
000000ae mov eax,dword ptr [ebp-10h]
000000b1 cmp eax,dword ptr [ebp-14h]
000000b4 jge 000000C3
{
result2 = true;
000000b6 mov eax,1
000000bb and eax,0FFh
000000c0 mov dword ptr [ebp-8],eax
}
答案 2 :(得分:1)
这两个表达式将导致相同数量的测试,因为逻辑和运算符(&&
)在C和C#中都有短路语义。你的问题的前提(表示程序的第二种方式导致较少的分支)因此是不正确的。
答案 3 :(得分:0)
要知道的唯一方法是衡量。
CLR将真和假表示为1和0,因此如果使用逻辑表达式有一些优势,我不会感到惊讶。我们来看看:
static void BenchBranch() {
Stopwatch sw = new Stopwatch();
const int NMAX = 1000000000;
bool a = true;
bool b = false;
bool c = true;
sw.Restart();
int sum = 0;
for (int i = 0; i < NMAX; i++) {
if (a)
if (b)
if (c)
sum++;
a = !a;
b = a ^ b;
c = b;
}
sw.Stop();
Console.WriteLine("1: {0:F3} ms ({1})", sw.Elapsed.TotalMilliseconds, sum);
sw.Restart();
sum = 0;
for (int i = 0; i < NMAX; i++) {
if (a && b && c)
sum++;
a = !a;
b = a ^ b;
c = b;
}
sw.Stop();
Console.WriteLine("2: {0:F3} ms ({1})", sw.Elapsed.TotalMilliseconds, sum);
sw.Restart();
sum = 0;
for (int i = 0; i < NMAX; i++) {
sum += (a && b && c) ? 1 : 0;
a = !a;
b = a ^ b;
c = b;
}
sw.Stop();
Console.WriteLine("3: {0:F3} ms ({1})", sw.Elapsed.TotalMilliseconds, sum);
}
结果:
1: 2713.396 ms (250000000)
2: 2477.912 ms (250000000)
3: 2324.916 ms (250000000)
因此,使用逻辑运算符而不是嵌套条件语句似乎有一点点优势。但是,任何特定的实例都可能会产生不同的结果。
最后,这样的微优化是否值得,取决于代码的性能关键程度。