条件分支的失败侧是否更有效?将其作为错误处理方是个好主意吗?

Is the fall-through side of a conditional branch more efficient? Is it a good idea to make that the error-handling side?

考虑一个调用另一个函数并检查错误的函数。假设函数CheckError() returns 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

就条件跳转本身的循环计数而言,您构建代码的方式绝对没有区别。唯一重要的是分支是否被正确预测。如果是,则分支成本 周期。如果不是,分支将花费 数十甚至数百 个周期。硬件中的预测逻辑不依赖于代码的结构方式,你基本上无法控制它(CPU 设计者已经尝试过 "hints" 但结果是净输)(但请参阅“Why is it faster to process a sorted array than an unsorted array?”了解高级算法决策如何产生巨大差异)。

但是,还有另一个因素需要考虑:"hotness"。如果 "error processing" 代码几乎永远不会被实际使用,最好将它移出行外 — way 移出行外,到可执行映像的它自己的子部分 — 所以它不会在 I-cache 中浪费 space。就何时执行此操作做出准确的决定是 profile-guided optimization 最有价值的好处之一——我猜仅次于决定是否针对每个功能甚至每个基本块进行优化 [=36] =] 或速度。

只有在您将其作为学习练习或实现不能实现的东西时,才应将可读性作为手写汇编的首要考虑因素 以更高级的语言实现(例如上下文切换的核心)。如果你这样做是因为你需要从一个关键的内部循环中挤出周期,并且它不会出来不可读,你可能需要做更多的周期压缩。