累加双精度与整型

Accumulating doubles vs ints

本文关键字:整型 双精度      更新时间:2023-10-16

当将a声明为双精度类型时,与将其声明为int类型相比,下面的代码要慢一些:

double a = 0;
int j[1000];
for(int i=0; i<1000; i++){
    a += (i * j[i]);
}

双精度加法的性能下降是因为编译器选择了不同的汇编指令,而不是如果a被声明为int会选择什么?

我试图了解CPU是否在运行时对其自身的单/双精度进行任何"转换",这在汇编和成本执行时间中无法看到?

让我们将循环中的表达式分解为各个部分。为此,我们将重写代码,使每行都是一次赋值和一次操作。当从int版本开始时,它看起来像这样:

// not depending on a
/* 1 */ auto t1 = j[i];
/* 2 */ auto t2 = i * j
// depending on decltype(a)
/* 3 */ decltype(a+t2) t3 = static_cast<decltype(a+t2)>(t2);
/* 4 */ decltype(a+t3) t4 = static_cast<decltype(a+t3)>(a);
/* 5 */ a = t3 + t4;

前两个操作完全不依赖于a的类型,并且在任何一种情况下都将执行完全相同的操作。

但是,从操作3开始,有一个区别。这样做的原因是为了添加at2,编译器必须首先将它们转换为通用类型。在a是整数的情况下,操作3和4根本不做任何事情(int + int产生int,因此两次强制转换都将int s转换为int s)。然而,在adouble的情况下,t2必须在相加之前转换为double (int + double产生double)。

这意味着操作5中的加法类型也不同:它可以是intdouble加法。忽略double通常是int的两倍这一明显的方面,这意味着计算机在这一点上需要做一些不同的事情。

对x64的影响

在使用优化编译器为现代x64机器编译此程序时,应该注意的是,当按此声明时,整个程序可能会被优化掉。假设没有发生这种情况,并且编译器没有应用任何非法优化,并且您可以使用未初始化的变量(j的元素)引入UB,则可能发生以下情况:

// not depending on a
MOV EAX, i // copy i to EAX register
IMUL j[i] // EAX = EAX * j[i] (high 32 bits are stored in EDX and ignored)
// if a is int
ADD a, EAX // integer addition: a += EAX
// else if a is double
CVTSI2SD XMM0, EAX // convert the multiplication result to double
ADDPD a, XMM0 // double addition: a += XMM0
// endif

一个好的编译器会稍微展开循环,并交错一些循环,因为循环限制是已知的。正如您所看到的,操作和依赖链的指令至少增加了两倍。而且,第二个版本中的指令比第一个版本中的单个指令慢。

虽然我确信第二个版本可以用一个更有效的版本来表述,但应该注意的是,第一个版本的整数ADD是在任何CPU上最快的运算之一,通常比它的浮点运算更快。

那么,回答你的问题:CPU确实执行浮点数和整数之间的转换-这在汇编中是可见的,并且具有(潜在的重大)运行成本。

单精度呢?

既然你也问了单精度,让我们检查使用float时会发生什么:

// not depending on a
MOV EAX, i // copy i to EAX register
IMUL j[i] // EAX = EAX * j[i] (high 32 bits are stored in EDX and ignored)
// if a is float
CVTSI2SS XMM0, EAX // convert the multiplication result to float
ADDPS a, XMM0 // float addition: a += XMM0

汇编没有显示出显著的差异(我们只是用两个D替换double,用S替换single)。而且,有趣的是,性能上的差异也很小(例如,一个Haswell内核转换为double比转换为float要多花1 μ op,而加法本身显示相同的性能)。

验证

为了验证我的说法,我已经运行了2000000次循环,并确保a没有被优化掉。结果如下:

int   : 601.1 ms
float : 2567 ms
double: 2593 ms

我忽略了你的例子不编译,数组j没有初始化(我希望你会解决这个问题)。

浮点运算通常比整数运算慢。但是,您的代码有一个更昂贵的操作:将整数转换为浮点数。您的代码遭受双重打击(双关语)使用混合模式算术。