浮点与现代硬件上的整数计算

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

现在,我记得大约读了386天的浮点计算如此慢,我相信(IIRC)有一个可选的共同进程。 但是现在肯定有更复杂和更强大的CPU,如果进行浮点或整数计算,“速度”没有什么区别? 特别是由于实际的计算时间相对于导致pipe道失速或从主存储器中取出某些东西而言是微小的。

我知道正确的答案是在目标硬件上进行基准testing,那么testing它的好方法是什么? 我写了两个微小的C ++程序,并将它们的运行时间与Linux上的“time”进行了比较,但是实际运行时间太可变(不能帮助我在虚拟服务器上运行)。 没有花费我的整个一天运行数百个基准,制作图表等是否有我可以做的,以获得合理的相对速度testing? 任何想法或想法? 我完全错了吗?

我使用的程序如下,它们在任何方面都不相同:

#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(从下面的评论粘贴):我们目前有一个广泛的代码库。 真的,我反对泛泛的说法,“因为整数计算速度更快,所以我们不能使用浮点数” – 而且我正在寻找一种方法来解决这个广义的假设。 我意识到,在完成所有工作之后,我们不可能预测确切的结果。

无论如何,感谢所有你的优秀答案和帮助。 随意添加任何其他:)。

唉,我只能给你一个“这取决于”的答案…

从我的经验来看,有很多很多的variables来performance…特别是在整数和浮点math之间。 由于不同的处理器具有不同的“stream水线”长度,因此处理器之间的差异很大(即使在相同的系列中也是如此)。 此外,一些操作通常非常简单(例如加法),并且具有通过处理器的加速路线,而其他操作(例如分区)需要更长的时间。

另一大variables是数据所在的位置。 如果您只添加了一些值,那么所有的数据都可以驻留在caching中,并可以快速发送到CPU。 已经拥有caching中的数据的非常非常慢的浮点操作比整数操作要快很多倍,因为需要从系统内存中复制一个整数。

我假设你问这个问题是因为你正在研究一个关键性能的应用程序。 如果您正在开发x86架构,并且需要额外的性能,则可能需要考虑使用SSE扩展。 这可以大大加快单精度浮点运算的速度,因为一次可以对多个数据执行相同的操作,另外还有一个用于SSE操作的单独的寄存器组。 (我注意到在你的第二个例子中,你用“float”而不是“double”,这让我觉得你使用的是单精度math)。

*注意:使用旧的MMX指令实际上会使程序变慢,因为这些旧的指令实际上使用了与FPU相同的寄存器,因此不可能同时使用FPU和MMX。

例如(更less的数字更快),

64位Intel Xeon X5550 @ 2.67GHz,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.81GHz,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] 

正如Dan所指出的那样 ,即使您对时钟频率(在stream水线devise中本身可能会产生误导)进行标准化, 结果也会因CPU体系结构 (单独的ALU / FPU性能 以及每个ALU / FPU的实际数量核心在超标量devise中影响了多less个独立的操作可以并行执行 – 后面的因素不是由下面的代码执行的,因为下面的所有操作都是顺序依赖的。

穷人的FPU / ALU操作基准:

 #include <stdio.h> #ifdef _WIN32 #include <sys/timeb.h> #else #include <sys/time.h> #endif #include <time.h> 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; } 

加法比rand快得多,所以你的程序是(尤其是)无用的。

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

通常,使用整数算术来尝试FP作业是一个缓慢的处方。

定点和浮点math之间的实际速度可能存在显着差异,但ALU与FPU的理论最佳情况吞吐量完全不相关。 相反,在你的架构上,整数和浮点寄存器(实际寄存器,而不是寄存器名称)的数量不会被你的计算所使用(例如,用于循环控制),每个types的元素的数量适合一个caching行,考虑到整数与浮点math的不同语义,可能进行优化 – 这些效应将占主导地位。 algorithm的数据依赖性在这里起着重要的作用,所以没有一般​​的比较可以预测你的问题的性能差距。

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

您也可能一次将更多的整数操作数放入caching中。 所以即使在FPU理论上具有较高吞吐量的机器上,定点版本也可能比浮点版本的性能好一个数量级。

要考虑两点 –

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

与往常一样,唯一确定的方法是分析您的实际程序。

第二点是,目前大多数CPU都有SIMD浮点指令,可以同时对多个浮点值进行操作。 例如,您可以将4个浮点数加载到一个SSE寄存器中,并且在它们上执行4次乘法并行处理。 如果你可以重写代码的一部分来使用SSE指令,那么它看起来可能会比整数版本更快。 Visual c + +提供编译器内在函数来执行此操作,请参阅http://msdn.microsoft.com/en-us/library/x5c07e2a(v=VS.80).aspx获取一些信息。

TIL这个变化很大(很多)。 这里有一些使用gnu编译器的结果(顺便说一句,我也通过在机器上编译来检查,gen g ++ 5.4来自xenial是比linaro精确的4.6.3更快的地狱)

英特尔i7 4700MQ 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 

英特尔i3 2370M有类似的结果

 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 

英特尔(R)赛扬(R)2955U(运行xenial的Acer 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 1GB液滴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 vs FPU mul。 加法和减法显得不同。

我跑了一个testing,只是加了1,而不是rand()。 结果(在x86-64上)是:

  • 简短:4.260s
  • int:4.020s
  • 漫长的:3.350s
  • 浮动:7.330s
  • 双倍:7.210s

除非你正在编写每秒钟被调用数百万次的代码(例如,在graphics应用程序中画一条线到屏幕上),否则整数与浮点运算几乎不是瓶颈。

通常效率问题的第一步是分析您的代码,以查看运行时间的实际使用情况。 这个linux命令是gprof

编辑:

虽然我想你总是可以使用整数和浮点数来实现线条绘制algorithm,但要多次调用它,看看它是否有所作为:

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

如果没有剩余操作,则浮点版本会慢很多。 由于所有的添加都是顺序的,所以cpu将不能并行求和。 延迟将是至关重要的。 FPU添加延迟通常是3个周期,而整数加法是1个周期。 然而,余数运算符的分频器可能是关键部分,因为在现代cpu上没有完全stream水线。 因此,假设除法/余数指令将占用大部分时间,由于加延迟而产生的差异将会很小。

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

(a)一旦你需要更多的整数操作,你可能需要更多的,所以轻微的速度优势是超过了额外的操作

(b)浮点代码更简单,这意味着编写代码会更快,这意味着如果速度很关键,可以花更多时间优化代码。

基于这种可靠的“我听到的东西”,在过去,整数计算比浮点运算速度快了20到50倍,而现在这个速度还不到两倍。