可预测地分析单个函数

时间:2016-11-24 16:15:42

标签: c gcc optimization profiling

我需要一种更好的数字代码分析方法。假设我在64位x86上使用Cygwin中的GCC并且我不打算购买商业工具。

情况就是这样。我在一个线程中运行一个函数。除了内存访问之外,没有代码依赖或I / O,可能除了链接的数学库之外。但是在大多数情况下,它都是表查找,索引计算和数值处理。我已经在堆和堆栈上缓存对齐的所有数组。由于算法的复杂性,循环展开和长宏,汇编列表可能会变得非常冗长 - 数千条指令。

我一直在使用matlab中的tic / toc定时器,bash shell中的time实用程序,或者直接在函数周围使用时间戳计数器(rdtsc)。问题是这样的:时间的方差(可能多达运行时间的20%)大于我所做的改进的大小,所以我没办法知道改变后代码是好还是坏。你可能认为现在是时候放弃了。但我不同意。如果您坚持不懈,许多渐进式改进可能会导致性能提高两到三倍。

我多次遇到的一个特别令人抓狂的问题是我做了一个改变,表现似乎始终提高了20%。第二天,收益就会丢失。现在我可能做出了我认为对代码无害的改变,然后完全忘记了它。但是我想知道是否可能发生其他事情。就像我认为GCC不会产生100%确定性输出一样。或者它可能更简单,就像操作系统将我的过程转移到更繁忙的核心。

我考虑过以下几点,但我不知道这些想法是否可行或有意义。如果是,我想要明确说明如何实施解决方案。 目标是最小化运行时的方差,以便有意义地比较不同版本的优化代码。

  • 专注于处理器的核心,只运行我的日常工作。
  • 直接控制缓存(加载或清除缓存)。
  • 确保我的dll或可执行文件始终加载到内存中的相同位置。我的想法是,缓存的集合关联性可能与RAM中的代码/数据位置相互作用,以改变每次运行的性能。
  • 某种循环精确仿真器工具(非商业用途)。
  • 是否可以对上下文切换进行一定程度的控制?或者甚至重要吗?我的想法是上下文切换导致可变性的时机,可能是通过在不合适的时间刷新管道。

过去,我通过计算汇编列表中的指令,在RISC架构上取得了成功。当然,这仅在指令数量很小时才有效。一些编译器(如TI的C67x代码编写器)将为您详细分析如何保持ALU忙碌。

我还没有发现GCC / GAS制作的装配清单特别有用。通过全面优化,代码可以遍布各处。对于汇编列表中分散的单个代码块,可以有多个位置指令。此外,即使我能理解组件如何映射回原始代码,我也不确定现代x86机器上的指令数和性能之间是否存在很大的相关性。

我尝试使用gcov进行逐行分析,但由于我构建的GCC版本与MinGW编译器不兼容,因此无法正常工作。

你可以做的最后一件事是平均多次试运行,但这需要永远。

编辑(RE:调用堆栈采样)

我的第一个问题是,实际上,我该怎么做?在其中一个power point幻灯片中,您展示了使用Visual Studio暂停程序。我所拥有的是由GCC编译的DLL,在Cygwin中进行了全面优化。然后由Matlab使用VS2013编译器编译的mex DLL调用它。

我使用Matlab的原因是因为我可以轻松地尝试不同的参数并可视化结果,而无需编写或编译任何低级代码。此外,我可以将我的优化DLL与高级Matlab代码进行比较,以确保我的优化没有破坏任何东西。

我使用GCC的原因是我对它的经验比使用Microsoft的编译器要多得多。我熟悉许多标志和扩展。此外,微软至少在过去一直不愿意维护和更新本机C编译器(C99)。最后,我已经看到GCC从商业编译器中解脱出来了,我已经查看了汇编列表,看看它是如何实际完成的。所以我对编译器实际上认为的方式有一些直觉。

现在,关于猜测要解决的问题。这不是真正的问题;它更像是猜测如何来修复它。在这个例子中,与数值算法中的情况一样,实际上没有I / O(不包括存储器)。没有函数调用。几乎没有任何抽象。就像我坐在一片撒拉包裹上面一样。我可以在下面看到计算机体系结构,而且它们之间什么也没有。如果我重新卷起所有的循环,我可能会在大约一页左右的时间内完成代码,我几乎可以计算得到的汇编指令。然后我可以粗略地比较一个核心能够做的理论操作数量,看看我有多接近最优。麻烦的是我失去了从展开中获得的自动矢量化和指令级并行化。展开后,装配清单太长,无法以这种方式进行分析。

关键是这段代码真的不多。但是,由于编译器和现代计算机体系结构的复杂性令人难以置信,即使在这个级别上也有相当多的优化。但我不知道小变化将如何影响编译代码的输出。让我举几个例子。

这第一个有些模糊,但我确定我已经看过几次。你做了一个小小的改变并获得了10%的改进。你做了另一个小改动,又获得了10%的改善。您撤消第一个更改并获得另外10%的改进。咦?编译器优化既不是线性的,也不是单调的。这是可能的,第二次更改需要一个额外的寄存器,它通过强制编译器改变其寄存器分配算法来打破第一次更改。也许,第二次优化以某种方式阻碍了编译器进行优化的能力,这种优化是通过撤消第一次优化来解决的。谁知道。除非编译器内省足以在每个抽象层次上转储其​​完整分析,否则你永远不会真正知道最终汇编的结果。

这是最近发生在我身上的一个更具体的例子。我手动编码AVX内在函数来加速过滤操作。我以为我可以展开外部循环以增加指令级并行性。所以我做了,结果是代码慢了两倍。发生的事情是没有足够的256位寄存器可供使用。因此编译器暂时将结果保存在堆栈上,从而导致性能下降。

正如我在this post所暗示的那样,你评论过它,最好告诉编译器你想要什么,但不幸的是,你经常别无选择并被迫进行调整优化,通常通过猜测和检查。

所以我想我的问题是,在这些情况下(代码在展开之前实际上很小,每次增量性能变化都很小,并且你在非常低的抽象级别工作),会不会更好具有"定时精度"或者是告诉我哪个代码优越更好地调用堆栈采样?

2 个答案:

答案 0 :(得分:2)

前段时间我遇到过类似的问题,但那是在Linux上,这使得调整更容易。基本上OS引入的噪声(称为“OS抖动”)在SPEC2000测试中大到5-10%(我可以想象,由于更大量的英国媒体报道,它在Windows上要高得多)。

我能够通过以下方式将偏差降至1%以下:

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

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

如果你需要,我可能会在今天晚些时候向github提供详细的说明。

- 编辑 -

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

答案 1 :(得分:1)

我是对的,你正在做的是做出有根据的猜测,修复它,修复它,然后尝试测量它是否有任何区别?

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

我不用担心。 如果有办法加速程序,这将确定它。 而且问题通常不仅仅是一个简单的陈述,比如&#34;行或例程X花费Y%的时间&#34;,但是&#34;它在某些情况下做Z的原因是&#34;&#34; ;而实际修复可能在其他地方。 在修复之后,可以再次完成该过程,因为之前较小的不同问题现在更大(以百分比表示,因为通过修复第一个问题减少了总数)。 重复是关键,因为每个加速因子都会倍增前一个,就像复利一样。

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

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