AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • 主页
  • 系统&网络
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • 主页
  • 系统&网络
    • 最新
    • 热门
    • 标签
  • Ubuntu
    • 最新
    • 热门
    • 标签
  • Unix
    • 最新
    • 标签
  • DBA
    • 最新
    • 标签
  • Computer
    • 最新
    • 标签
  • Coding
    • 最新
    • 标签
主页 / coding / 问题 / 79597905
Accepted
Grigory Rechistov
Grigory Rechistov
Asked: 2025-04-29 16:13:47 +0800 CST2025-04-29 16:13:47 +0800 CST 2025-04-29 16:13:47 +0800 CST

微基准测试示例,证明代码内联并不总是有利于性能

  • 772

TL;DR:许多资料都提到,过度内联函数有时会因为代码膨胀或其他因素而损害应用程序性能。有没有一个实际的程序示例可以以可衡量的方式证明这一点?


记住,微基准测试的使命是放大程序性能的某些方面。正因如此,任何人都可以轻松生成一个微基准测试,让任何特定问题看起来都像是大问题。// Rico Mariani 的性能花絮

我接触过的许多程序员都认为函数内联对应用程序性能绝对有益。我经常审查的 C/C++ 代码中,函数内联inline(或类似的)关键字被随意地应用于函数,无论其大小、用途、热度或位置如何。

很多情况下,这种奇怪的习惯(这里称之为“内联病” )对整体性能并无害:现代编译器对于哪些代码需要内联有着很好的判断能力,而且很少有代码会因为热度高到让(非)内联产生任何影响。然而,它往往会对最终的软件设计造成损害:更多内容最终会堆积在头文件中,文件不再能够独立编译,等等。

虽然很容易证明,在没有连续基准测试的情况下应用随机内联不会对最终性能产生任何可衡量的差异,但我正在寻找一个极端的例子,强制执行该问题会严格损害性能。

微基准测试就足够了;虽然它无法证明内联对实际应用的任何影响,但它应该能够证明,盲目地强制执行内联并非无条件的好主意。这实际上是几乎所有代码优化过程背后的想法:它可能有帮助,也可能有害,有时甚至没有区别。

此类示例微基准测试的一些要求。

  1. 它应该是一个相当短的程序,最好是用 C 或 C++ 编写的;其他可以强制内联的语言也欢迎。
  2. 它不必是执行任何有用操作的程序,它可能会做一些“愚蠢”的事情只是为了加载/强调底层硬件。
  3. 应该支持两种编译模式:强制内联和禁用内联。可以使用任何技术来实现:条件编译来重新定义内联注解,编译器后端标志来控制内联等等。
  4. 无论在哪一种模式下进行编译,它都应该格式良好并表现出相同的明确定义的行为。
  5. 它应该包含至少两个函数,一个调用另一个函数,目的是影响其中至少一个函数的内联。
  6. 它可能包含任何保证/强制函数内联的技术。例如,可以使用标准inline关键字或编译器特定的扩展(__forceinline等__attribute__ ((always_inline)))来指示编译器执行此操作,无论其判断如何。
  7. 运行时,可以轻松报告性能(延迟、执行时间或类似指标)。它可以仅使用time a.out,也可以在受影响的代码周围内部调用计时工具。
  8. 最后,当由特定版本中的至少一个特定编译器进行编译并在至少一个目标系统上运行时,所得到的程序的两个变体表现出统计上的显著差异,并且强制内联构建比非内联构建更慢。

我确实意识到性能高度依赖于主机参数;在一台机器上较慢的程序在另一台机器上可能会变得同样快甚至更快。但我正在寻找一种最坏的情况,即不受限制的内联显然会适得其反。

理想情况下,不影响内联的其他编译器后端选项(例如整体优化级别等)对于两个构建应该是相同的,以排除可观察到的差异由它们解释而不是由应用/跳过的内联解释的可能性。


我对此类程序的起点有一个想法,但我需要进一步的帮助来充实它:

  1. 内部函数非常庞大,几乎无法放入 CPU 的指令缓存中。
  2. 外部函数足够大,因此,如果将内部函数强制内联到其中,则生成的代码部分将大于 CPU 的指令缓存。
  3. 程序的控制流以这样的方式组织:当所有内容都内联时,它会经历更高频率的指令缓存未命中、缓存刷新或类似事件,如果不强制内联,则不会发生这些事件。
