编译的 c++ 代码使用 double 比 float 运行得快得多。解释?

计算科学 线性代数 C++ 编译 矢量化
2021-12-15 09:45:29

我在这里还是很新,我希望这个问题适合这个论坛,否则请帮助我将它迁移到更绿色的牧场。


我是一名电气工程师,专门研究应用数学来构建很酷的算法。有时在我的工作中,我尝试编写代码来帮助编译器生成运行速度与硬件允许的一样快的代码。前几天,我意识到当我将类型从 32 位浮点数更改为 64 位双精度并编译我的代码时......它运行更快。

现代 CPU 架构 SIMD 指令和 CPU 中的其他怪癖是否真的比单精度浮点运算更优化双精度?

对我来说这似乎是真的,至少我的本地机器是这样。我对此感到惊讶,因为我认为您可以在相同的逻辑上容纳两倍的浮点数。也许是因为工业需求,人们普遍使用双精度而不是浮点数,这使得他们专注于对双精度算术的良好支持?

或者,如果我错了,这不是 CPU 设计的优先级,也许是编译器的优先级?


这是一些最小的 godbolt 示例,其中包含 Nx8 矩阵的矩阵向量乘法https://godbolt.org/z/gPR9wj

例如,看看 clang 9.1.0 和 gcc 8.1 如何产生非常不同的输出 wrt 代码大小对我来说很有趣。

不过,我无法得出关于 float 与 double 的太多结论。(我们可以根据需要将 scal typedef 更改为 float 或 double。)

2个回答

我查看了您的简单代码示例,我怀疑您观察到的速度损失是由于 C 传统要求使用中间双精度运算来评估右侧表达式,即使源变量是单一的精度和输出位置(左侧目的地)是单精度。

这样做的目的是避免中间计算中的准确性损失,但如果程序员没有意识到这一点,只需像您所做的那样更改类型将导致在每个源变量上从 float 到 double 以及在存储左评估结果。

在您的矩阵乘法代码中,浮点计算显示为嵌套循环中的一行,因此四个生成的类型转换可能很容易与双精度“中间”值的一次乘法和一次加法的时间成本进行比较。

FLT_EVAL_METHOD自 C++ 11 以来,头文件中有一个标准的“定义” ,<cfloat>它公开了一个依赖于实现的参数,以反映中间表达式评估所需的额外精度(如果有的话) 。乍一看,人们可能希望FLT_EVAL_METHOD0 意味着不会进行不必要的转换或提升,但仔细阅读会发现,总是允许“计算就像所有中间结果具有无限范围和精度一样”。出于这个原因,我不会将该参数视为降低精度的保证。

最好的办法是查看生成的汇编语言文件,了解编译器插入浮点转换的频率。稍后我将尝试使用您的示例代码来说明这一点。

有多种可能的解释:

  • 由于floats 和doubles 的计算次数不同,例如,迭代次数/函数评估次数(或其他原因,正如@Richard 在评论中指出的那样)
  • 有一些类型转换/实现(比如模板化代码)对于float类型不是最优的,而不是doubles。我曾经在过度专业化模板实现时遇到过这个问题。
  • C++ 标准库可以根据类型特征为标准算法选择不同的路径。我遇到的一个最近的例子:排序算法double和我的自定义实现complex<double>,由于我有一个非平凡的构造函数,快速排序算法切换到合并排序的方式比“简单数据类型”要晚。但是,此类事情通常会受益于较小的 32 位数据类型。

SIMD 不太可能double比 for更有效float,除非您在一些具有异国情调的编译器的奇异架构上运行。相反,现在的趋势是针对单精度甚至半精度(深度学习的影响)进行优化,而不是doublequad

要尝试的事情:

  • 检查我在上面这个答案中触及的明显项目
  • 最小的可重现示例(正如@Richard 指出的那样)
  • Godbolt在最小可重现示例上查看发生了什么