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

YSC*_*YSC 6 c c++ optimization compiler-optimization

请考虑以下代码:

void error_handling();
bool method_impl();

bool method()
{
    const bool res = method_impl();
    if (res == false) {
        error_handling();
        return false;
    }
    return true;
}
Run Code Online (Sandbox Code Playgroud)

我知道当时method_impl()会返回true99.999%(是的,三位小数),但我的编译器没有.method()在时间消耗方面是部分关键.

  1. 我是否应该重写method()(并使其不太可读)以确保只有在method_impl()返回时才会发生跳转false?如果有,怎么样?
  2. 我应该让编译器为我做这项工作吗?
  3. 我应该让我的CPU的分支预测为我工作吗?

Tam*_*nut 6

你可以建议编译器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;
}
Run Code Online (Sandbox Code Playgroud)

这将在GCC中有效.


Nad*_*dir 5

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

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


YSC*_*YSC 5

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

基准代码

#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 << "error\n"; }
    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 << " ns\n";
    }
    {
        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 << " ns\n";
    }
    {
        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 << " ns\n";
    }
    {
        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 << " ns\n";
    }
}
Run Code Online (Sandbox Code Playgroud)

基准结果

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
Run Code Online (Sandbox Code Playgroud)

Demo

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
Run Code Online (Sandbox Code Playgroud)

Demo

结论

这些优化之间的差异很小,无法得出以下结论:如果在为已知的更常见路径优化分支中发现性能提升,则该增益太小而不值得麻烦和可读性损失。

  • 您在第一个测试中进行了 11 次试验,在另外两个测试中进行了 10 次试验。当您在第一个测试中取出第 11 个条目时,它与其他测试一致:http://coliru.stacked-crooked.com/a/2540c1a1f5d2b837。尝试 -02 第一个和最后一个花费完全相同的时间:http://coliru.stacked-crooked.com/a/7f37496b4992755e (3认同)
  • @immibis 好主意:完成。希望你是一个有耐心的人。 (2认同)
  • BHT 和 BPB 等分支预测资源是有限的,在实际应用中,处理器会定期丢失历史记录。如果您的代码的组织方式使得最有可能的路径是处理器静态预测的路径,那么平均而言您将领先。真正有趣的是,由于不采用的分支速度很快,因此对代码重新排序,使不采用的分支比采用的分支更有可能提高性能*无论*预测变量如何。GCC 肯定会根据 __builtin_expect() 提示重新排序代码。 (2认同)