C++ `__builtin_expect` / `__builtin_prefetch`:GCC/Clang 内建函数的高级用法

哈喽,各位好!今天咱们来聊点儿C++里的小秘密,但也可能藏着大能量的东西:__builtin_expect__builtin_prefetch。 这俩哥们儿是GCC和Clang编译器内置的函数,用好了能给你的代码提速,用不好嘛…可能就原地踏步,甚至倒退。别怕,咱们慢慢来,保证让你听明白,用得溜。

一、__builtin_expect:编译器,我跟你打个赌!

想象一下,你是个赌徒,但你不是跟赌场赌,而是跟编译器赌。__builtin_expect就是你用来下注的工具。 你告诉编译器:“嘿,老兄,我觉得这个条件表达式,99%的情况都会是真/假。” 编译器信你,然后优化代码,让你的程序跑得更快。

  • 语法:

    long __builtin_expect (long exp, long c);
    • exp:你要预测的条件表达式。注意,它会被转换成long类型。
    • c:你期望exp的值。通常是 0 (假) 或 1 (真)。
  • 工作原理:

    编译器会根据你的预测,调整生成的汇编代码。如果编译器认为你猜对了,它会把最有可能执行的代码放在更容易访问的位置,比如缓存里。 如果你猜错了,也没关系,程序照样能跑,只是性能可能会受到影响。

  • 为什么要用它?

    现代CPU都有分支预测器。分支预测器会猜测if语句里的条件是真还是假,如果猜对了,CPU就能继续执行,不用等待。如果猜错了,CPU就要回滚,重新执行,这会浪费时间。 __builtin_expect就是用来帮助分支预测器的。你告诉编译器哪个分支更有可能被执行,编译器就能生成更好的代码,让分支预测器更准确。

  • 例子:

    #include <iostream>
    
    int main() {
        int x = 5;
        // 告诉编译器,x > 0 几乎总是真的
        if (__builtin_expect(x > 0, 1)) {
            std::cout << "x is positiven";  // 大概率执行
        } else {
            std::cout << "x is not positiven"; // 小概率执行
        }
    
        return 0;
    }

    在这个例子中,我们告诉编译器 x > 0 几乎总是真的。编译器可能会把std::cout << "x is positiven"; 这行代码放在更快的路径上,让程序跑得更快。

  • 更实际的例子:

    考虑一个循环,循环里有一个条件判断,大部分情况下条件都是真的。

    #include <iostream>
    #include <vector>
    
    int main() {
        std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        int count = 0;
    
        for (int i = 0; i < data.size(); ++i) {
            // 假设data[i] < 5 发生的概率很低
            if (__builtin_expect(data[i] < 5, 0)) {
                count++;
            }
        }
    
        std::cout << "Count: " << count << std::endl;
        return 0;
    }

    在这个例子里,我们告诉编译器 data[i] < 5 发生的概率很低。如果大部分情况下 data[i] 都大于等于5,那么编译器就能更好地优化循环,减少分支预测错误的次数。

  • 什么时候用?

    • 当你知道某个条件表达式,在大部分情况下都会是真或假的时候。
    • 在性能敏感的代码里,比如循环、排序算法等。
    • 避免过度使用! 不要滥用 __builtin_expect,否则可能会适得其反。只有在你确实对条件表达式的概率有把握的时候才使用它。
  • 注意事项:

    • __builtin_expect 只是给编译器的提示,编译器可以选择忽略它。
    • 不同的编译器对 __builtin_expect 的处理方式可能不一样。
    • 不要用 __builtin_expect 来改变程序的逻辑。它只是用来优化性能的。
    • 务必进行性能测试,验证 __builtin_expect 是否真的提高了性能。

二、__builtin_prefetch:提前备货,缓存加速!

