针对已知的更常见的路径优化分支

Optimizing a branch for a known more-common path

本文关键字:路径 优化 分支 常见      更新时间:2023-10-16

请考虑以下代码段:

void error_handling();
bool method_impl();
bool method()
{
    const bool res = method_impl();
    if (res == false) {
        error_handling();
        return false;
    }
    return true;
}

我知道method_impl()会返回 99.999%true(是的,小数点后三位),但我的编译器没有。 就时间消耗而言,method()部分至关重要。

  1. 我是否应该重写method()(并使其可读性降低)以确保只有在method_impl()返回false时才会发生跳转?如果是,如何?
  2. 应该让编译器为我完成工作吗?
  3. 我应该让 CPU 的分支预测为我完成工作吗?

根据其他答案的建议,我对解决方案进行了基准测试。如果您考虑对这个答案投赞成票,也请对其他人投赞成票。

基准代码

#include <iostream>
#include <iomanip>
#include <string>
// solutions
#include <ctime>
// benchmak
#include <limits>
#include <random>
#include <chrono>
#include <algorithm>
#include <functional>
//
// Solutions
//
namespace
{
    volatile std::time_t near_futur = -1;
    void error_handling() { std::cerr << "errorn"; }
    bool method_impl() { return std::time(NULL) != near_futur; }
    bool method_no_builtin()
    {
        const bool res = method_impl();
        if (res == false) {
            error_handling();
            return false;
        }
        return true;
    }
    bool method_builtin()
    {
        const bool res = method_impl();
        if (__builtin_expect(res, 1) == false) {
            error_handling();
            return false;
        }
        return true;
    }
    bool method_builtin_incorrect()
    {
        const bool res = method_impl();
        if (__builtin_expect(res, 0) == false) {
            error_handling();
            return false;
        }
        return true;
    }
    bool method_rewritten()
    {
        const bool res = method_impl();
        if (res == true) {
            return true;
        } else {
            error_handling();
            return false;
        }
    }
}
//
// benchmark
//
constexpr std::size_t BENCHSIZE = 10'000'000;
class Clock
{
    std::chrono::time_point<std::chrono::steady_clock> _start;
public:
    static inline std::chrono::time_point<std::chrono::steady_clock> now() { return std::chrono::steady_clock::now(); }
    Clock() : _start(now())
    {
    }
    template<class DurationUnit>
    std::size_t end()
    {
        return std::chrono::duration_cast<DurationUnit>(now() - _start).count();
    }
};
//
// Entry point
//
int main()
{
    {
        Clock clock;
        bool result = true;
        for (std::size_t i = 0 ; i < BENCHSIZE ; ++i)
        {
            result &= method_no_builtin();
            result &= method_no_builtin();
            result &= method_no_builtin();
            result &= method_no_builtin();
            result &= method_no_builtin();
            result &= method_no_builtin();
            result &= method_no_builtin();
            result &= method_no_builtin();
            result &= method_no_builtin();
            result &= method_no_builtin();
        }
        const double unit_time = clock.end<std::chrono::nanoseconds>() / static_cast<double>(BENCHSIZE);
        std::cout << std::setw(40) << "method_no_builtin(): " << std::setprecision(3) << unit_time << " nsn";
    }
    {
        Clock clock;
        bool result = true;
        for (std::size_t i = 0 ; i < BENCHSIZE ; ++i)
        {
            result &= method_builtin();
            result &= method_builtin();
            result &= method_builtin();
            result &= method_builtin();
            result &= method_builtin();
            result &= method_builtin();
            result &= method_builtin();
            result &= method_builtin();
            result &= method_builtin();
            result &= method_builtin();
        }
        const double unit_time = clock.end<std::chrono::nanoseconds>() / static_cast<double>(BENCHSIZE);
        std::cout << std::setw(40) << "method_builtin(): " << std::setprecision(3) << unit_time << " nsn";
    }
    {
        Clock clock;
        bool result = true;
        for (std::size_t i = 0 ; i < BENCHSIZE ; ++i)
        {
            result &= method_builtin_incorrect();
            result &= method_builtin_incorrect();
            result &= method_builtin_incorrect();
            result &= method_builtin_incorrect();
            result &= method_builtin_incorrect();
            result &= method_builtin_incorrect();
            result &= method_builtin_incorrect();
            result &= method_builtin_incorrect();
            result &= method_builtin_incorrect();
            result &= method_builtin_incorrect();
        }
        const double unit_time = clock.end<std::chrono::nanoseconds>() / static_cast<double>(BENCHSIZE);
        std::cout << std::setw(40) << "method_builtin_incorrect(): " << std::setprecision(3) << unit_time << " nsn";
    }
    {
        Clock clock;
        bool result = true;
        for (std::size_t i = 0 ; i < BENCHSIZE ; ++i)
        {
            result &= method_rewritten();
            result &= method_rewritten();
            result &= method_rewritten();
            result &= method_rewritten();
            result &= method_rewritten();
            result &= method_rewritten();
            result &= method_rewritten();
            result &= method_rewritten();
            result &= method_rewritten();
            result &= method_rewritten();
        }
        const double unit_time = clock.end<std::chrono::nanoseconds>() / static_cast<double>(BENCHSIZE);
        std::cout << std::setw(40) << "method_rewritten(): " << std::setprecision(3) << unit_time << " nsn";
    }
}

基准测试结果

g++ -std=c++14 -O2 -Wall -Wextra -Werror main.cpp

               method_no_builtin(): 42.8 ns
                  method_builtin(): 44.4 ns
        method_builtin_incorrect(): 51.4 ns
                method_rewritten(): 39.3 ns

演示

g++ -std=c++14 -O3 -Wall -Wextra -Werror main.cpp

               method_no_builtin(): 32.3 ns
                  method_builtin(): 31.1 ns
        method_builtin_incorrect(): 35.6 ns
                method_rewritten(): 30.5 ns

演示

结论

这些优化之间的差异太小,

无法得出任何结论,除了:如果在针对已知的更常见路径优化分支时发现性能提升,则此收益太小,不值得麻烦和可读性损失。

你可以

建议编译器method_impl()将返回true:

void error_handling();
bool method_impl();
bool method()
{
    const bool res = method_impl();
    if (__builtin_expect (res, 0) == false) {
        error_handling();
        return false;
    }
    return true;
}

这将在海湾合作委员会中工作。

底层硬件已执行此优化。第一次预测会"失败",但在它 en.wikipedia.org/wiki/Branch_predictor 命中正确的选项之后。

您可以尝试应用 GCC 扩展并检查它是否更快,但我认为您几乎看不到它和没有它的任何区别。分支预测始终应用,它不是您启用的

不知道 std::time() 的实现,我不会得出太多结论从这个测试。 从您自己的结果来看,它似乎主导了循环中的时间。

FWIW,我在调整代码时自己自由地使用 possible()/unpossible()。 我不想更改代码的结构,但是在阅读程序集时,我希望看到公共路径是一条未获取分支的直线。 这里(对我来说)的关键是程序集的可读性。 事实上,这也是最快的分支是次要的(最快的可能分支是正确预测的未分支)。