【问题标题】:Optimizing a branch for a known more-common path针对已知的更常见路径优化分支
【发布时间】:2016-06-26 13:57:58
【问题描述】:

请考虑以下代码:

void error_handling();
bool method_impl();

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

我知道method_impl() 将返回true 99.999%(是的,三位小数)的时间,但我的编译器没有。 method() 在时间消耗方面是部分关键。

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

【问题讨论】:

  • 标题的“优化分支预测”怎么样,因为它与错误处理无关?我很确定答案类似于use likely/unlikely macros,但这是特定于编译器的。
  • 您应该反转逻辑,以便正常情况下是失败,异常情况是隐含的else。这不会破坏可读性。你现在拥有它的方式是 99.999% 的时间都有一个分支。
  • 您可以使用 __builtin_expect 提示编译器提出您的建议:stackoverflow.com/questions/1851299/…
  • 底层硬件已经执行了这个优化。第一次预测它会“失败”,但之后它会命中正确的选项en.wikipedia.org/wiki/Branch_predictor
  • @Nadir 你认为分支预测可以和使用 GCC 扩展 __builtin_expect 一样好吗?

标签: c++ c optimization compiler-optimization


【解决方案1】:

您可以建议编译器 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;
}

这适用于 GCC。

【讨论】:

【解决方案2】:

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

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

【讨论】:

    【解决方案3】:

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

    基准代码

    #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";
        }
    }
    

    基准测试结果

    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
    

    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
    

    Demo

    结论

    这些优化之间的差异太小,无法得出任何结论,除了:如果在优化已知更常见路径的分支时可以找到性能增益,那么这个增益太小,不值得麻烦,并且可读性下降。

    【讨论】:

    • 第一个测试中有 11 个试验,另外两个有 10 个试验。当您在第一个测试中取出第 11 个条目时,它与其他测试一致:coliru.stacked-crooked.com/a/2540c1a1f5d2b837。尝试使用 -02 第一个和最后一个花费完全相同的时间:coliru.stacked-crooked.com/a/7f37496b4992755e
    • @Sqeaky Ho 谢谢!要是我能给你代表的话就好了!我已经编辑了我的答案及其结论。再次感谢。
    • @immibis 好主意:完成。希望你是一个有耐心的人。
    • 我需要修改我的评论,说显然分支性能也很重要,并且可能比 TLB/icache 更重要。我的印象是,正确预测的分支具有 0-1 个周期的延迟,但只有在正确预测分支 并且 未采用时才会出现这种情况。如果采用分支,即使正确预测,延迟也会显着延长。最坏的情况被预测为未采取但随后采取。这篇博文给出了各种 x86 CPU 以及 Apple M1 的测量结果:blog.cloudflare.com/branch-predictor
    • BHT 和 BPB 等分支预测资源是有限的,在实际应用中,处理器会定期丢失历史记录。如果你的代码被组织成最可能的路径是处理器静态预测的路径,那么平均而言你会领先。真正有趣的是,由于未采用的分支速度很快,因此重新排序代码以使分支更有可能不采用而不是采用可提高预测器的性能无论。 GCC 肯定会根据 __builtin_expect() 提示重新排序代码。
    【解决方案4】:

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

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

    【讨论】:

    • 1/ 这不是答案,而是对我的答案的评论。
    • 2/ std::time 主导循环中的时间消耗是重点。我会让你再读一遍这个问题;)
    • @T Thorn - 提到 std::time() 的段落确实应该是对上一篇文章的评论,正如我们所见,这会分散人们对可能()/不太可能的实际有效观点的注意力()。我建议删除第一段。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多