__builtin_prefetch 就像一个仓库管理员,提前把你需要的东西从仓库里搬到你的办公室门口,等你用的时候就能立刻拿到,不用再跑去仓库找了。 它的作用就是提前把数据加载到缓存里,等你真正需要用的时候,就能更快地访问。

  • 语法:

    void __builtin_prefetch (const void *addr, int rw, int locality);
    • addr:你要预取的内存地址。
    • rw:读写意向。
      • 0:只读 (默认值)
      • 1:将要写入
    • locality:局部性提示,表示预取的数据被使用的可能性。
      • 0:预取后可能不会被重用 (最低级别)
      • 1:预取后可能只会使用一次
      • 2:预取后可能会使用几次
      • 3:预取后会被经常使用 (最高级别)
  • 工作原理:

    CPU的缓存是分级的,L1缓存最快,但容量最小,L2缓存次之,L3缓存再次之,内存最慢。 __builtin_prefetch 可以让你控制把数据加载到哪一级缓存。 一般来说,locality 的值越高,数据就越有可能被加载到更快的缓存里。

  • 为什么要用它?

    访问内存是很慢的操作。如果你的程序需要频繁地访问内存,那么性能可能会受到影响。 __builtin_prefetch 可以让你提前把数据加载到缓存里,减少访问内存的次数,提高程序的速度。

  • 例子:

    #include <iostream>
    #include <vector>
    
    int main() {
        std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
        // 提前预取data[5]的数据,只读,locality最高
        __builtin_prefetch(&data[5], 0, 3);
    
        // 一段时间后,访问data[5]
        std::cout << "data[5]: " << data[5] << std::endl;
    
        return 0;
    }

    在这个例子中,我们提前预取了 data[5] 的数据。当程序真正访问 data[5] 的时候,数据可能已经在缓存里了,所以访问速度会更快。

  • 更实际的例子:

    考虑一个遍历数组的循环。

    #include <iostream>
    #include <vector>
    
    int main() {
        std::vector<int> data(1024);
        for (int i = 0; i < data.size(); ++i) {
            data[i] = i;
        }
    
        // 预取步长,可以根据实际情况调整
        int prefetch_distance = 64;
    
        for (int i = 0; i < data.size(); ++i) {
            // 提前预取prefetch_distance个元素之后的数据
            if (i + prefetch_distance < data.size()) {
                __builtin_prefetch(&data[i + prefetch_distance]);
            }
    
            // 使用data[i]
            data[i] = data[i] * 2;
        }
    
        for (int i = 0; i < 10; ++i) {
            std::cout << data[i] << " ";
        }
        std::cout << std::endl;
    
        return 0;
    }

    在这个例子里,我们在循环中提前预取了未来要访问的数据。这样可以减少CPU等待内存的时间,提高循环的速度。 prefetch_distance 的值需要根据实际情况调整,太小了预取效果不明显,太大了可能会浪费资源。

  • 什么时候用?

    • 当你的程序需要频繁地访问内存的时候。
    • 在遍历数组、链表等数据结构的时候。
    • 在进行矩阵运算、图像处理等操作的时候。
    • 避免过度使用! 预取太多数据可能会导致缓存污染,反而降低性能。
  • 注意事项:

    • __builtin_prefetch 只是给CPU的提示,CPU可以选择忽略它。
    • 不同的CPU对 __builtin_prefetch 的处理方式可能不一样。
    • 不要用 __builtin_prefetch 来改变程序的逻辑。它只是用来优化性能的。
    • 务必进行性能测试,验证 __builtin_prefetch 是否真的提高了性能。
    • 预取距离 (prefetch distance) 需要根据缓存大小、数据访问模式等因素进行调整。

三、__builtin_expect__builtin_prefetch 的最佳实践

  • 性能测试,性能测试,还是性能测试! 这是最重要的。不要想当然地认为用了这两个函数就能提高性能。一定要进行性能测试,验证你的代码是否真的变快了。
  • 使用合适的编译选项。 确保你的编译器开启了优化选项 (比如 -O2-O3)。
  • 了解你的硬件。 不同的CPU和缓存架构,对 __builtin_expect__builtin_prefetch 的效果可能会不一样。
  • 代码可读性。 不要为了优化而牺牲代码的可读性。如果你的代码变得难以理解,那么维护成本可能会更高。
  • 结合使用。 __builtin_expect__builtin_prefetch 可以结合使用,以达到更好的性能。

四、表格总结:

特性 __builtin_expect __builtin_prefetch
作用 告诉编译器某个条件表达式的概率,帮助分支预测器 提前把数据加载到缓存里,减少访问内存的次数
语法 long __builtin_expect (long exp, long c); void __builtin_prefetch (const void *addr, int rw, int locality);
参数 exp: 条件表达式, c: 期望的值 (0 或 1) addr: 内存地址, rw: 读写意向, locality: 局部性提示
使用场景 条件判断,循环,性能敏感的代码 频繁访问内存,遍历数据结构,矩阵运算,图像处理
注意事项 只是给编译器的提示,需要性能测试,避免过度使用 只是给CPU的提示,需要性能测试,避免缓存污染,调整预取距离
是否改变程序逻辑

五、高级用法和注意事项

  • 结合模板元编程: 可以使用模板元编程在编译时确定一些条件,并根据这些条件使用 __builtin_expect
  • 自定义内存分配器: 在自定义内存分配器中,可以使用 __builtin_prefetch 提前预取即将分配的内存块,以提高分配速度。
  • NUMA架构: 在NUMA (Non-Uniform Memory Access) 架构下,可以使用 __builtin_prefetch 将数据预取到离当前线程更近的内存节点上,减少跨节点访问的延迟。
  • 并发编程: 在多线程程序中,可以使用 __builtin_prefetch 提前预取其他线程可能需要访问的数据,以减少线程间的竞争。 但是需要谨慎使用,避免造成不必要的缓存失效。
  • 调试: 调试使用了 __builtin_expect__builtin_prefetch 的代码可能会比较困难。 可以使用编译器提供的选项 (比如 -fno-builtin) 来禁用这些内建函数,方便调试。
  • 编译器版本: 不同的编译器版本对 __builtin_expect__builtin_prefetch 的支持程度可能不一样。 需要查阅编译器文档,了解具体的用法和限制。

六、总结

__builtin_expect__builtin_prefetch 是C++里强大的优化工具,但也需要谨慎使用。 记住,性能优化是一个迭代的过程,需要不断地测试和调整。 不要盲目地使用这些技巧,一定要了解它们的原理,并根据实际情况进行选择。 希望今天的讲解能帮助你更好地理解和使用这两个内建函数,让你的代码跑得更快!

好了,今天的讲座就到这里。 谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注