【问题标题】:Why is this C++ code execution so slow compared to java?为什么这个 C++ 代码的执行速度比 java 慢?
【发布时间】:2017-11-04 16:31:56
【问题描述】:

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

1st 21 cmets 后的结论 ...

测试代码:

  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 倍。

我的原始项目(比测试代码复杂得多):

  1. Java 快 1.8 倍
  2. C++ 快 1.9 倍
  3. C++ 快 2 倍
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)
}

【问题讨论】:

  • 你开启优化了吗?
  • @BillB 是的,对于发布代码,您至少应该打开优化:g++ -O3 -o cpp_test cpp_test.cpp 但是在我的系统上,Java 在这个测试中仍然比c++ 快一点,我也会想知道为什么 :) 可能是 Java 优化器注意到了 C++ 优化器没有的机会,毕竟这是过于简单的代码
  • 尝试在 Java 和 C++ 的最后将intArray 输出到标准输出,因为优化器可能会删除与数组相关的代码,如果这不会改变可观察的行为。
  • @BillB 添加标志 -fprofile-generate 后编译,然后运行程序,然后使用 -fprofile-use 重新编译,然后再次运行。顺便说一句,您可能还想添加-march=native
  • 它是-O3,带有大写 O。并且(不确定您是否正确) -fprofile-use 替换 -fprofile-generate。

标签: java c++ performance


【解决方案1】:

我唯一一次让 C++ 程序的性能优于 Java 是在使用分析信息时。这表明运行时信息(Java 默认获取)中有一些东西可以加快执行速度。

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

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

然而,Java 的优势在于能够在运行时和编译时使用信息。这允许 Java 将中间分支识别为最常出现的分支,因此可以为管道预测此分支。

【讨论】:

  • 是否为第一个 if 语句帮助的可能性添加特定于编译器的提示?
  • @Dunes 现代 CPU 实际上做了很多分支预测。他们保留了条件跳转的历史:en.wikipedia.org/wiki/Branch_predictor#Global_branch_prediction
  • @tambre 当我尝试时,在各种 if 上添加 __builtin_expect 并没有显着帮助(取决于猜测的值,我尝试了所有组合,有时会有点痛)。
【解决方案2】:

不知何故,即使在 -O3 和 -Os 中,GCC 和 clang 也无法展开此循环并提取不变量,但 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.4 秒)。

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

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

关于您在更复杂的代码(也就是现实生活)中的发现:Java 的优化器非常适合小循环,对此进行了很多思考,但 Java 在虚拟调用和 GC 上浪费了很多时间。

最终归结为在具体架构上运行的机器指令,谁提出最好的集合,谁就赢了。不要假设编译器会“做正确的事”,看看生成的代码,配置文件,重复。

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

    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 仍然无法对其进行矢量化)。

【讨论】:

  • 知道何时值得展开是一个难题。分析信息有助于 gcc 做出该决定。然而,展开目前在优化管道中很晚才完成,为时已晚,无法获得一些好处(这是 gcc 中的一个已知限制)。在这里,用break 替换finished=true 会意外地有所帮助,尽管代码仍然不如JVM 生成的代码好。
  • "不如JVM生成的那个"??你认为 JVM 版本不错吗?不,finished 对性能没有任何影响。
  • 你链接的JVM版本看起来还不错,为什么,你觉得丑吗? finished 确实对我有影响,在所有情况下都启用了分析,它将运行时间从 >6s 减少到
  • 我不明白 java 怎么可能接近。我的意思是虚拟机必须读取每条指令并执行操作。除了缓存未命中之外,它的速度要慢得多。还是 java 将字节码即时编译为机器码?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多