performance
  • 1 1 个回答
  • 43 Views

1 个回答

  • Voted
  1. Best Answer
    Sam Mason
    2025-04-29T23:43:24+08:002025-04-29T23:43:24+08:00

    我曾经尝试过并使用 GCC 的循环展开从以下 C 代码中获得大量机器代码:

    #include <stdint.h>
    
    // from https://prng.di.unimi.it/xoshiro256starstar.c
    static inline uint64_t rotl(const uint64_t x, int k) {
        return (x << k) | (x >> (64 - k));
    }
    static uint64_t s[4];
    // a fast PRNG that will get inlined to generate lots of code
    static uint64_t next(void) {
        const uint64_t result = rotl(s[1] * 5, 7) * 9;
    
        const uint64_t t = s[1] << 17;
    
        s[2] ^= s[0];
        s[3] ^= s[1];
        s[1] ^= s[2];
        s[0] ^= s[3];
    
        s[2] ^= t;
    
        s[3] = rotl(s[3], 45);
    
        return result;
    }
    
    uint64_t benchmark() {
      uint64_t sum = 0;
    #ifdef UNROLL
      #pragma GCC unroll 65534
    #endif
      for (int i = 0; i < 5000; i++) {
    // do something
        if (sum & 1) {
          sum += next() >> 60;
        } else {
          sum += 1;
        }
      }
    
      return sum;
    }
    
    #include <inttypes.h>
    #include <stdio.h>
    #include <sys/random.h>
    #include <time.h>
    
    int main() {
      struct timespec t0, t1;
    
      // initialise from urandom
      getrandom(s, sizeof(s), 0);
    
      // run test 5 times
      for (int i = 0; i < 5; i++) {
        clock_gettime(CLOCK_MONOTONIC, &t0);
    
        uint64_t res = benchmark();
    
        clock_gettime(CLOCK_MONOTONIC, &t1);
    
        double dt = t1.tv_sec - t0.tv_sec;
        dt += (t1.tv_nsec - t0.tv_nsec) / 1e9;
    
        printf("took %.1f us to calculate %" PRIu64 "\n", dt * 1e6, res);
      }
    }
    

    保存inlinecost.c并通过以下方式进行编译:

    gcc -Wall -Os -o inlinecost inlinecost.c
    gcc -Wall -Os -o inlinecost-unrolled inlinecost.c -DUNROLL
    

    给我以下二进制文件:

    -rwxr-xr-x 1 smason smason  15656 Apr 29 16:30 inlinecost
    -rwxr-xr-x 1 smason smason 597288 Apr 29 16:30 inlinecost-unrolled
    

    表明它确实生成了更多的代码。

    跑步inlinecost给我带来:

    took 12.4 us to calculate 26605
    took 12.0 us to calculate 26265
    took 12.3 us to calculate 26759
    took 12.1 us to calculate 26487
    took 12.3 us to calculate 26499
    

    同时inlinecost-unrolled给了我:

    took 167.4 us to calculate 27161
    took 28.1 us to calculate 26685
    took 24.8 us to calculate 26297
    took 25.0 us to calculate 26388
    took 24.2 us to calculate 26763
    

    您可以看到非内联代码运行得更加一致,而展开版本花费 10 倍的时间将机器代码从 RAM 加载到缓存中并运行它,然后“仅”花费两倍的时间来执行它。

    让循环benchmark产生更多的迭代(例如增加 5000 到 10000)会使这种差异更加明显,但代价是编译时间会更长。

    这是GodBolt 的链接,其中仅展开了 5 次迭代(太多次迭代会导致构建超时,因为它生成了太多代码),显示它正在内联 PRNG。

    希望这有用!

    更新:尝试改变benchmark如下方式:

    uint64_t benchmark() {
      uint64_t sum = next();
    #ifdef UNROLL
    #pragma GCC unroll 65534
    #endif
      for (int i = 0; i < 30000; i++) {
        if (sum == 0) {
          uint64_t x = 0;
    #ifdef UNROLL
    #pragma GCC unroll 1024
    #endif
          for (int j = 0; j < 4; j++) {
            x ^= next();
          }
          sum += x >> 60;
        } else {
          sum += 1;
        }
      }
    
      return sum;
    }
    

    这使得展开版本第一次运行大约需要 400µs,后续迭代大约需要 50µs,而循环版本似乎可靠地需要 7µs。我原本以为分支预测器处理这么多代码会很吃力,但至少我的 CPU 表现得非常好——一台 AMD 9900X,也就是 Zen5,不知道为什么我在下面的评论中想起了 Zen4。

    • 1

