【问题标题】:How expensive is it to convert between int and double?在 int 和 double 之间转换有多贵?
【发布时间】:2015-04-24 10:56:23
【问题描述】:

我经常看到将整数转换为双精度整数再转换为双精度整数的代码(有时是有充分理由的,有时不是),我突然想到这似乎是我程序中的“隐藏”成本。我们假设转换方法是截断。

那么,它到底有多贵?我确信它会因硬件而异,所以让我们假设一个新的英特尔处理器(Haswell,如果你愿意,尽管我会接受任何东西)。一些我会感兴趣的指标(虽然一个好的答案不需要全部):

  1. 生成的指令数
  2. 使用的周期数
  3. 与基本算术运算相比的相对成本

我还假设,考虑到我们每秒可以执行的计算量与数据量之间的差异,我们最能感受到转换速度慢的影响的方式是功耗而不是执行速度实际上可以每秒到达CPU。

【问题讨论】:

标签: c++ x86 c++-cli x86-64 micro-optimization


【解决方案1】:

以下是我自己可以挖掘的,对于 x86-64 使用 SSE2 进行 FP 数学(不是旧版 x87,其中更改 C++ 截断语义的舍入模式很昂贵):

  1. 当我从 clang 和 gcc 中 take a look at the generated assembly 时,它看起来就像将 int 转换为 double,它归结为一条指令:cvttsd2si

    doubleintcvtsi2sd。 (cvtsi2sdlcvtsi2sd 的 AT&T 语法,具有 32 位操作数大小。)

    通过自动矢量化,我们得到cvtdq2pd

    所以我想问题变成了:那些的成本是多少?

  2. 这些指令的成本与 FP addsd 加上 movq xmm, r64 (fp movq r64, xmm (integer

    Intel® 64 and IA-32 Architectures Optimization Reference Manual 表示cvttsd2si 指令的成本是 5 延迟(参见附录 C-16)。 cvtsi2sd,取决于您的架构,延迟从 Silvermont 上的 1 到其他几个架构上的 7-16 不等。

    Agner Fog's instruction tables 具有更准确/合理的数字,例如 Silvermont 上 cvtsi2sd 的 5 周期延迟(每 2 个时钟吞吐量 1 个),或 Haswell 上的 4c 延迟,每时钟吞吐量一个(如果您避免依赖在目标寄存器上与旧的上半部分合并,就像 gcc 通常与 pxor xmm0,xmm0 一样。

    SIMD 打包-float 到打包-int 很棒;单片机。但转换为double 需要随机播放来更改元素大小。 SIMD float/doubleint64_t 直到 AVX512 才存在,但可以在有限范围内手动完成。

    英特尔的手册将延迟定义为:“执行内核完成执行构成指令的所有微操作所需的时钟周期数。”但更有用的定义是从输入就绪到输出就绪的时钟数。如果有足够的并行性让乱序执行来完成其工作,吞吐量比延迟更重要:What considerations go into predicting latency for operations on modern superscalar processors and how can I calculate them by hand?

  3. 同样的英特尔手册说整数 add 指令花费 1 延迟,整数 imul 花费 3(附录 C-27)。 FP addsdmulsd 在 Skylake 上以每时钟 2 个吞吐量运行,具有 4 个周期延迟。 SIMD 版本和 FMA 版本相同,具有 128 位或 256 位向量。

    在 Haswell 上,addsd / addpd 每时钟吞吐量只有 1 个,但由于专用的 FP-add 单元,延迟为 3 个周期。

所以,答案归结为:

1) 它经过硬件优化,编译器利用硬件机制。

2) 就一个方向的周期数而言,它的成本仅比乘法多一点,而另一个方向的数量变化很大(取决于您的架构)。它的成本既不是免费的,也不是荒谬的,但考虑到编写代码的容易程度并以一种不明显的方式产生成本,它可能值得更多关注。

