Floating point vs integer calculations on modern hardware

我正在用 C + + 做一些性能关键的工作,我们目前正在使用整数计算的问题,本质上是浮点数,因为“它更快”。这导致了很多恼人的问题,并且增加了很多恼人的代码。

现在,我记得读过关于浮点计算如何如此缓慢大约386天,我相信(IIRC)有一个可选的协处理器。但是可以肯定的是,当今的 CPU 复杂度和功能都呈指数级增长,如果进行浮点运算或整数运算,它在“速度”上没有什么不同?特别是因为实际的计算时间与导致管道停止或从主存中提取某些内容相比是很小的?

我知道正确的答案是对目标硬件进行基准测试,测试的好方法是什么?我编写了两个小小的 C + + 程序,并将它们的运行时间与 Linux 上的“ time”进行了比较,但实际的运行时间变化太大(对我在虚拟服务器上运行没有帮助)。除了花一整天的时间来做数百个基准测试,制作图表等等,我还能做些什么来合理测试相对速度呢?有什么想法吗?我完全错了吗?

The programs I used as follows, they are not identical by any means:

#include <iostream>
#include <cmath>
#include <cstdlib>
#include <time.h>


int main( int argc, char** argv )
{
int accum = 0;


srand( time( NULL ) );


for( unsigned int i = 0; i < 100000000; ++i )
{
accum += rand( ) % 365;
}
std::cout << accum << std::endl;


return 0;
}

程序2:

#include <iostream>
#include <cmath>
#include <cstdlib>
#include <time.h>


int main( int argc, char** argv )
{


float accum = 0;
srand( time( NULL ) );


for( unsigned int i = 0; i < 100000000; ++i )
{
accum += (float)( rand( ) % 365 );
}
std::cout << accum << std::endl;


return 0;
}

先谢谢你!

编辑: 我关心的平台是在桌面 Linux 和 Windows 机器上运行的常规 x86或 x86-64。

编辑2(粘贴从下面的注释) : 我们有一个广泛的代码库目前。实际上,我遇到过这样的一般性问题,即我们“一定不要使用 float,因为整数计算更快”——我正在寻找一种方法(如果这是真的)来证明这种一般性假设是错误的。我意识到,如果不做所有的工作并在事后对其进行分析,就不可能为我们预测确切的结果。

无论如何,感谢你所有出色的回答和帮助。请随意添加其他内容:)。

124879 次浏览

除非你正在编写每秒调用数百万次的代码(例如,在图形应用程序中画一条直线到屏幕) ,否则整数和浮点运算很少成为瓶颈。

解决效率问题的通常第一步是对代码进行概要分析,以查看运行时真正花费在哪里。这个命令的 linux 命令是 gprof

编辑:

Though I suppose you can always implement the line drawing algorithm using integers and floating-point numbers, call it a large number of times and see if it makes a difference:

http://en.wikipedia.org/wiki/Bresenham's_algorithm

加法比 rand快得多,所以你的程序(特别)没用。

您需要确定性能热点并逐步修改您的程序。听起来好像您的开发环境有问题,需要首先解决。是不是不可能在你的电脑上运行一个小问题集的程序?

一般来说,使用整数算法尝试 FP 作业会导致速度变慢。

基于那个“我听说过的一些事情”,在过去,整数计算比浮点运算快20到50倍,而现在,它的速度不到浮点运算的两倍。

唉,我只能给你一个“视情况而定”的答案..。

根据我的经验,性能有很多很多的变量,特别是整数和浮点数之间的变量。由于不同的处理器有不同的“管道”长度,因此它在不同的处理器之间有很大的差异(即使是在同一个系列中,比如 x86)。另外,有些操作通常非常简单(如加法) ,并且在处理器中有一个加速路由,而其他操作(如除法)需要花费更长的时间。

另一个大变量是数据所在的位置。如果您只需要添加几个值,那么所有数据都可以驻留在缓存中,这样就可以快速地将它们发送到 CPU。一个非常非常缓慢的浮点操作,如果已经在缓存中存储了数据,那么它将比整数操作快很多倍,在整数操作中,需要从系统内存中复制一个整数。

我假设您问这个问题是因为您正在处理一个性能关键的应用程序。如果您正在为 x86体系结构开发,并且需要额外的性能,那么可能需要考虑使用 SSE 扩展。这可以大大加快单精度浮点运算,因为可以同时对多个数据执行相同的操作,另外还有用于 SSE 操作的单独的寄存器 * 银行。(我注意到在第二个示例中,您使用了“ float”而不是“ double”,这使我认为您使用的是单精度数学)。

