4

我正在开发一个(相当大的)现有单线程 C 应用程序。在这种情况下,我修改了应用程序以执行一些非常少的额外工作,包括每次调用特殊函数时递增计数器(此函数被调用约 80.000 次)。该应用程序在运行 64 位 Linux 内核 3.2.0-31-generic 和 -O3 选项的 Ubuntu 12.04 上编译。

令人惊讶的是,代码的检测版本运行得更快,我正在调查原因。我测量执行时间clock_gettime(CLOCK_PROCESS_CPUTIME_ID)并获得有代表性的结果,我报告了超过 100 次运行的平均执行时间值。此外,为了避免来自外界的干扰,我尽可能尝试在没有任何其他应用程序运行的系统中启动应用程序(附带说明,因为 CLOCK_PROCESS_CPUTIME_ID 返回进程时间而不是挂钟时间,其他应用程序“应该”在理论上只影响缓存而不直接影响进程执行时间)

我怀疑“指令缓存效应”,也许稍微大一点(几个字节)的检测代码在缓存中的适合度不同且更好,这个假设是否可以想象?我尝试使用 valegrind --tool=cachegrind 进行一些缓存调查,但不幸的是,检测版本比初始版本具有更多的缓存未命中(因为它似乎是合乎逻辑的)。

欢迎任何关于这个主题的提示和可能有助于找出为什么检测代码运行得更快的想法(一些 GCC 优化在一种情况下可用,而在另一种情况下不可用,为什么?,...)

4

2 回答 2

4

由于问题中没有太多细节,我只能推荐一些在调查问题时需要考虑的因素。

很少有额外的工作(例如递增计数器)可能会改变编译器关于是否应用一些优化的决定。编译器并不总是有足够的信息来做出完美的选择。它可能会尝试优化瓶颈是代码大小的速度。当没有太多数据需要处理时,它可能会尝试自动矢量化计算。编译器可能不知道要处理什么样的数据或将执行代码的 CPU 的确切型号是什么。

  1. 增加计数器可能会增加某些循环的大小并阻止循环展开。这可能会减少代码大小(并改善代码局部性,这对于指令或微码缓存或循环缓冲区很有用,并允许 CPU 快速获取/解码指令)。
  2. 增加计数器可能会增加某些函数的大小并阻止内联。这也可以减少代码大小。
  3. 增加计数器可能会阻止自动矢量化,这又可能会减小代码大小。

即使此更改不会影响编译器优化,它也可能会改变 CPU 执行代码的方式。

  1. 如果您在适当的位置插入反增量代码,充满分支目标,这可能会使分支目标的密度降低并改善分支预测。
  2. 如果您在某个特定的分支目标之前插入计数器递增代码,这可能会使分支目标的地址更好地对齐,并使代码获取更快。
  3. 如果您在写入某些数据之后但再次加载相同数据之前放置计数器递增代码(并且由于某种原因存储到加载转发不起作用),加载操作可能会更早完成。
  4. 插入计数器递增代码可以防止两次冲突的加载尝试到 L1 数据高速缓存中的同一组。
  5. 计数器递增代码的插入可能会改变一些 CPU 调度程序的决定,并使一些执行端口及时用于一些性能关键的指令。

要研究编译器优化的效果,您可以比较在添加反递增代码之前和之后生成的汇编代码。

要调查 CPU 影响,请使用允许检查处理器性能计数器的分析器。

于 2012-10-03T13:06:50.890 回答
1

根据我对嵌入式编译器的经验猜测,编译器中的优化工具会寻找递归任务。也许额外的代码迫使编译器看到更多递归的东西,并且它以不同的方式构造了机器代码。编译器为优化做了一些奇怪的事情。在某些语言中(我认为是 Perl?),“不是”条件比“真”条件执行得更快。您的调试工具是否允许您单步执行代码/程序集比较?这可以增加一些关于编译器决定如何处理额外任务的见解。

于 2012-10-03T11:39:46.720 回答