对于调用 glibc 的 sin() 的函数,我应该信任 callgrind 内部还是外部的分析?

Should I trust profiling inside or outside of callgrind for a function that calls glibc's sin()?

我正在开发一个音频库,其中需要在非常紧凑的循环中计算数字的正弦。根据用户的目标和环境,结果中的各种程度的不准确性可能是可以容忍的,因此我提供了在几个具有不同精度和速度特性的正弦近似值之间进行选择的能力。其中一个显示在 callgrind 下 运行 时比 glibc 的 sin() 快 ~31%,但如果库是使用 -O3 编译的,则在 运行 之外时慢 2%如果使用 -Ofast 编译,速度会慢 25%。在设计库的界面方面,我应该相信 callgrind 还是“本地”结果?

我的直觉是不相信 callgrind 并接受挂钟结果,因为无论如何这才是最终真正重要的。但是,我担心我所看到的是由我的处理器 (i7-7700k)、编译器 (gcc 10.2.0) 或我的环境的其他方面(Arch Linux、内核 v5.1)的某些特殊情况引起的。 9.13) 可能不会转移给其他用户。 callgrind 是否有可能向我展示一些“大体​​上正确”的信息,即使它对我来说并不完全正确?

库内正弦实现的相对性能差异在 callgrind 内外保持不变;只有 glibc sin() 的表观性能不同。这些模式适用于可变工作量和重复运行。有趣的是,使用 -O1 时,相对性能差异在 callgrind 内部和外部具有可比性,但使用 -O0-O2-O3-Ofast 时则不然。

glibc 的 sin() 的输入在很多方面都是一个很好的例子:它是一个 double,它总是 <= 2π,并且永远不会是次正规的、NaN 或无限的。这让我想知道 glibc sin() 是否有时会调用我的 CPU 的 fsin 指令,因为英特尔的文档说它对于参数 < ~3π/4 是相当准确的(见Intel 64 and IA-32 Architectures Developer's Manual: Vol. 1,第 8-22 页)。如果是这种情况,Valgrind VM 的行为似乎可能对该指令具有明显不同的性能特征,因为理论上在开发过程中可能比更频繁使用的指令更少关注它。但是,我已经阅读了 glibc 中 sin() 的当前 Linux x86-64 实现的 C 源代码,我不记得那样的事情,我也没有在 callgrind 反汇编中看到它(它似乎是使用通用 AVX 指令“手动”完成其工作)。我听说 glibc 曾在 fsin 年前使用过,但我的理解是,由于其准确性问题,他们停止使用了。

我发现的唯一一个讨论与我所看到的内容一致的地方是 an old thread on the GCC mailing list,但是尽管浏览起来很有趣,但我没有注意到那里有任何澄清这一点的东西(而且我会小心翼翼地从 2012 年开始以面值获取信息。

当您 运行 在 Callgrind 或 Valgrind 家族的任何其他工具下运行程序时,它会被即时反汇编。然后对中间表示进行检测,并将其转换回本机指令集。

Callgrind 和 Cachegrind 为您提供的分析数据是他们正在建模的简化处理器的数据。由于他们没有现代 CPU 流水线的详细模型,他们的结果将无法准确反映实际性能的差异(他们可以捕获对“此函数执行的指令比其他函数多 3 倍”的顺序的影响函数”,而不是“这个指令序列可以用更高的指令级并行执行”)。

在循环中计算类似于 sin 的函数的最重要的事情之一是允许对计算进行矢量化:在 x86 上,SSE2 为 double 提供 2 倍的矢量化因子,为 [=12 提供 4 倍的矢量化因子=].如果你有可内联的无分支近似函数,编译器可以更容易地实现这一点,尽管也有足够新的 Glibc 和 GCC 存在的可能性(但你需要将 -ffast-math 标志的一个大子集传递给 GCC 来实现它)。

如果您还没有看到它:Arm 的 optimized-routines repository 有许多函数的现代向量化实现,包括单精度和双精度的 sin/cos。

P.S。 sin 不应该 returns 对于一个很小但非零的参数的零结果。当 x 接近于零时,sin(x)x 相差小于 x*x*x,因此当您接近零时,x 成为最接近 的可表示数sin x.