注意: 使用旧的 MMX 指令实际上会降低程序的速度,因为那些旧的指令实际上使用了与 FPU 相同的寄存器,使得不可能同时使用 FPU 和 MMX。

我运行了一个测试,将 rand ()替换为1,结果(在 x86-64上)是:

  • 短期: 4.260秒
  • Int: 4.020 s
  • Long Long: 3.350 s
  • 浮动: 7.330秒
  • 双倍: 7.210秒

固定点和浮点数学之间在实际速度上可能存在显著差异,但是 ALU 和 FPU 的理论最佳情况吞吐量是完全不相关的。相反,整数寄存器和浮点寄存器(实际寄存器,而不是寄存器名称)的数量在你的体系结构中没有被你的计算使用(例如循环控制) ,每种类型的元素的数量适合一个缓存线,优化可能考虑到不同的语义整数和浮点数学-这些影响将占主导地位。在这里,算法的数据依赖性起着重要作用,因此没有一般的比较能够预测问题的性能差距。

例如,整数加法是可交换的,所以如果编译器看到一个循环,就像你用于基准测试(假设随机数据是提前准备好的,这样就不会模糊结果) ,它可以展开循环,计算没有依赖关系的部分和,然后在循环终止时添加它们。但是对于浮点数,编译器必须按照你要求的顺序执行操作(你已经得到了序列点,所以编译器必须保证相同的结果,这就不允许重新排序) ,所以每个加法对前一个加法的结果有很强的依赖性。

您可能会在缓存中一次适合更多的整数操作数。因此,即使在理论上 FPU 具有更高吞吐量的机器上,定点版本的性能也可能比浮点版本高出一个数量级。

For example (lesser numbers are faster),

64位 Intel Xeon X5550@2.67 GHz,gcc 4.1.2 -O3

short add/sub: 1.005460 [0]
short mul/div: 3.926543 [0]
long add/sub: 0.000000 [0]
long mul/div: 7.378581 [0]
long long add/sub: 0.000000 [0]
long long mul/div: 7.378593 [0]
float add/sub: 0.993583 [0]
float mul/div: 1.821565 [0]
double add/sub: 0.993884 [0]
double mul/div: 1.988664 [0]

32位双核 AMD Opteron (tm)处理器265@1.81 GHz,gcc 3.4.6 -O3

short add/sub: 0.553863 [0]
short mul/div: 12.509163 [0]
long add/sub: 0.556912 [0]
long mul/div: 12.748019 [0]
long long add/sub: 5.298999 [0]
long long mul/div: 20.461186 [0]
float add/sub: 2.688253 [0]
float mul/div: 4.683886 [0]
double add/sub: 2.700834 [0]
double mul/div: 4.646755 [0]

