我有两个不同的程序来解决用 C++ 编写的数学问题(程序 A
和 B
)。
A
的性能大约比 B
好 10 倍(在持续时间方面)。现在我通过 valgrind 工具 callgrind 计算了执行的 CPU 指令,发现程序 A
只需要程序 B
执行的指令的 1/3。我本以为该系数约为 1/10。
我知道有些 CPU 指令需要更多的 CPU 周期(如内存访问),但根据设计 A
应该比 B
包含更多这种昂贵的指令。
我也不知道 callgrind 如何计算此类指令(在文档中找不到任何相关信息)。
任何人都可以对这种行为给出合理的解释吗?时间差
编辑:(由于评论)
不幸的是,代码太全面了,不能在这里发布……这两个程序都在同一台机器上执行。两者都是完全并行化的(每个线程运行一个独立的程序拷贝,只需要在找到解决方案时告诉其他线程)。但是指令计数是在一个线程上完成的,因为 callgrind 无论如何都会对程序进行排序。如前所述,A
比B
需要更多的内存。
我不期待一个正确的答案,只是给我一些可能导致该问题的提示。
您也无需指定运行它的平台,每个 CPU 都有自己的一套注意事项。
例如在 x86 上,指令数量和总执行时间之间的相关性很小,如下所示:
- x86 首先将指令翻译成内部微操作码 (uops) 然后运行这些指令,因此它基本上执行与可执行文件中人类可见的机器代码完全不同的代码。它还对 uops 进行了重新排序,因此即使您模拟了翻译,您也无法确定它们的执行顺序(除非您模拟 CPU、缓存和内存的整个架构)。
- 多个 uops 可以在单个时钟滴答中执行,或者以其他方式,单个 uop 可能会阻塞 CPU 几个时钟滴答,因此任何两个 x86 指令的执行时间可以相差 100+ 倍(即使是相同的指令在如果两次运行因缓存未命中而停止,则执行时间可能会有很大差异(~100 倍)。
- 内存访问。内存比 CPU 慢得多,因此以可预测的模式顺序读取内存并使数据尽可能紧凑(以完全重用缓存)与代码速度比指令(算法)数量更相关。在某些极端情况下,设计良好的数据结构甚至可以击败更好的算法,例如
std::vector<int>
对于 ~100k 项目,随机插入/删除比列表快得多,尽管 vector 插入/删除是 O(n^2),而列表仅约为 O(n)。 CPU 手所能及的唯一内存是 L0 缓存,L1 就像在街上,L2 就像去其他城市旅行,L3 不同的国家。内存本身几乎就像在月球上。
- 其他 I/O 访问.. 如果内存很慢,那么访问磁盘就像去太阳 (SSD) 或超越太阳系 (HDD)。
如您所见,在极端情况下,x86 CPU 甚至可以执行具有约 200 条指令的代码 + 处理 30 倍以上内存的速度与具有约 10 条指令的不同例程一样快。在极端情况下,差异可能非常极端,人类很难通过阅读源代码来想象。这就是为什么在 x86 上证明代码优化合理的唯一有效方法是使用足够接近真实数据的数据来分析代码。仅仅根据理论、大 O 表示法和“直觉”进行“优化”很容易适得其反,这在 10-20 年前是有效的,即使在那时我们也使用工具分析结果以验证 yield 。
当然,大量的指令本身更容易从缓存中消失,导致指令读取停滞,但如果您可以创建更好的数据结构,那么即使是 30kiB vs 1kiB 的代码也可以合理(尽管它有很多风险被操作系统中断时的缓存未命中数)。
callgrind 网站说:“可选地,缓存模拟和/或分支预测(类似于 Cachegrind)可以生成有关应用程序运行时行为的更多信息。”,因此您可以做得更好数据多于指令数,看看是否存在瓶颈,代码/数据的重组会减少停顿。