首页 > 编程语言 >Modern C++——使用分支预测优化代码性能

Modern C++——使用分支预测优化代码性能

时间:2024-09-06 19:24:58浏览次数:12  
标签:C++ 代码 Modern likely value 编译器 unlikely 优化 分支

大纲

在C++20中,新引入了一对属性关键字[[likely]][[unlikely]],它们用于为编译器提供关于代码分支执行概率的额外信息,以帮助编译器进行更好的优化。这对属性是基于长期实践中开发人员对程序执行路径的深入理解而设计的,特别是在面对复杂逻辑和频繁分支的情况下。

[[likely]]

[[likely]]属性用于标记某个分支条件在运行时更有可能为真。当编译器遇到这种标记时,它会尝试优化与该分支相关的代码,以提高整体的执行效率。具体来说,编译器可能会重新组织指令序列,使更可能执行的路径在缓存中更频繁地命中,从而减少对主内存的访问,并降低指令的等待时间。

[[unlikely]]

相反,[[unlikely]]属性用于标记某个分支条件在运行时不太可能为真。编译器在处理这样的分支时,会考虑优化与该分支相关联的代码的加载和执行,以便减少对处理器资源的占用,特别是当这个不太可能发生的分支实际上未执行时。通过减少对不太可能执行的路径的优化投资,编译器可以集中资源来优化更常执行的代码部分。

样例

#include <chrono>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <random>
#include <functional>
 
namespace with_attributes {
    constexpr double pow(double x, long long n) noexcept {
        if (n <= 0) [[unlikely]]
            return 1;
        else [[likely]]
            return x * pow(x, n - 1);
    }
} // namespace with_attributes
 
namespace no_attributes {
    constexpr double pow(double x, long long n) noexcept {
        if (n <= 0)
            return 1;
        else
            return x * pow(x, n - 1);
    }
} // namespace no_attributes

double calc(double x, std::function<double(double, long long)> f) noexcept {
    constexpr long long precision{16LL};
    double y{};
    for (auto n{0LL}; n < precision; n += 2LL)
        y += f(x, n);
    return y;
}

double gen_random() noexcept {
    static std::random_device rd;
    static std::mt19937 gen(rd());
    static std::uniform_real_distribution<double> dis(-1.0, 1.0);
    return dis(gen);
}
 
volatile double sink{}; // ensures a side effect
 
int main() {
    auto benchmark = [](auto fun, auto rem)
    {
        const auto start = std::chrono::high_resolution_clock::now();
        for (auto y{1ULL}; y != 500'000'000ULL; ++y)
            sink = calc(gen_random(), fun);
        const std::chrono::duration<double> diff =
            std::chrono::high_resolution_clock::now() - start;
        std::cout << "Time: " << std::fixed << std::setprecision(6) << diff.count()
                  << " sec " << rem << std::endl; 
    };
 
    benchmark(with_attributes::pow, "(with attributes)");
    benchmark(no_attributes::pow, "(without attributes)");
    benchmark(with_attributes::pow, "(with attributes)");
    benchmark(no_attributes::pow, "(without attributes)");
    benchmark(with_attributes::pow, "(with attributes)");
    benchmark(no_attributes::pow, "(without attributes)");
}

上面代码的pow函数中n的值大部分时候大于0,于是我们在with_attributes::pow的else部分标记为[[likely]];而很少出现的小于等于0的时候,使用[[unlikely]]标记。这样编译器就会根据我们的标记来优化代码。

作为对照组,no_attributes::pow除了没有标记外和with_attributes::pow完全一致。

我们看下对比结果。
在这里插入图片描述
可以发现使用了分支预测标记的代码效率更高(本例提升了约20%性能)。

应用场景

这些属性的应用可以帮助提升在复杂分支结构下的程序性能。特别是在性能敏感的应用中,如实时系统高性能计算科学计算,合理使用[[likely]][[unlikely]]属性可以显著提升执行效率。然而,它们并不是万能的,开发者需要基于程序的实际执行路径和统计数据来谨慎选择使用。

题外

在研究这个专题时,我最开始预计编译器会优化分支顺序,于是设计了如下的代码

    if (value == 0) [[unlikely]] {
        sum += value.get_value();
    } else if (value == 1) [[unlikely]] {
        sum += value.get_value();
    } else if (value == 2) [[unlikely]] {
        sum += value.get_value();
    } else if (value == 3) [[unlikely]] {
        sum += value.get_value();
    } else if (value == 4) [[unlikely]] {
        sum += value.get_value();
    } else if (value == 5) [[unlikely]] {
        sum += value.get_value();
    } else if (value == 6) [[likely]] {
        sum += value.get_value();
    } 