作为 丹指出,即使你规范化的时钟频率(这可能会在流水线设计本身误导) ,results will vary wildly based on CPU architecture(个人 一个 href = “ http://en.wikipedia.org/wiki/as well as实际 ALU/FPU 数目每核心可在 超标量的设计,这影响多少 independent operations can execute in parallel-后者的因素不是由下面的代码,因为下面的所有操作是顺序依赖的)

穷人的 FPU/ALU 操作基准:

#include <stdio.h>
#ifdef _WIN32
#include <sys/timeb.h>
#else
#include <sys/time.h>
#endif
#include <time.h>
#include <cstdlib>


double
mygettime(void) {
# ifdef _WIN32
struct _timeb tb;
_ftime(&tb);
return (double)tb.time + (0.001 * (double)tb.millitm);
# else
struct timeval tv;
if(gettimeofday(&tv, 0) < 0) {
perror("oops");
}
return (double)tv.tv_sec + (0.000001 * (double)tv.tv_usec);
# endif
}


template< typename Type >
void my_test(const char* name) {
Type v  = 0;
// Do not use constants or repeating values
//  to avoid loop unroll optimizations.
// All values >0 to avoid division by 0
// Perform ten ops/iteration to reduce
//  impact of ++i below on measurements
Type v0 = (Type)(rand() % 256)/16 + 1;
Type v1 = (Type)(rand() % 256)/16 + 1;
Type v2 = (Type)(rand() % 256)/16 + 1;
Type v3 = (Type)(rand() % 256)/16 + 1;
Type v4 = (Type)(rand() % 256)/16 + 1;
Type v5 = (Type)(rand() % 256)/16 + 1;
Type v6 = (Type)(rand() % 256)/16 + 1;
Type v7 = (Type)(rand() % 256)/16 + 1;
Type v8 = (Type)(rand() % 256)/16 + 1;
Type v9 = (Type)(rand() % 256)/16 + 1;


double t1 = mygettime();
for (size_t i = 0; i < 100000000; ++i) {
v += v0;
v -= v1;
v += v2;
v -= v3;
v += v4;
v -= v5;
v += v6;
v -= v7;
v += v8;
v -= v9;
}
// Pretend we make use of v so compiler doesn't optimize out
//  the loop completely
printf("%s add/sub: %f [%d]\n", name, mygettime() - t1, (int)v&1);
t1 = mygettime();
for (size_t i = 0; i < 100000000; ++i) {
v /= v0;
v *= v1;
v /= v2;
v *= v3;
v /= v4;
v *= v5;
v /= v6;
v *= v7;
v /= v8;
v *= v9;
}
// Pretend we make use of v so compiler doesn't optimize out
//  the loop completely
printf("%s mul/div: %f [%d]\n", name, mygettime() - t1, (int)v&1);
}


int main() {
my_test< short >("short");
my_test< long >("long");
my_test< long long >("long long");
my_test< float >("float");
my_test< double >("double");


return 0;
}

有两点需要考虑

现代硬件可以重叠指令,并行执行指令,并重新排列指令,以便充分利用硬件。而且,任何重要的浮点数程序也可能有重要的整数工作,即使它只是计算到数组的索引,循环计数器等,所以即使你有一个缓慢的浮点数指令,它很可能是运行在一个单独的位硬件与一些整数工作重叠。我的观点是,即使浮点指令比整数指令慢,您的整个程序也可能运行得更快,因为它可以利用更多的硬件。

像往常一样,唯一可以确定的方法就是对实际的程序进行概要分析。

第二点是,现在大多数 CPU 都有针对浮点数的 SIMD 指令,可以同时操作多个浮点数值。例如,您可以将4个浮点数加载到一个 SSE 寄存器中,并在它们上并行执行4个乘法运算。如果您可以重写部分代码以使用 SSE 指令,那么它似乎比整数版本更快。Visualc + + 提供了编译器内部函数来实现这一点,有关信息,请参见 http://msdn.microsoft.com/en-us/library/x5c07e2a(v=VS.80).aspx

今天,整数运算通常比浮点运算快一点。因此,如果可以使用相同的整数和浮点运算进行计算,请使用整数。然而,你说“这会导致很多恼人的问题,并增加了很多恼人的代码”。这听起来像是您需要更多的操作,因为您使用的是整数算术而不是浮点数。在这种情况下,浮点运行会更快,因为

  • 一旦您需要更多的整数操作,您可能需要更多的整数操作,因此这个小小的速度优势被更多的操作所消耗

  • 浮点代码更简单,这意味着编写代码更快,这意味着如果速度至关重要,您可以花更多的时间优化代码。

如果没有余数操作,那么浮点数版本会慢得多。由于所有的添加都是顺序的,CPU 将无法并行处理求和。延迟时间至关重要。FPU 加法延迟通常为3个周期,而整数加法为1个周期。然而,其余运营商的分割器可能是关键部分,因为它没有完全流水线上的现代中央处理器。因此,假设除/余指令将消耗大部分时间,因添加延迟而产生的差异将很小。

这个变化(很大)。下面是使用 gnu 编译器得到的一些结果(顺便说一下,我还在机器上进行了编译,Xenial 的 gnu g + + 5.4比 linaro 的4.6.3快得多,精确地说)

英特尔 i74700MQ Xenial

short add: 0.822491
short sub: 0.832757
short mul: 1.007533
short div: 3.459642
long add: 0.824088
long sub: 0.867495
long mul: 1.017164
long div: 5.662498
long long add: 0.873705
long long sub: 0.873177
long long mul: 1.019648
long long div: 5.657374
float add: 1.137084
float sub: 1.140690
float mul: 1.410767
float div: 2.093982
double add: 1.139156
double sub: 1.146221
double mul: 1.405541
double div: 2.093173

英特尔 i32370M 也有类似的结果

short add: 1.369983
short sub: 1.235122
short mul: 1.345993
short div: 4.198790
long add: 1.224552
long sub: 1.223314
long mul: 1.346309
long div: 7.275912
long long add: 1.235526
long long sub: 1.223865
long long mul: 1.346409
long long div: 7.271491
float add: 1.507352
float sub: 1.506573
float mul: 2.006751
float div: 2.762262
double add: 1.507561
double sub: 1.506817
double mul: 1.843164
double div: 2.877484

英特尔(注册商标)赛扬(注册商标)2955U (宏基 C720 Chromebook 运行异种)

short add: 1.999639
short sub: 1.919501
short mul: 2.292759
short div: 7.801453
long add: 1.987842
long sub: 1.933746
long mul: 2.292715
long div: 12.797286
long long add: 1.920429
long long sub: 1.987339
long long mul: 2.292952
long long div: 12.795385
float add: 2.580141
float sub: 2.579344
float mul: 3.152459
float div: 4.716983
double add: 2.579279
double sub: 2.579290
double mul: 3.152649
double div: 4.691226

DigitalOcean 1 GB Droplet Intel (R) Xeon (R) CPU E5-2630L v2(运行可靠)

short add: 1.094323
short sub: 1.095886
short mul: 1.356369
short div: 4.256722
long add: 1.111328
long sub: 1.079420
long mul: 1.356105
long div: 7.422517
long long add: 1.057854
long long sub: 1.099414
long long mul: 1.368913
long long div: 7.424180
float add: 1.516550
float sub: 1.544005
float mul: 1.879592
float div: 2.798318
double add: 1.534624
double sub: 1.533405
double mul: 1.866442
double div: 2.777649

AMD Opteron (tm)处理器4122(精密)

short add: 3.396932
short sub: 3.530665
short mul: 3.524118
short div: 15.226630
long add: 3.522978
long sub: 3.439746
long mul: 5.051004
long div: 15.125845
long long add: 4.008773
long long sub: 4.138124
long long mul: 5.090263
long long div: 14.769520
float add: 6.357209
float sub: 6.393084
float mul: 6.303037
float div: 17.541792
double add: 6.415921
double sub: 6.342832
double mul: 6.321899
double div: 15.362536

这使用来自 http://pastebin.com/Kx8WGUfg的代码作为 benchmark-pc.c

g++ -fpermissive -O3 -o benchmark-pc benchmark-pc.c

我已经运行了多次,但这似乎是一个情况,一般数字是相同的。

一个值得注意的例外似乎是 ALU mul 和 FPU mul。加法和减法似乎有很大的不同。

以下是图表格式(点击全尺寸,越小越快越好) :

Chart of above data

更新到容纳@Peter Cordes

Https://gist.github.com/lewiscowles1986/90191c59c9aedf3d08bf0b129065cccc

i7 4700MQ Linux Ubuntu Xenial 64-bit (all patches to 2018-03-13 applied)
    short add: 0.773049
short sub: 0.789793
short mul: 0.960152
short div: 3.273668
int add: 0.837695
int sub: 0.804066
int mul: 0.960840
int div: 3.281113
long add: 0.829946
long sub: 0.829168
long mul: 0.960717
long div: 5.363420
long long add: 0.828654
long long sub: 0.805897
long long mul: 0.964164
long long div: 5.359342
float add: 1.081649
float sub: 1.080351
float mul: 1.323401
float div: 1.984582
double add: 1.081079
double sub: 1.082572
double mul: 1.323857
double div: 1.968488
AMD Opteron (tm)处理器4122(精确,DreamHost 共享主机)
    short add: 1.235603
short sub: 1.235017
short mul: 1.280661
short div: 5.535520
int add: 1.233110
int sub: 1.232561
int mul: 1.280593
int div: 5.350998
long add: 1.281022
long sub: 1.251045
long mul: 1.834241
long div: 5.350325
long long add: 1.279738
long long sub: 1.249189
long long mul: 1.841852
long long div: 5.351960
float add: 2.307852
float sub: 2.305122
float mul: 2.298346
float div: 4.833562
double add: 2.305454
double sub: 2.307195
double mul: 2.302797
double div: 5.485736
Intel 至强 E5-2630L v2@2.4 GHz (Trusty 64-bit,DigitalOcean VPS)
    short add: 1.040745
short sub: 0.998255
short mul: 1.240751
short div: 3.900671
int add: 1.054430
int sub: 1.000328
int mul: 1.250496
int div: 3.904415
long add: 0.995786
long sub: 1.021743
long mul: 1.335557
long div: 7.693886
long long add: 1.139643
long long sub: 1.103039
long long mul: 1.409939
long long div: 7.652080
float add: 1.572640
float sub: 1.532714
float mul: 1.864489
float div: 2.825330
double add: 1.535827
double sub: 1.535055
double mul: 1.881584
double div: 2.777245