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

Mar*_*ark 20 c++ x86 c++-cli x86-64 micro-optimization

我经常看到代码将int转换为双精度转换为双精度并再次转换(有时候出于好的理由,有时候没有),而且我刚刚想到这似乎是我程序中的"隐藏"成本.我们假设转换方法是截断.

那么,它有多贵?我确定它会因硬件而异,所以让我们假设一个新的英特尔处理器(Haswell,如果你愿意,虽然我会采取任何措施).我会感兴趣的一些指标(虽然一个好的答案不需要全部):

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

我还假设我们最敏锐地体验慢转换的影响的方式是关于功率使用而不是执行速度,因为我们每秒可以执行多少次计算相对于实际到达的数据量的差异在每秒CPU.

Mar*_*ark 30

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

  1. 当我看看生成的程序集从铛和gcc,它看起来像投intdouble,把它归结为一条指令:cvttsd2si.

    从它doubleintcvtsi2sd.(具有32位操作数大小的cvtsi2sdlAT&T语法cvtsi2sd.)

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

    所以我想问题就是:那些成本多少?

  2. 这些指令的成本与FP addsd加上a movq xmm, r64(fp < - 整数)或movq r64, xmm(整数< - fp)大致相同,因为它们在相同的端口,主流(Sandybridge/Haswell/Sklake)Intel CPU上解码为2 uop.

    64和IA-32架构优化参考手册说,成本cvttsd2si指令是5延迟(见附录C-16).cvtsi2sd根据您的体系结构,延迟时间从Silvermont的1到其他几个体系结构的7-16.

    Agner Fog的指令表具有更准确/更合理的数字,例如cvtsi2sdSilvermont上的5周期延迟(每2个时钟吞吐量为1),或者Haswell上的4c延迟,每个时钟吞吐量一个(如果您避免依赖于目标寄存器)与旧的上半部合并,就像gcc通常所做的那样pxor xmm0,xmm0).

    SIMD打包float到包装 - int很棒; 单个uop.但转换为double需要随机改变元素大小.SIMD float/double < - > int64_t在AVX512之前不存在,但可以在有限范围内手动完成.

    英特尔手册将延迟定义为:"执行内核完成执行构成指令的所有μop所需的时钟周期数." 但更有用的定义是输入准备好直到输出就绪的时钟数.如果没有足够的并行性来执行无序执行,那么吞吐量比延迟更重要: 在现代超标量处理器上预测操作的延迟需要考虑哪些因素以及如何手动计算它们?.

  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,浮动-point寄存器寄存器`mulss/ps/sd/pd`有lat = 5,1/thru = 0.5,以及32位和64位整数和浮点值之间的各种`cvt*`转换部分有lat = 3-4和1/thru = 1. (6认同)

650*_*502 7

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

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

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

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

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

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

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

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

  • 如果你使用`-m32 -msse2`怎么办? (3认同)