【问题标题】:Profiling a Single Function Predictably可预测地分析单个函数
【发布时间】: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_SETsched_setaffinity
  • 我在一家主要在 Windows 世界中运营的商业公司工作。这些选项在 Windows 上是否可用?
  • 似乎有适用于 Windows 的 Qcachegrind 但是 sched_setaffinity 是一个特定于 linux 的调用。

标签: c gcc optimization profiling


【解决方案1】:

前段时间我也遇到过类似的问题,但那是在 Linux 上,它更容易调整。基本上,操作系统引入的噪音(称为“操作系统抖动”)在 SPEC2000 测试中高达 5-10%(我可以想象在 Windows 上它会更高,因为有更多的膨胀软件)。

通过以下组合,我能够将偏差降低到 1% 以下:

  • 禁用动态频率缩放(最好在 BIOS 和 Linux 内核中执行此操作,因为并非所有内核版本都能可靠地执行此操作)
  • 禁用内存预取和其他花哨的设置,如“Turbo Boost”等(BIOS 再次)
  • 禁用超线程
  • 在内核中启用高性能进程调度程序
  • 将进程绑定到内核以防止线程迁移(使用内核 0 - 出于某种原因,它在我的内核上更可靠,如图)
  • 引导至单用户模式(其中不运行任何服务) - 这在基于 systemd 的现代发行版中并不容易
  • 禁用 ASLR
  • 禁用网络
  • 删除操作系统页面缓存

可能还有更多,但 1% 的噪音对我来说已经足够了。

如果您需要,我可能会在今天晚些时候将详细说明发送到 github。

-- 编辑--

我已经发布了我的基准测试脚本和说明here

【讨论】:

  • 这是一个有用的答案,尽管我希望有一个面向 Windows 的解决方案。如果没有其他人做出重大贡献,我会接受这个作为我的答案。
  • 是的,这并不能完全回答您的问题,但至少列出了性能抖动的最常见原因。很抱歉这么说,但 Windows 可能更难驯服(如果有的话)。
【解决方案2】:

您所做的是对要修复的问题进行有根据的猜测,修复它,然后尝试衡量它是否有任何不同,我说得对吗?

我采用不同的方式,当代码变大时效果特别好。 而不是猜测(我当然可以),我让程序告诉我时间是如何度过的,使用this method。 如果该方法告诉我大约 30% 用于做某事,我可以集中精力寻找更好的方法来做到这一点。 然后我可以运行它并计时。 我不需要太多的精确度。 如果它更好,那就太好了。 如果情况更糟,我可以撤消更改。 如果大致相同,我可以说“哦,好吧,也许它并没有节省多少,但让我们再做一遍,找到另一个问题,”

我不必担心。 如果有办法加速程序,这将查明它。 通常问题不仅仅是一个简单的陈述,比如“行或例程 X 花费了 Y% 的时间”,而是“在某些情况下这样做的原因是 Z”,而实际的修复可能在其他地方。 修复它之后,可以再次执行该过程,因为以前较小的不同问题现在更大(以百分比表示,因为通过修复第一个问题减少了总数)。 重复是关键,因为每个加速因素都会与之前的所有因素相乘,例如复利。

当程序不再指出我可以解决的问题时,我可以确定它几乎是最佳的,或者至少没有其他人可能会击败它。

在这个过程中,我不需要非常精确地测量时间。 之后,如果我想在 powerpoint 中吹嘘它,也许我会做多次计时来获得更小的标准误差,但即便如此,人们真正关心的是整体加速因素,而不是精度。

【讨论】:

  • 我将在有精力尝试理解后立即阅读全文。我还很无知,但是如果不控制大多数非确定性来源以使“操作系统抖动”小于更改的大小(正如我提到的),似乎不可能确定修复是更好还是更差) 或大量平均(这很耗时)。
  • @Todd:如果您不熟悉它,这可能看起来很奇怪,但所有支持here 并非偶然。而且,虽然每个程序都不同,但我喜欢向this example 展示加速比为 730 倍的地方。有两种精度:计时精度(不太确定什么需要时间)和诊断精度(所花费的时间是什么,但如果或多或少不并不会真正影响您对此的处理方式)。
  • 我对这个很感兴趣,如果你写一本书,我可能有一天会读到它。如果一些概率论是从第一原理中定义的,并且有很多很好的例子,那么它将会非常有助于理解它。但是,我不相信这种技术适用于我的问题。手头的问题实际上是我如何加快速度,而不是问题出在哪里。我要编辑我的帖子;那么你可以决定这是否有意义。
  • 我刚刚在一个同事用c#编写的不相关程序上使用了你的采样方法。手动采样在调用外部实用程序时停止了 80%。 VS2013 采样分析器告诉我,大约 90% 的时间被用于与大量字符串操作相关的一些内部例程。我不知道当你在 VS 中点击暂停时会发生什么。使用手动采样时,对外部程序的调用是否可能强烈偏向程序在何处/何时停止?我应该相信哪个?
  • @Todd:不幸的是,基于采样的分析器的一个特性(除了随机暂停和可能的缩放)是它们对 I/O 或其他系统等待视而不见。 没有充分的理由。这只是一个历史意外,因为第一个采样分析器对指令指针进行了采样,这在 I/O 期间是没有意义的。再加上精确计时可以帮助你发现问题的奇怪想法,导致其他人效仿。 Here are the issues.长话短说,采样VS不会显示问题。
猜你喜欢
  • 2021-07-26
  • 1970-01-01
  • 1970-01-01
  • 2011-03-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-09-08
相关资源
最近更新 更多