【讨论】:

  • 为了清楚起见:Agner Fog 的超棒手册“指令表”报告称,在 Haswell 上,integer register-register add 的延迟 = 1,倒数吞吐量 = 0.25;整数寄存器寄存器mul/imull 64x64 位有 lat = 3, 1/thru = 1, 浮点寄存器寄存器addss/ps/sd/pd 有 lat = 3, 1/thru = 1, 浮点寄存器寄存器mulss/ps/sd/pd lat = 5, 1/thru = 0.5,32 位和 64 位整数和浮点值之间的各种cvt* 转换大部分具有 lat = 3-4 和 1/thru = 1。
  • @IwillnotexistIdonotexist - 彻底:)。非常感谢!
  • 是的,float/double int 转换的成本与 FP 添加的成本差不多,而且实际上在相同的执行单元上运行。 (agner.org/optimize)。 SIMD floatint 是有效的,但是 SIMD doubleint 也需要一个 shuffle uop,并且 SIMD float/doubleint64_t 直到 AVX512 才存在。
  • 最终进行了重大编辑以更正此答案。当你写这篇文章时,你在挖掘手册的基础上得出的图片中有一些严重的空白。我也许应该写下我自己的答案,所以如果你想回滚,请告诉我;我可以将我写的内容放在单独的答案中。
  • 感谢所有相关人员的精彩回答。这非常有用!
【解决方案2】:

当然,这类问题取决于具体的硬件,甚至取决于模式。

x86 我的 i7 在 32 位模式下使用时 使用默认选项 (gcc -m32 -O3) 从 intdouble 的转换非常快,反之则慢得多,因为 C 标准规定了一个荒谬的规则(小数截断)。

这种舍入方式对数学和硬件都不利,需要 FPU 切换到这种特殊的舍入模式,执行截断,然后切换回合理的舍入方式。

如果您需要快速进行 float->int 转换,使用简单的 fistp 指令会更快,并且计算结果也更好,但需要一些内联汇编。

inline int my_int(double x)
{
  int r;
  asm ("fldl %1\n"
       "fistpl %0\n"
       :"=m"(r)
       :"m"(x));
  return r;
}

比简单的x = (int)y; 转换快 6 倍以上(并且不偏向于 0)。

同样的处理器,在 64 位模式下使用时没有速度问题,使用 fistp 代码实际上会使代码运行速度稍慢。

显然硬件人员放弃并直接在硬件中实现了糟糕的舍入算法(所以糟糕的舍入代码现在可以快速运行)。

【讨论】:

  • 您在什么平台上得出的结论是它快了 6 倍?一两年前,我遇到了一个类似的问题,有人问为什么你的答案中的代码更好,我的第一反应是“你怎么知道它更好”,事实证明,如果你有一个支持 SSE 的处理器(因此对于 x86,自 2000 年左右引入的东西),那么不使用这个技巧会更快,而只是让编译器生成“正确的”指令。我看看能不能找到我的答案,但现在必须上班,所以稍后再做。
  • @MatsPetersson:这是在 i7 上测试但编译 -m32,编译 64 位代码时不存在问题(实际上使用幼稚转换更快)。
  • 如果你使用-m32 -msse2会怎样?
  • @MatsPetersson:您需要使用-m32 -msse2 -mfpmath=sse 才能真正使用 SSE2 进行标量FP 数学运算。或-m32 -msse3fisttp (truncating conversion without changing the rounding mode)。当然,针对 x86-64 的 gcc 默认为 -mfpmath=sse,但 32 位仍默认为 x87,主要仅在自动矢量化时使用 SSE,IIRC。
  • 如果您能找到编译器与fistpcvtsd2si 完全内联的函数,例如(int)rint(x)lrint(x),则可以使用四舍五入转换为整数-fno-math-errnoround() for float in C++
猜你喜欢
  • 1970-01-01
  • 2023-03-14
  • 1970-01-01
  • 2011-02-23
  • 2012-09-03
  • 2012-12-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多