【发布时间】:2017-04-09 00:33:33
【问题描述】:
我需要一种更好的方法来分析数字代码。假设我在 64 位 x86 上的 Cygwin 中使用 GCC,并且我不打算购买商业工具。
情况是这样的。我有一个函数在一个线程中运行。除了内存访问之外,没有代码依赖或 I/O,可能除了一些链接的数学库。但在大多数情况下,它都是表查找、索引计算和数值处理。我已经缓存了堆和堆栈上的所有数组。由于算法、循环展开和长宏的复杂性,汇编列表可能会变得相当冗长——数千条指令。
我一直在使用 Matlab 中的 tic/toc 计时器、bash shell 中的时间实用程序,或者直接在函数周围使用时间戳计数器 (rdtsc)。问题是这样的:时间的差异(可能高达运行时间的 20%)大于我正在进行的改进的大小,所以我无法知道如果代码在更改后更好或更差。你可能会认为是时候放弃了。但我不同意。如果您坚持不懈,许多增量改进可以使性能提高两到三倍。
我曾多次遇到的一个特别令人抓狂的问题是,我进行了更改,并且性能似乎持续提高了 20%。第二天,收益就消失了。现在有可能我对代码进行了我认为无害的更改,然后完全忘记了它。但我想知道是否可能发生其他事情。就像我相信的那样,GCC 可能不会产生 100% 的确定性输出。或者可能是更简单的事情,比如操作系统将我的进程转移到更繁忙的核心。
我考虑过以下几点,但我不知道这些想法是否可行或是否有意义。如果是,我想明确说明如何实施解决方案。 目标是最小化运行时的差异,以便我可以有意义地比较不同版本的优化代码。
- 专门为我的处理器的一个内核运行我的例程。
- 直接控制缓存(加载或清除)。
- 确保我的 dll 或可执行文件始终加载到内存中的同一位置。我的想法是,缓存的集合关联性可能会与 RAM 中的代码/数据位置交互,从而改变每次运行的性能。
- 某种周期精确的仿真器工具(非商业)。
- 是否可以对上下文切换进行一定程度的控制?或者它甚至重要吗?我的想法是上下文切换的时间会导致可变性,可能是通过导致管道在不合时宜的时间被刷新。
过去,我通过计算汇编列表中的指令在 RISC 架构上取得了成功。当然,这只适用于指令数量较少的情况。一些编译器(例如 TI 的 C67x 代码编辑器)会详细分析它是如何让 ALU 保持忙碌的。
我没有发现 GCC/GAS 生成的组装清单特别有用。完全优化后,代码会到处移动。对于分散在程序集列表中的单个代码块,可以有多个位置指令。此外,即使我能够理解程序集如何映射回我的原始代码,我也不确定现代 x86 机器上的指令数和性能之间是否存在很大的相关性。
我尝试使用 gcov 进行逐行分析,但由于我构建的 GCC 版本与 MinGW 编译器不兼容,它无法正常工作。
您可以做的最后一件事是多次试运行的平均值,但这需要很长时间。
编辑(RE:调用堆栈采样)
实际上,我的第一个问题是,我该怎么做?在您的一张幻灯片中,您展示了使用 Visual Studio 暂停程序。我拥有的是一个由 GCC 编译的 DLL,在 Cygwin 中进行了全面优化。然后由 Matlab 使用 VS2013 编译器编译的 mex DLL 调用。
我使用 Matlab 的原因是因为我可以轻松地试验不同的参数并将结果可视化,而无需编写或编译任何低级代码。此外,我可以将我优化的 DLL 与高级 Matlab 代码进行比较,以确保我的优化没有破坏任何东西。
我使用 GCC 的原因是我使用它的经验比使用 Microsoft 的编译器要多得多。我熟悉许多标志和扩展。此外,至少在过去,Microsoft 一直不愿意维护和更新本机 C 编译器 (C99)。最后,我看到 GCC 从商业编译器中脱颖而出,我查看了汇编列表以了解它是如何实际完成的。所以我对编译器实际上是如何思考有一些直觉。
现在,关于猜测要解决的问题。这不是真正的问题。这更像是猜测 如何 修复它。在这个例子中,就像数值算法中经常出现的情况一样,实际上没有 I/O(不包括内存)。没有函数调用。几乎没有抽象。就像我坐在一块保鲜膜上。我可以看到下面的计算机架构,中间没有任何东西。如果我重新卷起所有循环,我可能可以将代码放在大约一页左右,并且我几乎可以计算得到的汇编指令。然后我可以粗略地比较单个核心能够执行的理论操作数,看看我有多接近最优。那么问题是我失去了从展开中获得的自动矢量化和指令级并行化。展开,汇编清单太长,无法用这种方式分析。
关键是这段代码实际上并没有太多内容。然而,由于编译器和现代计算机体系结构的难以置信的复杂性,即使在这个级别上也需要进行相当多的优化。但我不知道微小的变化会对编译代码的输出产生多大的影响。让我举几个例子。
第一个有点含糊,但我相信我已经见过几次了。你做了一个小的改变,得到了 10% 的改进。你又做了一个小改动,又得到了 10% 的改进。您撤消第一个更改并获得另外 10% 的改进。嗯?编译器优化既不是线性的,也不是单调的。有可能,第二个更改需要一个额外的寄存器,这通过强制编译器更改其寄存器分配算法来破坏第一个更改。也许,第二次优化以某种方式阻止了编译器进行优化的能力,这是通过撤消第一次优化来修复的。谁知道。除非编译器足够内省,可以在每个抽象级别转储其完整分析,否则您永远不会真正知道最终程序集是如何结束的。
这是一个最近发生在我身上的更具体的例子。我正在手动编码 AVX 内在函数以加快过滤器操作。我想我可以展开外循环以增加指令级并行性。所以我做了,结果是代码慢了一倍。发生的事情是没有足够的 256 位寄存器可供使用。所以编译器暂时将结果保存在堆栈上,这会降低性能。
正如我在this post 中提到的那样,你评论过,最好告诉编译器你想要什么,但不幸的是,你经常别无选择,不得不手动调整优化,通常是通过猜测和检查。
所以我想我的问题是,在这些场景中(代码在展开之前实际上很小,每次增量性能变化都很小,而且您的工作抽象级别非常低),最好有“计时精度”还是调用堆栈采样更能告诉我哪个代码更好?
【问题讨论】:
-
你可以看看 kcachegrind 来分析,尤其是缓存访问。 (显示缓存未命中)。
-
要将进程附加到 CPU,您有
CPU_SET和sched_setaffinity -
我在一家主要在 Windows 世界中运营的商业公司工作。这些选项在 Windows 上是否可用?
-
似乎有适用于 Windows 的 Qcachegrind 但是
sched_setaffinity是一个特定于 linux 的调用。
标签: c gcc optimization profiling