相关问题

  • 提高斯特林数计算效率

  • Haskell 速度问题,执行程序的两个部分比单独执行任一部分花费的时间要长得多

  • 为什么优化后的 g 脚本代码比“低效”的代码慢?

  • 如何让JMeter用户不等待响应

  • 如何解释两个处理器之间巨大的执行速度差异?

Sidebar

Stats

  • 问题 205573
  • 回答 270741
  • 最佳答案 135370
  • 用户 68524
  • 热门
  • 回答
  • Marko Smith

    重新格式化数字,在固定位置插入分隔符

    • 6 个回答
  • Marko Smith

    为什么 C++20 概念会导致循环约束错误,而老式的 SFINAE 不会?

    • 2 个回答
  • Marko Smith

    VScode 自动卸载扩展的问题(Material 主题)

    • 2 个回答
  • Marko Smith

    Vue 3:创建时出错“预期标识符但发现‘导入’”[重复]

    • 1 个回答
  • Marko Smith

    具有指定基础类型但没有枚举器的“枚举类”的用途是什么?

    • 1 个回答
  • Marko Smith

    如何修复未手动导入的模块的 MODULE_NOT_FOUND 错误?

    • 6 个回答
  • Marko Smith

    `(表达式,左值) = 右值` 在 C 或 C++ 中是有效的赋值吗?为什么有些编译器会接受/拒绝它?

    • 3 个回答
  • Marko Smith

    在 C++ 中,一个不执行任何操作的空程序需要 204KB 的堆,但在 C 中则不需要

    • 1 个回答
  • Marko Smith

    PowerBI 目前与 BigQuery 不兼容:Simba 驱动程序与 Windows 更新有关

    • 2 个回答
  • Marko Smith

    AdMob:MobileAds.initialize() - 对于某些设备,“java.lang.Integer 无法转换为 java.lang.String”

    • 1 个回答
  • Martin Hope
    Fantastic Mr Fox msvc std::vector 实现中仅不接受可复制类型 2025-04-23 06:40:49 +0800 CST
  • Martin Hope
    Howard Hinnant 使用 chrono 查找下一个工作日 2025-04-21 08:30:25 +0800 CST
  • Martin Hope
    Fedor 构造函数的成员初始化程序可以包含另一个成员的初始化吗? 2025-04-15 01:01:44 +0800 CST
  • Martin Hope
    Petr Filipský 为什么 C++20 概念会导致循环约束错误,而老式的 SFINAE 不会? 2025-03-23 21:39:40 +0800 CST
  • Martin Hope
    Catskul C++20 是否进行了更改,允许从已知绑定数组“type(&)[N]”转换为未知绑定数组“type(&)[]”? 2025-03-04 06:57:53 +0800 CST
  • Martin Hope
    Stefan Pochmann 为什么 {2,3,10} 和 {x,3,10} (x=2) 的顺序不同? 2025-01-13 23:24:07 +0800 CST
  • Martin Hope
    Chad Feller 在 5.2 版中,bash 条件语句中的 [[ .. ]] 中的分号现在是可选的吗? 2024-10-21 05:50:33 +0800 CST
  • Martin Hope
    Wrench 为什么双破折号 (--) 会导致此 MariaDB 子句评估为 true? 2024-05-05 13:37:20 +0800 CST
  • Martin Hope
    Waket Zheng 为什么 `dict(id=1, **{'id': 2})` 有时会引发 `KeyError: 'id'` 而不是 TypeError? 2024-05-04 14:19:19 +0800 CST
  • Martin Hope
    user924 AdMob:MobileAds.initialize() - 对于某些设备,“java.lang.Integer 无法转换为 java.lang.String” 2024-03-20 03:12:31 +0800 CST

热门标签

python javascript c++ c# java typescript sql reactjs html

Explore

  • 主页
  • 问题
    • 最新
    • 热门
  • 标签
  • 帮助

Footer

AskOverflow.Dev

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve