性能优化: C++与Java表现不如预期

4
我写了两个程序来实现一个简单的矩阵乘法算法,一个是用C++写的,另一个是用Java写的。出乎我的意料,Java程序的运行速度比C++程序快2.5倍左右。我对C++还很生疏,希望能够得到有关如何使C++程序运行更快的建议。
我的程序借鉴了这篇博客文章中的代码和数据:http://martin-thoma.com/matrix-multiplication-python-java-cpp
以下是我目前使用的编译标志:
g++ -O3 main.cc    

javac Main.java

以下是目前编译器/运行时的版本:

$ g++ --version
g++.exe (GCC) 4.8.1
Copyright (C) 2013 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ java -version
java version "1.8.0_05"
Java(TM) SE Runtime Environment (build 1.8.0_05-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.5-b02, mixed mode)

我的电脑是一台2012年左右的核心i3笔记本电脑,运行Windows和MinGW。以下是当前的性能测试结果:

$ time ./a.exe < ../Testing/2000.in
507584919
real    0m36.469s
user    0m0.031s
sys     0m0.030s

$ time java Main < ../Testing/2000.in
507584919
real    0m14.299s
user    0m0.031s
sys     0m0.015s

下面是C++程序:

#include <iostream>
#include <cstdio>
using namespace std;

int *A;
int *B;
int height;
int width;

int * matMult(int A[], int B[]) {
        int * C = new int[height*width];
        int n = height;
        for (int i = 0; i < n; i++) {
            for (int k = 0; k < n; k++) {
                for (int j = 0; j < n; j++) {
                    C[width*i+j]+=A[width*i+k] * B[width*k+j];
                }
            }
        }
        return C;
}

int main() {
  std::ios::sync_with_stdio(false);
  cin >> height;
  cin >> width;
  A = new int[width*height];
  B = new int[width*height];
  for (int i = 0; i < width*height; i++) {
    cin >> A[i];
  }

  for (int i = 0; i < width*height; i++) {
    cin >> B[i];
  }

  int *result = matMult(A,B);
  cout << result[2];
}

以下是Java程序:

import java.util.*;
import java.io.*;

public class Main {

    static int[] A;
    static int[] B;
    static int height;
    static int width;

public static void main(String[] args) {
    try {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        height = Integer.parseInt(reader.readLine());
        width = Integer.parseInt(reader.readLine());
        A=new int[width*height];
        B=new int[width*height];
        int index = 0;

        String thisLine;
        while ((thisLine = reader.readLine()) != null) {
            if (thisLine.trim().equals("")) {
                break;
            } else {
                String[] lineArray = thisLine.split("\t");
                for (String number : lineArray) {
                    A[index] = Integer.parseInt(number);
                    index++;
                }
            }
        }

        index = 0;
        while ((thisLine = reader.readLine()) != null) {
            if (thisLine.trim().equals("")) {
                break;
            } else {
                String[] lineArray = thisLine.split("\t");
                for (String number : lineArray) {
                    B[index] = Integer.parseInt(number);
                    index++;
                }
            }
        }

        int[] result = matMult(A,B);
        System.out.println(result[2]);

        reader.close();


    } catch (Exception e) {
        e.printStackTrace();
    }
}

public static int[] matMult(int[] A, int[] B) {
        int[] C = new int[height*width];
        int n = height;
        for (int i = 0; i < n; i++) {
            for (int k = 0; k < n; k++) {
                for (int j = 0; j < n; j++) {
                    C[width*i+j]+=A[width*i+k] * B[width*k+j];
                }
            }
        }
        return C;
    }
}

这里有一个2000x2000的测试用例链接:https://mega.nz/#!sglWxZqb!HBts_UlZnR4X9gZR7bG-ej3xf2A5vUv0wTDUW-kqFMA 这里有一个2x2的测试用例链接:https://mega.nz/#!QwkV2SII!AtfGuxPV5bQeZtt9eHNNn36rnV4sGq0_sJzitjiFE8s 如果您能解释一下我在C++中做错了什么,或者为什么我的C++实现比Java运行得慢,那将不胜感激!
编辑:按建议修改程序,使其不实际执行乘法,而只是读取数组并打印出一个数字。以下是性能结果。C++程序的IO速度较慢,但仅占差异的一部分。
$ time ./IOonly.exe < ../Testing/2000.in
7
944
real    0m8.158s
user    0m0.000s
sys     0m0.046s

$ time java IOOnly < ../Testing/2000.in
7
944
real    0m1.461s
user    0m0.000s
sys     0m0.047s

