为什么预分配的函数指针的性能比分支差



我有一个带有enum成员变量的类。其中一个成员函数的行为基于这个enum,因此作为一个"可能的"优化,我将两个不同的行为作为两个不同函数,并为类提供一个在构造时设置的成员函数指针。我这样模拟这种情况:

enum catMode {MODE_A, MODE_B};
struct cat
{
    cat(catMode mode) : stamp_(0), mode_(mode) {}
    void
    update()
    {
        stamp_ = (mode_ == MODE_A) ? funcA() : funcB();
    }
    uint64_t stamp_;
    catMode  mode_;
};
struct cat2
{
    cat2(catMode mode) : stamp_(0), mode_(mode)
    {
        if (mode_ = MODE_A)
            func_ = funcA;
        else
            func_ = funcB;
    }
    void
    update()
    {
        stamp_ = func_();
    }
    uint64_t stamp_;
    catMode  mode_;
    uint64_t (*func_)(void);
};

然后我创建了一个cat对象和一个长度为32的数组。我遍历数组将其放入缓存,然后调用cats更新方法32次,并使用rdtsc将延迟存储在数组中。。。

然后,我使用rand()ulseep()和一些任意的strcmp()调用一个循环数百次的函数。。回来后,我再次做32的事情。

结果是,具有分支的方法似乎总是在44+/-10循环附近,而具有函数指针的方法往往在130循环附近。我很好奇为什么会出现这种情况?

如果有什么不同的话,我会期待类似的表现。此外,模板化很难成为一种选择,因为对真正的cat类进行完全的专门化会过于夸张。

如果没有一个完整的SSCCE,我就无法像处理此类问题那样处理这个问题
所以我能做的最好的事情就是推测:

这两种情况的核心区别在于,您有一个分支与一个函数指针。您看到的差异强烈暗示funcA()funcB()是非常小的函数。

可能性#1:

在代码的分支版本中,funcA()funcB()由编译器内联。这不仅跳过了函数调用开销,而且如果函数足够琐碎,那么分支也可以完全优化。

另一方面,函数指针不能内联,除非编译器能够在编译时解析它们。

可能性#2:

通过将分支与函数指针进行比较,可以将分支预测器与分支目标预测器进行比较。

分支目标预测与分支预测不同。在分支的情况下,处理器需要预测分支的方式。在函数指针的情况下,它需要预测分支到哪里。

处理器的分支预测器很可能比其分支目标预测器准确得多。但话说回来,这都是猜测。。。

最新更新