J.S*_*ith 3 optimization x86 assembly micro-optimization
考虑一个调用另一个函数并检查错误的函数.假设函数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
Run Code Online (Sandbox Code Playgroud)
第二个版本在错误时采取分支,或者通过正常路径.错误处理代码位于函数的末尾.
CALL CheckError
TEST EAX,EAX
JZ ErrorProcessing
Normal:
... ;some usual code here
ErrorProcessing:
... ;some error processing code here
Run Code Online (Sandbox Code Playgroud)
这两种方法中哪一个更好?为什么?
就个人而言,我认为第一个代码具有更好的代码结构(更易读和可编程),因为代码是紧凑的.但是,我也认为第二个代码通常具有更好的速度(在无错误的情况下),因为一个未采用的条件跳转需要2-3个时钟周期(可能我在这里太挑剔)少于一个.
无论如何,我发现我测试的所有编译器在编译if语句时都使用第一个模型.例如:
if (GetActiveWindow() == NULL)
{
printf("Error: can't get window's handle.\n");
return -1;
}
printf("Succeed.\n");
return 0;
Run Code Online (Sandbox Code Playgroud)
这应编译为(没有任何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
Run Code Online (Sandbox Code Playgroud)
在条件跳转本身的循环计数方面,你构造代码的方式绝对没有区别.唯一重要的是分支是否被正确预测.如果是,则分支成本为零周期.如果不是,分支机构需要花费数十甚至数百个周期.硬件中的预测逻辑并不依赖于代码的结构方式,并且您基本上无法控制它(CPU设计人员已尝试过"提示",但结果却是净损失)(但请参阅" 为什么处理排序数组比处理未排序数组更快吗? "高级算法决策如何产生巨大差异".
然而,还有另一个需要考虑的因素:"热度".如果"错误处理"的代码几乎从来没有真正习惯,最好是将其移动脱节的- 方式脱节,自身的可执行映像节-因此,它并没有在I-cache中浪费空间.做出关于何时这样做的准确决定是配置文件引导优化的最有价值的好处之一- 我认为仅次于决定每个功能甚至每个基本块是否优化空间或速度.
只有当您将其作为学习练习,或者实现无法用更高级语言实现的内容(例如上下文切换的内容)时,可读性应该是手工编写程序集时的主要关注点.如果你这样做是因为你需要从一个关键的内环中挤出循环,并且它不会变得难以理解,你可能会有更多的循环挤压.