1
你实际上测量了这两种情况下仅加载文件所需的时间吗? - Mats Petersson
刚刚完成了。将在上方发布结果。 - vancan1ty
在我的机器上,使用g++ 4.9.2编译的版本运行需要约27秒。其中96%的时间用于执行“matMul”。使用几周前的“clang++” v3.8只需要8.5秒。 - Mats Petersson
我知道这两个程序都没有被优化。我只是想更多地了解这些编程语言的性能模型(例如竞赛编程等)。 - vancan1ty
1
这可能看起来是一个愚蠢的问题,但你总是先检查C++程序,然后再检查Java程序吗?这可能会导致输入文件在Java程序运行时被缓存。 - The Dark
显示剩余4条评论
2个回答

4
我无法分析Java的执行,因为它创建了一个临时可执行模块,在被“使用”后会消失。不过,我认为它确实执行了SSE指令来获得速度[或者如果禁用了SSE指令,那么它会展开循环,就像clang++一样]。
但是,通过g++(4.9.2)和clang++编译,我可以清楚地看到,clang将循环优化为使用SSE指令,而gcc则没有。因此生成的代码要慢4倍。将代码更改为在每个维度中使用2000的常量值[因此编译器“知道”高度和宽度的尺寸]后,gcc编译器也会生成需要大约8秒(在我的机器上)的代码,与“变量”值相比需要27秒[在这里clang编译的代码稍微快一些,但我认为在噪声范围内]。
总的结论:编译器的质量/聪明程度将极大地影响紧密循环的性能。代码越复杂和多样化,C++解决方案生成更好的代码的可能性就越大,而简单易于编译的问题在Java代码中很可能更好[作为规则,但不能保证]。我预计java编译器使用分析来确定循环的次数。
编辑: 时间的结果可以用于确定读取文件是否需要很长时间,但是您需要某种分析工具来确定实际输入是否使用了大量CPU时间等。
Java引擎使用“即时编译器” ,它使用分析来确定特定代码被命中的次数(您也可以在C++中这样做,大型项目经常会这样做!),从而使它能够展开循环,或者在运行时确定循环中的迭代次数。考虑到此代码执行了2000*2000*2000个循环,并且在知道值的大小时,C++编译器实际上做得更好,这告诉我们Java运行时初始情况下并没有做得更好(至少不是最初的),只是它随着时间的推移改进了性能。
不幸的是,由于Java运行时的工作方式,它不会留下二进制代码,因此我无法真正分析它的操作方式。
关键在于您正在进行的实际操作很简单,逻辑也很简单,只是有很多操作而已,而且您正在使用一个微不足道的实现。例如,Java和C++都将受益于手动展开循环。

Mats,感谢您的回答。我按照您建议的进行了修改——将高度和宽度硬编码为2000——在这种情况下,C++在我的电脑上运行8秒,比原来快了4倍。看起来这只是编译器实现的问题。为什么Java在简单循环方面表现更好,而C++在复杂循环方面表现更好呢? - vancan1ty
实际上,通过展开循环,即使关闭SSE指令,clang在这个特定基准测试中也表现得比gcc更好,最终速度大约快3倍(clang++为9.2秒,g++编译的代码为26.9秒)【不包括常量2000值】。 - Mats Petersson
我之前的评论说在我的机器上只需要8秒,这是不正确的。我忘记加回矩阵乘法了,我把它删除以测试IO时间。当我将数字设为常量时,使用GCC确实可以获得很好的加速效果,只需要21秒。 - vancan1ty
哦,以我机器上的情况来看,“真实”时间与“用户”和“系统”时间之和相匹配,意味着没有时间丢失。你的主目录是在网络驱动器或其他类似的地方吗? - Mats Petersson
我认为“real”、“user”和“sys”时间不能用来衡量将矩阵读入程序并存储在内存中所需的时间。如果您注释掉矩阵乘法,这个C++程序在我的电脑上大约需要8秒钟。 - vancan1ty
显示剩余2条评论

2

C++并不比Java默认更快

C++作为一种语言的速度很快,但是一旦你将库加入到混合中,你就会受制于这些库的速度。

标准主要是为了性能而建立的。标准库是以设计和正确性为重点编写的。

C++给了你优化的机会!
如果你对标准库的性能不满意,你可以使用自己优化过的版本。

例如,标准C++ IO对象在设计上非常美观(流、本地化、facet、内部缓冲区),但这使它们在性能方面表现糟糕。 如果你正在为Windows操作系统编写程序,你可以使用ReadFileWriteConsole作为IO机制。
如果你切换到这些函数而不是标准库,你的程序的性能将比Java高几个数量级。


1
除了这个问题与库的速度无关(至少在我的机器上不是),执行代码所需的时间超过90%是矩阵乘法,大约7%是从文件中读取整数(剩下的3%在“各种其他难以确定其用途的小函数”中,包括一些处理内存管理的操作系统内核部分,但您可以预期在运行时间为8秒且分配了许多兆字节数据空间的某些代码中出现此类情况)。 - Mats Petersson
我不认为C++标准在性能方面很糟糕。由于没有虚函数,因此会进行极端的编译器优化。 - Chameleon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接