在 int 和 double 之间转换有多昂贵?

How expensive is it to convert between int and double?

我经常看到代码将整数转换为双精度数,再将整数转换为双精度数,然后再次返回(有时是出于充分的理由,有时不是),我突然想到这似乎是 "hidden" 成本我的程序。假设转换方法是截断。

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

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

我还假设我们最敏锐地体验到缓慢转换的影响的方式是电源使用而不是执行速度,考虑到我们每秒可以执行多少计算相对于如何计算的差异很多数据实际上可以每秒到达 CPU。

这是我自己可以挖掘的东西,对于 x86-64 使用 SSE2 进行 FP 数学运算(不是传统的 x87,其中更改 C++ 的 t运行cation 语义的舍入模式很昂贵):

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

    doubleintcvtsi2sd。 (cvtsi2sdl 32 位操作数大小的 cvtsi2sd 的 AT&T 语法。)

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

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

  2. 这些指令每条的成本大约相当于一个 FP addsd 加上一个 movq xmm, r64(fp <- 整数)或 movq r64, xmm(整数 <- fp) ,因为它们在主流 (Sandybridge/Haswell/Sklake) 英特尔 CPU 的相同端口上解码为 2 微指令。

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

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

    SIMD packed-float 到 packed-int 很棒;单身狗。但是转换为 double 需要洗牌以更改元素大小。 SIMD float/double<->int64_t 直到 AVX512 才存在,但可以在有限范围内手动完成。

    Intel 的手册将延迟定义为:"The number of clock cycles that are required for the execution core to complete the execution of all of the μops that form an instruction." 但更有用的定义是从输入准备就绪到输出准备就绪的时钟数。如果乱序执行有足够的并行度来完成其工作,则吞吐量比延迟更重要:What considerations go into predicting latency for operations on modern superscalar processors and how can I calculate them by hand?.

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

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

所以,答案归结为:

1) 它是硬件优化的,编译器利用了硬件机制。

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

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

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

这种舍入方式对数学和硬件都不利,需要 FPU 切换到这种特殊的舍入模式,执行 t运行cation,然后切换回正常的舍入方式。

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

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 代码实际上会使代码 运行 稍微慢一些。

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