提问人:ingotangjingle 提问时间:9/10/2023 更新时间:9/10/2023 访问量:81
用函数指针替换 if 语句有什么缺点?
What is the downside of replacing if statements with function pointers?
问:
我正在寻找优化我的更新循环,并绕过了这段代码:
// Define two lambda functions
auto iftrue = [](int x, int y) -> int {
return x;
};
auto iffalse = [](int x, int y) -> int {
return y;
};
// Define a pointer to a function that takes two integers and returns an integer
typedef int (*FunctionPointer)(int, int);
FunctionPointer ptr = nullptr;
void SetFalse()
{
ptr = iffalse;
}
void SetTrue()
{
ptr = iftrue;
}
int main() {
int a = 5, b = 3;
SetTrue();
// Use the second lambda function
int result = ptr(a, b);
std::cout << "Result: " << result << std::endl;
SetFalse();
// Use the second lambda function
result = ptr(a, b);
std::cout << "Result: " << result << std::endl;
return 0;
}
它击中了我,这难道不是总是比 if 语句运行得更好吗?
我检查了速度,嗯......https://onlinegdb.com/mCZc6Eb7Z
似乎功能更快。在可能的情况下,不应该将所有内容都更改为函数指针吗?
答:
简短的回答是,在处理代码时,编译器会尝试预测语句的结果以进行优化。但是,如果预测不正确,则成本可能会更高。if
对于函数指针,您可以删除执行路径的条件部分,因此不会发生上述情况,并且在某些情况下,代码会运行得更快。话虽如此,这里的这种速度优势发生在少数情况下。
因此,最终,您的问题的答案是分支预测。
评论
编译器太聪明了,它完全 https://godbolt.org/z/jT5KWTPoe 优化了函数调用。这种微基准测试可能很难实现,现代编译器可能会走很多捷径并扭曲您的结果。
使用 quick-bench,我们可以看到当编译器无法优化函数调用时,它比函数指针快 3 倍:https://quick-bench.com/q/3-9AgNCJN1MwiD2dkJ-FttbGVWYif
语句可能很慢,因为 CPU 必须猜测分支将走哪条路,如果它出错了,那么它将推测性地执行了错误的指令,然后必须放弃此执行并从分支点重新开始计算。if
但是,函数调用可能比失败的分支预测更昂贵。要调用函数,您需要在堆栈上保存各种寄存器,执行分支(CPU 可能无法预测),运行该函数,然后从堆栈中恢复寄存器。
如果在两组功能之间切换,交换函数指针可能比重复执行便宜,但这是分支预测器的理想情况,当总是采用相同的分支时,因此任何好处都可能是最小的。if
首先,你的测试方法有很大的缺陷。如果您想获得有意义的结果,您应该使用诸如 quick-bench 之类的网站。
其次,函数指针没有得到编译器的很好的优化。 它们并不总是很好地内联和简化。
函数指针可以更快是否合理
让我们比较一下三种实现:
int choose_if(bool c, int x, int y) {
if (c) return x; // most naive version using if statements
else return y;
}
int choose_ptr(bool c, int x, int y) {
static constexpr int(*selectors[2])(int, int) = { // table of two function pointers
[](int a, int b) { return a; },
[](int a, int b) { return b; }
};
return selectors[c](x, y); // choose a function pointer and call it; no branches
}
int (*selector)(int, int);
int choose_ptr(int x, int y) {
return selector(x, y); // let's assume that we've made the previous decision
// in advance, and just have to call the pointer
}
这为我们提供了以下输出 (https://godbolt.org/z/bd1ceb9s8):
choose_if(bool, int, int): # @choose_if(bool, int, int)
mov eax, esi
test edi, edi
cmove eax, edx
ret
choose_ptr(bool, int, int): # @choose_ptr(bool, int, int)
mov eax, edi
lea rcx, [rip + choose_ptr(bool, int, int)::selectors]
mov edi, esi
mov esi, edx
jmp qword ptr [rcx + 8*rax] # TAILCALL
choose_ptr(int, int): # @choose_ptr(int, int)
mov rax, qword ptr [rip + selector]
jmp rax # TAILCALL
choose_if
是最幼稚的,也是最朴素的。 只有两个周期的延迟。 需要更多的工作:它访问内存并进行间接调用,这两者都可能很昂贵。它没有优化到上面的简单版本,即使这在技术上是可行的。 只是一个间接(尾部)调用。cmove
choose_ptr
cmove
choose_ptr
在某些体系结构上,调用函数指针可以击败 if 语句,这在原则上是合理的,尤其是在没有分支预测形式的非常简单的 CPU 上。但是,由于函数指针的优化不佳,并且由于 等无分支指令,这在x86_64上极不可能。cmov
还要记住,访问内存比使用寄存器更潜在、更昂贵,并且函数指针的大多数使用都会迫使您访问内存。
基准
这个极其幼稚的基准显然不能代表 if 语句与函数指针的整体关系,但证实了这三个函数的性能与直觉相符。
评论
bool
ChoosePtr
ChoosePtrPrecomputed
评论
if
if