如果我们的设想成立,则编译器进入如下编译优化:先对比value==6这个条件,然后再对比其他条件。这样经常执行的value==6的分支只要执行一次对比,而省去了多余的其他5次对比。但是我在Linux和Windows两个平台上都做了实验,发现编译器并没有因为我们的标记而优化条件对比的顺序。所以这优化类还需要我们程序员自己来做。

如果没有做顺序调优,那么编译器对本例进行了什么优化导致优20%的性能提升呢?这个问题我们会在《从汇编层看64位程序运行——likely提示编译器的优化案例》中解答。

参考代码

https://github.com/f304646673/cpulsplus/tree/master/likely

参考资料

标签:C++,代码,Modern,likely,value,编译器,unlikely,优化,分支
From: https://blog.csdn.net/breaksoftware/article/details/141598433

相关文章

  • 枚举: C++和Python实现鸡兔同笼问题
    作者制作不易,关注、点赞、收藏一下吧!目录1.Python实现2.C++实现1.Python实现首先,我们需要输入头和脚的数量:head=int(input("请输入头的数量:"))feet=int(input("请输入脚的数量:"))input()实现输入,int()实现把字符串型(str)换为整型(int)。然后,进行循环......
  • 常用的hook代码
    //==UserScript==//@namehookxxx//@namespacehttp://tampermonkey.net///@version0.1//@descriptiontrytotakeovertheworld!//@authorYou//@run-atdocument-start//@matchhttps://match.yuanrenxue.cn/match......
  • c++的面向过程与面向对象
    面向过程与面向对象面向过程:在编程时重点考虑如何解决问题,以及解决问题的具体步骤。面向对象:在编程时重点考虑的是"谁"能解决问题(类、结构),以及"它"解决问题时所需要属性(成员变量)和功能(成员函数)。抽象:把“解决问题者”当作思考或观察对象,把解决问题所需的具备的属性和功能......
  • c++的类和对象
    类和对象什么是类把抽象结果(利用面向对象的思维模式,思考、观察出的结果),使用用C++的语法封装出一种类似结构的自定义数据类型(复合数据类型)。如何设计类struct结构名{  成员函数;//结构的成员默认访问权限是public  成员变量;};​class类名{  成员......
  • 拥抱数智化,JNPF低代码平台如何推动企业转型升级
    随着信息技术的飞速发展,企业面临的市场竞争日益激烈,传统的业务流程和管理模式已经难以满足快速变化的市场需求。数智化转型成为企业持续发展的必由之路。在这一过程中,低代码开发平台扮演了至关重要的角色。本文将探讨JNPF低代码平台如何助力企业拥抱数智化,实现转型升级。什么......
  • 一键解锁企业数智化转型:JNPF低代码平台的实践与应用
    随着信息技术的飞速发展,企业面临的市场竞争日益激烈,数智化转型已成为企业持续发展的必由之路。数智化转型不仅涉及技术层面的革新,还包括业务流程、管理模式以及企业文化等多方面的深刻变革。在这一过程中,低代码平台作为一种新兴的开发工具,正逐渐成为企业实现快速转型的重要推手......
  • git如何回滚代码
     方法一:使用gitreset命令命令可以将当前分支的HEAD指针指向指定的提交,从而回退代码到指定版本。该命令有三种模式:–soft、–mixed和--hard。它们的区别在于回退代码的程度不同。--mixed(默认):将HEAD指针和暂存区都回退到指定提交,但不改变工作区的内容。--soft......
  • 修复Microsoft Visual C++ 2015中msvcp140_ATOMIC_WAIT.dll缺失的5大策略
    在电脑使用过程中,我们经常会遇到一些错误提示,其中之一就是“msvcp140_ATOMIC_WAIT.dll丢失”。这个错误提示通常出现在运行某些程序或游戏时,给使用者带来了很大的困扰。那么,如何解决这个问题呢?一,原因分析msvcp140_ATOMIC_WAIT.dll是MicrosoftVisualC++2015运行时库的一部......
  • C++常见知识掌握
    1.Linux软件开发、调试与维护内核与系统结构Linux内核是操作系统的核心,负责管理硬件资源,提供系统服务,它是系统软件与硬件之间的桥梁。主要组成部分包括:进程管理:内核通过调度器分配CPU时间给各个进程,实现进程的创建、调度、终止等操作。使用进程描述符(task_struct)来存储进程......
  • 代码大全避免命名冲突的策略
    在编程中,避免全局命名空间的命名冲突是一个重要的考虑,因为这有助于保持代码的清晰和可维护性。以下是一些在不同编程语言中划分全局命名空间和避免命名冲突的策略:C++和C#在C++和C#中,可以使用namespace关键字来组织代码,避免命名冲突。cpp//C++示例namespaceUser......