与java相比,为什么这个C ++代码执行速度如此之慢?

时间:2017-06-03 10:26:20

标签: java c++ performance

我最近用Java编写了一个计算密集型算法,然后将其翻译成C ++。令我惊讶的是,C ++的执行速度要慢得多。我现在编写了一个更短的Java测试程序,以及相应的C ++程序 - 见下文。我的原始代码具有很多数组访问权限,测试代码也是如此。 C ++的执行时间要长5.5倍(请参阅每个程序末尾的注释)。

结论1 st 以下21条评论......

测试代码:

  1. g++ -o ... Java快5.5倍
  2. g++ -O3 -o ... Java快2.9倍
  3. g++ -fprofile-generate -march=native -O3 -o ...(运行,然后g++ -fprofile-use等)Java快1.07倍。
  4. 我原来的项目(比测试代码复杂得多):

    1. Java快1.8倍
    2. C ++快1.9倍
    3. C ++快2倍
    4. Software environment:
          Ubuntu 16.04 (64 bit).
          Netbeans 8.2 / jdk 8u121 (java code executed inside netbeans)
          g++ (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609
          Compilation: g++ -o cpp_test cpp_test.cpp
      

      Java代码:

      public class JavaTest {
          public static void main(String[] args) {
              final int ARRAY_LENGTH = 100;
              final int FINISH_TRIGGER = 100000000;
              int[] intArray = new int[ARRAY_LENGTH];
              for (int i = 0; i < ARRAY_LENGTH; i++) intArray[i] = 1;
              int i = 0;
              boolean finished = false;
              long loopCount = 0;
              System.out.println("Start");
              long startTime = System.nanoTime();
              while (!finished) {
                  loopCount++;
                  intArray[i]++;
                  if (intArray[i] >= FINISH_TRIGGER) finished = true;
                  else if (i <(ARRAY_LENGTH - 1)) i++;
                  else i = 0;
              }
              System.out.println("Finish: " + loopCount + " loops; " +
                  ((System.nanoTime() - startTime)/1e9) + " secs");
              // 5 executions in range 5.98 - 6.17 secs (each 9999999801 loops)
          }
      }
      

      C ++代码:

      //cpp_test.cpp:
      #include <iostream>
      #include <sys/time.h>
      int main() {
          const int ARRAY_LENGTH = 100;
          const int FINISH_TRIGGER = 100000000;
          int *intArray = new int[ARRAY_LENGTH];
          for (int i = 0; i < ARRAY_LENGTH; i++) intArray[i] = 1;
          int i = 0;
          bool finished = false;
          long long loopCount = 0;
          std::cout << "Start\n";
          timespec ts;
          clock_gettime(CLOCK_REALTIME, &ts);
          long long startTime = (1000000000*ts.tv_sec) + ts.tv_nsec;
          while (!finished) {
              loopCount++;
              intArray[i]++;
              if (intArray[i] >= FINISH_TRIGGER) finished = true;
              else if (i < (ARRAY_LENGTH - 1)) i++;
              else i = 0;
          }
          clock_gettime(CLOCK_REALTIME, &ts);
          double elapsedTime =
              ((1000000000*ts.tv_sec) + ts.tv_nsec - startTime)/1e9;
          std::cout << "Finish: " << loopCount << " loops; ";
          std::cout << elapsedTime << " secs\n";
          // 5 executions in range 33.07 - 33.45 secs (each 9999999801 loops)
      }
      

2 个答案:

答案 0 :(得分:5)

在使用分析信息时,我唯一能让C ++程序超越Java的时间。这表明运行时信息中有一些东西(默认情况下是Java),可以更快地执行。

除了非平凡的if语句之外,你的程序中没有太多事情发生。也就是说,如果不分析整个程序,很难预测哪个分支最有可能。这让我相信这是一个分支错误预测问题。现代CPU执行instruction pipelining,这允许更高的CPU吞吐量。但是,这需要预测下一条要执行的指令。如果猜测错误,则必须清除指令管道,并加载正确的指令(这需要时间)。

在编译时,编译器没有足够的信息来预测哪个分支最有可能。 CPU也做了一些分支预测,但这通常是循环循环和ifs if(而不是其他)。

但是,Java具有能够在运行时和编译时使用信息的优点。这允许Java将中间分支标识为最常出现的分支,因此为管道预测此分支。

答案 1 :(得分:4)

不知何故,GCC和clang都无法展开这个循环,即使在-O3和-Os中也会取出不变量,但是Java会这样做。

Java的最终JITted汇编代码与此类似(实际上是repeated twice):

    while (true) {
        loopCount++;
        if (++intArray[i++] >= FINISH_TRIGGER) break;
        loopCount++;
        if (++intArray[i++] >= FINISH_TRIGGER) break;
        loopCount++;
        if (++intArray[i++] >= FINISH_TRIGGER) break;
        loopCount++;
        if (++intArray[i++] >= FINISH_TRIGGER) { if (i >= ARRAY_LENGTH) i = 0; break; }
        if (i >= ARRAY_LENGTH) i = 0;
    }

通过这个循环,我得到了C ++和Java之间完全相同的时间(6.4s)。

为什么这样做合法?因为ARRAY_LENGTH是100,它是4的倍数。因此i只能超过100并且每4次迭代重置为0。

这看起来像GCC和clang的改进机会;它们无法展开迭代总数未知的循环,但即使强制展开,它们也无法识别仅适用于某些迭代的循环部分。

关于你在一个更复杂的代码(也就是现实生活)中的发现:Java的优化器非常适合小循环,已经考虑了很多,但Java在虚拟调用上浪费了大量时间和GC。

最后,它归结为在具体架构上运行的机器指令,无论谁想出最好的集合,都会获胜。不要假设编译器会“做正确的事情&#34;”,查看生成的代码,配置文件,重复。

例如,如果你稍微重构一下你的循环:

    while (!finished) {
        for (i=0; i<ARRAY_LENGTH; ++i) {
            loopCount++;
            if (++intArray[i] >= FINISH_TRIGGER) {
                finished=true;
                break;
            }
        }
    }

然后C ++将胜过Java(5.9s vs 6.4s)。 (revised C++ assembly

如果你可以允许轻微超限(在达到退出条件后增加更多intArray个元素):

    while (!finished) {
        for (int i=0; i<ARRAY_LENGTH; ++i) {
            ++intArray[i];
        }
        loopCount+=ARRAY_LENGTH;
        for (int i=0; i<ARRAY_LENGTH; ++i) {
            if (intArray[i] >= FINISH_TRIGGER) {
                loopCount-=ARRAY_LENGTH-i-1;
                finished=true;
                break;
            }
        }
    }

现在clang能够对循环进行矢量化并达到 3.5s 的速度,而Java的 4.8s (遗憾的是,GCC仍然无法向量化它)。