避免托管语言中的分支
本文关键字:分支 语言 | 更新日期: 2023-09-27 18:14:01
在C中,当编译到x86机器时,我通常会用逻辑表达式代替分支,当速度是最重要的方面时,即使条件很复杂,例如,代替
char isSomething() {
if (complexExpression01) {
if (complexExpression02) {
if(!complexExpression03) {
return 1;
}
}
}
return 0;
}
我会写:
char isSomething() {
return complexExpression01 &&
complexExpression02 &&
!complexExpression03 ;
}
现在很明显,这可能是一个更难维护,可读性较差的代码,但它实际上可能更快。
在使用托管代码(如c#)时,是否有任何理由采取相同的方式?托管代码中的"跳转"是否与非托管代码中的"跳转"一样昂贵(至少在x86上)?
总则
在常规编译器中,生成的代码通常是相同的,至少在假设您使用常规
时是如此。csc.exe /optimize+
cl.exe /O2
g++ -O2
和相关的默认优化模式。
一般的咒语是:概要文件,概要文件,概要文件(并且不要进行微优化,直到您的概要文件告诉您这样做)。您可以随时查看生成的代码2,看看是否有改进的空间。可以这样想,例如c#代码:
c#/。净
每个complexExpressions都是一个事实上的函数调用调用(call, calli, callvirt opcode3),需要将它的参数压入堆栈。返回值将被左压入堆栈,而不是退出时的参数。
现在,CLR是一个基于堆栈的虚拟机(即无寄存器),这相当于堆栈上的匿名临时变量。唯一的区别是代码中使用的标识符的数量。
现在JIT引擎如何处理它是另一回事:JIT引擎必须将这些调用转换为本机汇编,并可能通过调整寄存器分配,指令排序,分支预测等进行优化1
<子>1(尽管在实践中,对于这个示例,不允许进行更有趣的优化,因为complex function calls
可能有副作用,并且c#规范非常清楚求值顺序和所谓的序列)点。然而,注意, JIT引擎允许内联函数调用,以减少调用开销。子>
不仅当它们是非虚拟的,而且(IIRC)当运行时类型可以在编译时静态地知道某些。net框架内部。我必须为此查找参考资料,但事实上,我认为在。net框架4.0中引入了一些属性来显式地防止框架函数的内联;这样微软就可以在服务包/更新中修补库代码,即使用户程序集已经提前编译(ngen.exe)为本机映像。
C/c++
在C/c++中,内存模型要宽松得多(至少在c++ 11之前),代码通常在编译时直接编译为本机指令。另外,C/c++编译器通常会进行积极的内联,即使在这样的编译器中,代码也通常是相同的,除非您在编译时没有启用优化<子>2我使用子>
-
ildasm
或monodis
查看生成的IL代码 -
mono -aot=full,static
或mkbundle
生成本机对象模块,objdump -CdS
查看注释的本机汇编指令。
注意这纯粹是出于好奇,因为我很少以这种方式发现有趣的瓶颈。但是,请参阅Jon Skeet关于性能优化Noda.NET
的博客文章,以获得可能潜伏在泛型类生成的IL代码中的惊喜的好例子。
3Edit对于编译器内部的操作符不准确,尽管它们会将结果留在堆栈上。
这取决于托管语言的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
}
这两个表达式将导致相同数量的测试,因为逻辑和运算符(&&
)在C和c#中都具有短路语义。因此,你的问题的前提(第二种表达程序的方式导致较少的分支)是不正确的。
唯一的方法就是测量。
True和false被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)
因此,从这里看来,使用逻辑运算符而不是嵌套条件语句似乎有一点优势。然而,任何特定的实例可能会给出一些不同的结果。
最后,像这样的微优化是否值得取决于代码的性能关键程度。