条件分支的失败侧是否更有效?将其作为错误处理方是个好主意吗?
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] =] 或速度。
只有在您将其作为学习练习或实现不能实现的东西时,才应将可读性作为手写汇编的首要考虑因素 以更高级的语言实现(例如上下文切换的核心)。如果你这样做是因为你需要从一个关键的内部循环中挤出周期,并且它不会出来不可读,你可能需要做更多的周期压缩。
考虑一个调用另一个函数并检查错误的函数。假设函数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] =] 或速度。
只有在您将其作为学习练习或实现不能实现的东西时,才应将可读性作为手写汇编的首要考虑因素 以更高级的语言实现(例如上下文切换的核心)。如果你这样做是因为你需要从一个关键的内部循环中挤出周期,并且它不会出来不可读,你可能需要做更多的周期压缩。