113

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

现在,我记得在大约 386 天时读到浮点计算如此缓慢,我相信(IIRC)有一个可选的协处理器。但是现在,随着 CPU 的复杂性和强大程度呈指数级增长,如果进行浮点或整数计算,“速度”肯定没有区别吗?特别是因为与导致管道停顿或从主内存中获取某些内容相比,实际计算时间很短?

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

我使用的程序如下,它们无论如何都不相同:

#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;
}

方案二:

#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(从下面的评论中粘贴):我们目前拥有广泛的代码库。真的,我已经反对我们“不能使用浮点数,因为整数计算更快”的概括 - 我正在寻找一种方法(如果这甚至是真的)来反驳这个概括的假设。我意识到,如果不做所有的工作并在事后进行分析,就不可能为我们预测确切的结果。

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

4

11 回答 11

56

例如(数字越小速度越快),

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 指出的那样,即使您对时钟频率进行了标准化(这在流水线设计中本身可能会产生误导),结果也会因 CPU 架构(单个ALU / FPU性能以及每个可用的 ALU/FPU实际数量)而大相径庭。超标量设计中的核心,它影响可以并行执行的独立操作的数量——后一个因素不受下面的代码执行,因为下面的所有操作都是顺序相关的。)

穷人的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;
}
于 2010-03-31T06:11:25.497 回答
39

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

根据我的经验,性能有很多很多变量……尤其是在整数和浮点数学之间。它因处理器而异(即使在同一个系列中,例如 x86),因为不同的处理器具有不同的“管道”长度。此外,一些操作通常非常简单(例如加法)并且通过处理器具有加速路径,而其他操作(例如除法)则需要更长的时间。

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

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

*注意:使用旧的 MMX 指令实际上会降低程序的速度,因为这些旧指令实际上使用了与 FPU 相同的寄存器,因此无法同时使用 FPU 和 MMX。

于 2010-03-31T04:05:24.897 回答
38

直到这变化(很多)。这是使用 gnu 编译器的一些结果(顺便说一句,我还通过在机器上编译进行了检查,来自 xenial 的 gnu g++ 5.4 比来自 linaro 的 4.6.3 精确得多)

Intel 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

Intel 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

Intel(R) Celeron(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 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。加法和减法似乎微不足道。

这是上面的图表形式(点击查看完整尺寸,较低的速度更快,更可取):

以上数据图表

更新以适应@Peter Cordes

https://gist.github.com/Lewiscowles1986/90191c59c9aedf3d08bf0b129065cccc

i7 4700MQ Linux Ubuntu Xenial 64 位(适用于 2018-03-13 的所有补丁)
    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 Xeon E5-2630L v2 @ 2.4GHz (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
于 2017-01-10T14:17:25.810 回答
31

定点和浮点数学之间的实际速度可能存在显着差异,但 ALU 与 FPU 的理论最佳情况吞吐量完全无关。取而代之的是,您的体系结构上的整数和浮点寄存器(实际寄存器,而不是寄存器名称)的数量,您的计算不使用其他方式(例如,用于循环控制),适合缓存行的每种类型的元素数量,考虑到整数与浮点数学的不同语义,可能进行优化——这些效果将占主导地位。算法的数据依赖性在这里起着重要作用,因此一般比较无法预测问题的性能差距。

例如,整数加法是可交换的,所以如果编译器看到一个循环,就像你在基准测试中使用的那样(假设随机数据是预先准备好的,所以它不会混淆结果),它可以展开循环并计算部分和没有依赖关系,然后在循环终止时添加它们。但是对于浮点,编译器必须按照您请求的相同顺序执行操作(您在那里有序列点,因此编译器必须保证相同的结果,这不允许重新排序)所以每个添加都强烈依赖于上一个的结果。

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

于 2010-03-31T05:20:48.473 回答
20

加法比 快得多rand,因此您的程序(尤其是)无用。

您需要识别性能热点并逐步修改您的程序。听起来您的开发环境存在需要首先解决的问题。对于一个小问题集是否不可能在您的 PC 上运行您的程序?

通常,尝试使用整数运算的 FP 作业会导致速度变慢。

于 2010-03-31T03:24:05.740 回答
8

需要考虑的两点——

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

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

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

于 2010-03-31T08:11:44.720 回答
5

如果没有余数运算,浮点版本会慢很多。由于所有的加法都是顺序的,cpu 将无法并行化求和。延迟将是至关重要的。FPU 加法延迟通常为 3 个周期,而整数加法为 1 个周期。但是,余数运算符的除法器可能是关键部分,因为它在现代 cpu 上没有完全流水线化。因此,假设除法/余数指令将消耗大部分时间,由于加法延迟造成的差异将很小。

于 2014-12-22T16:25:27.313 回答
4

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

效率问题通常的第一步是分析您的代码,以查看运行时真正花费的地方。用于此的 linux 命令是gprof.

编辑:

虽然我想你总是可以使用整数和浮点数来实现画线算法,但调用它很多次,看看它是否有所作为:

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

于 2010-03-31T03:21:49.843 回答
4

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

  • 一旦你需要更多的整数运算,你可能需要更多,所以轻微的速度优势不仅仅是被额外的运算所吞噬

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

于 2014-04-02T23:49:04.627 回答
3

我运行了一个测试,只是在数字上加了 1 而不是 rand()。结果(在 x86-64 上)是:

  • 短:4.260s
  • 诠释:4.020s
  • 长长:3.350s
  • 浮动:7.330s
  • 双倍:7.210s
于 2010-03-31T04:47:24.860 回答
0

基于那个非常可靠的“我听说过的东西”,在过去,整数计算的速度大约是浮点数的 20 到 50 倍,而现在它的速度还不到两倍。

于 2010-03-31T03:24:18.390 回答