考虑一个函数调用另一个函数并检查错误。假设函数CheckError()
执行失败返回0,其他数字表示执行成功。
第一版:成功或失败分支到错误处理代码(在函数的中间)。
CALL CheckError
TEST EAX,EAX ;check if return value is 0
JNZ Normal
ErrorProcessing:
... ;some error processing code here
Normal:
... ;some usual code here
第二个版本在错误时分支,或者掉入正常路径。错误处理代码位于函数末尾。
CALL CheckError
TEST EAX,EAX
JZ ErrorProcessing
Normal:
... ;some usual code here
ErrorProcessing:
... ;some error processing code here
这两种方法哪一种更好?为什么?
就我个人而言,我认为第一个代码有更好的代码结构(更具可读性和可编程性),因为代码紧凑。然而,我也认为第二个代码通常有更好的速度(在没有错误的情况下),因为一个不采取条件跳转需要2-3个时钟周期(也许我在这里太挑剔了)比一个采取。
无论如何,我发现我测试的所有编译器在编译if
语句时都使用第一个模型。例如:
if (GetActiveWindow() == NULL)
{
printf("Error: can't get window's handle.n");
return -1;
}
printf("Succeed.n");
return 0;
这应该编译成(没有任何exe入口例程):
CALL [GetActiveWindow] ;if (GetActiveWindow() == NULL)
TEST EAX,EAX
JNZ CodeSucceed
;printf("Error.......n"); return -1
PUSH OFFSET "Error.........n"
CALL [Printf]
ADD ESP,4
OR EAX,0FFFFFFFFH
JMP Exit
CodeSucceed: ;printf("Succeed.n"); return 0
PUSH OFFSET "Succeed.n"
CALL [Printf]
ADD ESP,4
XOR EAX,EAX
Exit:
RETN
就条件跳转本身的循环计数而言,您构建代码的方式对绝对没有影响。唯一重要的是分支预测是否正确。如果是,分支花费 0 个循环。如果不是,分支花费数十甚至数百个周期。硬件中的预测逻辑不依赖于代码的结构方式,你基本上无法控制它(CPU设计师已经尝试过"提示",但结果是净损失)(但请参阅"为什么处理排序数组比处理未排序数组更快?"了解高级算法决策如何产生巨大差异)。
然而,还有一个因素要考虑:"性感"。如果"错误处理"代码几乎永远不会被实际使用,那么最好将其移出行(方式出行)到可执行映像的自己的子部分,这样它就不会浪费I-cache中的空间。准确决定何时进行优化是配置文件引导优化的最有价值的好处之一——我想,这仅次于根据每个函数甚至每个基本块来决定是为空间优化还是为速度优化。
当手工编写汇编程序时,可读性应该是首要考虑的问题,只有当是作为学习练习,或者是为了实现无法在高级语言中实现的东西(例如上下文切换的核心)时,才应该考虑。如果您这样做是因为您需要从关键的内循环中挤出循环,并且没有显示不可读,那么您可能需要做更多的循环压缩工作。