Java可能比C++快吗?为什么?

这是一段用C++写的计算十万以内的回文素数算法。

#include <iostream>
using namespace std;
int main()
{
    int input_num=100000;
    int pp_count=0;
    for(int each=2; each<=input_num; each++)
    {
        int factorization_lst=0;
        for(int factor=1; factor<=each; factor++)
            if(each%factor==0&&!(factor>each/factor))
                factorization_lst++;
        if(factorization_lst==1)
        {
            int antitone=0,each_cpy=each;
            while(each_cpy)
            {
                antitone=antitone*10+each_cpy%10;
                each_cpy/=10;
            }
            if(antitone==each)
            {
                pp_count++;
                cout<<pp_count<<':'<<each<<endl;
            }
        }
    }
    return 0;
}

稍微做一下修改的Java版,加了计时相关的部分。

public class main {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        int input_num = 100000;
        int pp_count = 0;
        for (int each = 2; each <= input_num; each++) {
            int factorization_lst = 0;
            for (int factor = 1; factor <= each; factor++)
                if (each % factor == 0 && !(factor > each / factor))
                    factorization_lst++;
            if (factorization_lst == 1) {
                int antitone = 0, each_cpy = each;
                while (each_cpy != 0) {
                    antitone = antitone * 10 + each_cpy % 10;
                    each_cpy /= 10;
                }
                if (antitone == each) {
                    pp_count++;
                    System.out.println(pp_count + ":" + each);
                }
            }
        }
        System.out.println(System.currentTimeMillis() - start);
    }
}

执行结果:
Codeblocks
eclipse
同样的算法,C++用了230s,Java只用了124s。这是为什么呢,不是说C++的速度更快吗?

注:运行环境是树莓派3B的官方raspbian(在我的笔记本上运行过,但仅相差一秒不明显,java17s),C++和Java分别用的默认仓库的codeblocks和eclipse(都不是最新版本,eclipse的版本是2012年的3.8.1,codeblocks是2016年的16.01),gcc已经默认开启了-O2优化选项,但还是如此相差悬殊。已经看过类似于这样的解释文章。但还是不太明白。我的代码只有一个main,没有内联函数。Java编译器难道不也是只分指令集的吗,怎么能够编译出更加优化的字节码呢?而且这段代码,Java还能怎么优化呢?


追加:
按照@Untitled(sf没有艾特的功能吗)的提示,做下一个实验证明JIT对Java执行速度的影响。这次使用命令行直接编译,绕过IDE的影响。个人感觉两分钟仅输出百来行的话IO操作对速度的影响可忽略不计。
(由于这次图片屡次上传失败因此只贴出shell相关操作,加上C++编译结果)

pi@raspberrypi:~/workspace/testjava/src $ javac main.java
pi@raspberrypi:~/workspace/testjava/src $ java main
1:2
# 省略计算输出
113:98689
110494
# 110秒,比在eclipse中执行的速度还快,接下来禁用JIT
pi@raspberrypi:~/workspace/testjava/src $ java -Xint main
1:2
# 省略计算输出
113:98689
797514
# 797秒,明显慢于使用JIT的
pi@raspberrypi:~/workspace/testjava/src $ 
# C++编译
pi@raspberrypi:~/cpplearn $ g++ -o main main.cpp
pi@raspberrypi:~/cpplearn $ time ./main
1:2
# 省略计算输出
113:98689

real    4m5.606s
user    4m5.581s
sys    0m0.000s
#245秒,接下来启用-O2选项
pi@raspberrypi:~/cpplearn $ g++ -O2 -o main main.cpp
pi@raspberrypi:~/cpplearn $ time ./main
1:2
# 省略计算输出
113:98689

real    3m50.631s
user    3m50.384s
sys    0m0.010s
# 230秒,快了一点,和在codeblocks编译的速度差不多
pi@raspberrypi:~/cpplearn $ 

JIT确实是大幅度提升了Java的执行速度。(从797110)

看了一下JIT的相关资料(1,2),感觉就算是这样,也不过就是不经过JVM直接执行了Java代码,这和C++的编译原理不是一样的吗?最多只是持平,怎么还会快这么多呢
其实我不懂怎么反汇编,所以也不知道这怎么回事。我的循环也不是空的。可能的话,我想知道Java的JIT是怎么加快执行这段代码的速度的。


追加:
经过几次实验,发现在x86/x64架构中无论是在Windows还是Linux,实体机还是虚拟机,C++的速度在总体上都比Java更胜一筹。arm的设备我除了树莓派,剩下的只有Android手机了。我准备在一台诺基亚7(骁龙630,4GB,原生Android 8.0,无root,已经尽可能关掉所有后台应用,在我看来是相当稳定的测试环境。)上面进行测试。用来测试的软件有两个在手机上运行的IDE(部署Linux Deploy还是太麻烦了)AIDE (用来编译Java代码)和CIDE (用来编译C++代码,编译器为aarch64的gcc7.2)。

由于在CIDE无法显示程序执行时间,因此这次在C++代码也加入了计时

#include <iostream>
#include <ctime>
using namespace std;
int main()
{
    clock_t start = clock();
    int input_num = 100000;
    int pp_count = 0;
    for (int each = 2; each <= input_num; each++)
    {
        int factorization_lst = 0;
        for (int factor = 1; factor <= each; factor++)
            if (each % factor == 0 && !(factor > each / factor))
                factorization_lst++;
        if (factorization_lst == 1)
        {
            int antitone = 0, each_cpy = each;
            while (each_cpy)
            {
                antitone = antitone * 10 + each_cpy % 10;
                each_cpy /= 10;
            }
            if (antitone == each)
            {
                pp_count++;
                cout << pp_count << ':' << each << endl;
            }
        }
    }
    cout << 1000*(clock() - start) / CLOCKS_PER_SEC;
    return 0;
}

优化选项改成使用-O3(默认为-Os)

执行结果:(这已经是我挑选出来所用时间最短的了)

C++用了43s

Java用了37s

.....
(已经经过多次测试)


追加:
听从Untitled的建议使用clang编译(Raspbian默认没有安装,还得自己apt install clang一下)
速度有了质的飞跃。(但还没越过Java)
不使用优化选项:3m22s(202s)
使用-O2选项:3m05s(185s)(使用-O3与-O2的执行速度是差不多的)

顺带一提,我再次执行java版时去掉计时的那两行代码,

    //long start = System.currentTimeMillis();
    //System.out.println(System.currentTimeMillis() - start);

然后使用time命令计时,结果时间延长了零点几秒...


追加:
今晚身体不适,但还是抽出一点时间写了Android上的测试应用。(下载)
在编写过程中,我已经尽量保证了公平。
因为今晚急着早点休息,暂时未进行充分的测试(但大体上C++比Java快很多)。大家可以自行下载测试一下,晚些时候我再发布一下详细测试结果。

主要代码:

MainActivity.java

package ryuunoakaihitomi.javacppperfcomparison;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.WindowManager;
import android.widget.Toast;

import java.util.Timer;
import java.util.TimerTask;

public class MainActivity extends Activity {

    public static final String TAG = "JCPC";

    static {
        System.loadLibrary("native-lib");
    }

    @SuppressWarnings("ConstantConditions")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        setContentView(R.layout.activity_main);
        getActionBar().setTitle("logcat -s JCPC");
        Log.i(TAG, "Finding palindromic primes within 100,000.(Waiting for 3s)");
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        final long jTime = pcTimer(true);
                        final long cTime = pcTimer(false);
                        runOnUiThread(new Runnable() {
                            @SuppressLint("DefaultLocale")
                            @Override
                            public void run() {
                                Toast.makeText(getApplicationContext(), String.format("java:%d\ncpp:%d", jTime, cTime), Toast.LENGTH_LONG).show();
                                finish();
                            }
                        });
                    }
                }).start();
            }
        }, 3000);
    }

    public native void cpp();

    long pcTimer(boolean isJava) {
        long lStart = System.currentTimeMillis();
        if (isJava)
            Java.kernel();
        else
            cpp();
        long lTime = System.currentTimeMillis() - lStart;
        Log.i(TAG, "total time:" + lTime);
        return lTime;
    }
}

Java.java

package ryuunoakaihitomi.javacppperfcomparison;

public class Java {

    static {
        System.loadLibrary("native-lib");
    }

    static void kernel() {
        int iInputNumber = 100000;
        int iPalprimeCount = 0;
        for (int iEach = 2; iEach <= iInputNumber; iEach++) {
            int iFactorizationList = 0;
            for (int iFactor = 1; iFactor <= iEach; iFactor++)
                if (iEach % iFactor == 0 && !(iFactor > iEach / iFactor))
                    iFactorizationList++;
            if (iFactorizationList == 1) {
                int iAntitone = 0, iEachCopy = iEach;
                while (iEachCopy != 0) {
                    iAntitone = iAntitone * 10 + iEachCopy % 10;
                    iEachCopy /= 10;
                }
                if (iAntitone == iEach) {
                    iPalprimeCount++;
                    ResultPrint(iPalprimeCount, iEach);
                }
            }
        }
    }

    public static native void ResultPrint(int c, int e);
}

native-lib.cpp

#include <jni.h>
#include <android/log.h>
#include <string>

using namespace std;

void kernel();

void kernel_log(string, int, int);

extern "C" JNIEXPORT void

JNICALL
Java_ryuunoakaihitomi_javacppperfcomparison_MainActivity_cpp(
        JNIEnv *,
        jobject /* this */) {
    kernel();
}

void kernel() {
    int input_num = 100000;
    int pp_count = 0;
    for (int each = 2; each <= input_num; each++) {
        int factorization_lst = 0;
        for (int factor = 1; factor <= each; factor++)
            /*Expression can be simplified to 'factor <= each / factor' less... (Ctrl+F1)
            This inspection finds the part of the code that can be simplified, e.g. constant conditions, identical if branches, pointless boolean expressions, etc.*/
            if (each % factor == 0 && factor <= each / factor)
                factorization_lst++;
        if (factorization_lst == 1) {
            int antitone = 0, each_cpy = each;
            while (each_cpy) {
                antitone = antitone * 10 + each_cpy % 10;
                each_cpy /= 10;
            }
            if (antitone == each) {
                pp_count++;
                kernel_log("c", pp_count, each);
            }
        }
    }
}

void kernel_log(string t, int c, int e) {
    __android_log_print(ANDROID_LOG_DEBUG, "JCPC", "%s %d:%d", t.c_str(), c, e);
}

extern "C"
JNIEXPORT void JNICALL
Java_ryuunoakaihitomi_javacppperfcomparison_Java_ResultPrint(JNIEnv *, jobject, jint c,
                                                             jint e) {
    kernel_log("j", c, e);
}

追加:

准备环境:

  • 测试之前已经完全运行过一次
  • 禁用Xposed,暂时冻结了占用后台的应用,电量至少在30%保证稳定供电

实验三次取各自的最小值,实验结果:
说明:表格前四列的值均来自于android.os.Build中对应名称的常量

MODEL MANUFACTURER DISPLAY SDK_INT Java耗时 C++耗时
GT-I9300 samsung lineage_i9300-userdebug 7.1.2 NJH47F 0f9e26b899 25 192169 171928
Redmi 4A Xiaomi NJH47F 25 66009 31907
m2 Meizu Flyme 6.3.0.0A 22 37722 34654
A2 softwinner 升级版四核2G运存 19 239865 202402
Redmi Note 3 Xiaomi OPM1.171019.018 27 22299 18105
TA-1041 HMD Global 00CN_1_34E 26 37310 20234
HTC 802t htc LRX22G release-keys 21 48211 125279

可以看出,绝大多数的arm Android设备运行C++的速度快过Java。但是最后这一行的结果超出了预料。
仅仅是测试之一,读数主要看日志

这个设备的CPU是骁龙600。(好奇怪......)

另:我前两天买了一个香橙派zero plus,用的全志H5。C++45s,java70s

我的所有arm设备已经测试完成,我能不能得到以下结论。

在一小部分的arm指令集架构设备中,Java的运行速度会快于C++。

想知道原因。

阅读 17.7k
8 个回答

我在学校的老服务器上跑了一遍你给的代码,cpp的用系统自带的time计时,java的用程序自己的结果,结果是cpp的25.125s的用户时间,25.130s的墙上时钟时间;java的25.240s的用户时间,25.250s的墙上时钟时间,自称25114毫秒(25.114s)运行结束,感觉好像是差不多的。为了排除I/O操作的影响,我把除了java版的最后一句输出外的cpp和java的所有输出都注释掉再试了一次,结果还是差不多。所以我打算把十万改成一百万,还没跑完,有空再把结果放上来哈哈哈哈哈哈哈哈哈哈哈哈哈

以上是非常临时且不严谨的实验结果,没空做大量的对照实验。我想某些情况下,代码逻辑的完全相同的java程序比cpp程序或c程序快的原因应该就是编译器优化和个别操作实现的差异的原因,除此之外暂时想不到别的原因。不管是什么语言写的程序,实际“运行”的一定是机器指令,cpp和c编译后的正文部分是一堆机器指令序列,java编译后是jvm的机器指令序列(是的吧?),但执行的时候还要jvm去解释字节码,解释执行的时候仍要执行宿主机的机器指令,不过好像还有JIT的说法?(不是很熟悉java)。所以我相信理论上来讲,不使用JIT的话,java肯定是没有cpp和c快的(在“相同的程序”的意义下,但这个“相同”又很难定义,你懂就好),因为java还要“解释”,然后才能“执行”。因此我觉得一定是编译器的锅,但也只是觉得,我手上也没有什么实验结果。

你可以试试能不能把JIT关了,再比较结果?

有兴趣的话可以多试试不同的程序,简单的,复杂的,多种程序对比(记得java好像会自动跳过一些无意义的循环体,比如空转的循环体,这点要注意)。其中简单的程序可以对编译出来的文件进行反汇编,分析理论执行效率,找到导致差异的原因。


试了一下在树莓派3B上跑,结果比题主的慢多了,但java确实比cpp快,用的和楼主一样的代码,cpp跑了4分多钟,java跑了将近2分钟,quite interesting :D。cpp和java均使用了默认编译参数,g++版本是4.9.2,javac版本是1.8.0_65,java版本是1.8.0_65-b17。

然后我又给cpp版的加了-O2参数,时间提升到3分40多秒——还是没java版的快emmmmm

这就比较神奇了,于是我又试了一下别的程序——我自己写了个斐波那契数列求值的——这回舒服了,cpp的1.6s,java的3.4s,更多详情日后再说,先扫墓去了以下是测试代码

递归求斐波那契数列

测试代码

// test.cc
#include <iostream>

using namespace std;

int fibo(int n)
{
    switch(n)
    {
        case 1: return 1;
        case 2: return 1;
        default: return fibo(n - 1) + fibo(n - 2);
    }
}

int main()
{
    cout << fibo(40) << endl;
    return 0;
}
// main.java
public class main {
  private static int fibo(int n) {
    switch(n) {
      case 1: return 1;
      case 2: return 1;
      default: return fibo(n - 1) + fibo(n - 2);
    }
  }

  public static void main(String[] args) {
    long start = System.currentTimeMillis();
    System.out.println(fibo(40));
    System.out.println(System.currentTimeMillis() - start);
  }
}
// Makefile
main: test.cc main.java
    g++ test.cc -o test -O2
    javac main.java
# test.sh
time(./test) 1>cpp.res 2>cpp.time
time(java main) 1>java.res 2>java.time

测试命令

make
./test.sh

结果

clipboard.png

那个prompt是用powerline-shell

反正是要烧CPU,就没用动态规划了(其实是忘了怎么动态规划了)

更多的5次重复实验

总结

可以初步推断,简单的递归cpp完胜java(除了“简单的”,还需要补充哪些限制,欢迎评论)。

然后我又测试了疯狂swap两个int

反复交换变量值

测试代码

// test.cc
#include <iostream>
#include <cstdlib>

using namespace std;


int main(int argc, char *argv[])
{
    int a = 1, b = 2;
    int times = atoi(argv[1]);
    for(int i = 0; i < times; i++)
    {
        a = a + b;
        b = a - b;
        a = a - b;
    }
    return 0;
}
// main.java
public class main {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        int a = 1;
        int b = 2;
        int times = Integer.parseInt(args[0]);
        for(int i = 0; i < times; i++) {
            a = a + b;
            b = a - b;
            a = a - b;
        }
        System.out.println(System.currentTimeMillis() - start);
    }
}
// Makefile
main: test.cc main.java
    g++ test.cc -o test
    javac main.java

注意:为什么不优化了呢,因为我发现优化的话cpp会偷懒,它知道实际上疯狂swap两个没用的东西又没输出等于啥都没干,所以会直接退出程序(O2优化),或空循环(O1优化)

# test.sh
time(./test $1) 2>cpp.time
time(java main $1) 1>java.res 2>java.time
cat cpp.time
cat java.res

测试命令

make
./test.sh 1000
./test.sh 10000
./test.sh 100000
./test.sh 1000000
./test.sh 10000000
./test.sh 100000000
./test.sh 1000000000

结果

clipboard.png

总结

看不太懂ATT风格的汇编代码,关闭了优化之后,cpp编译出来的二进制文件中多出了str(store register?)指令,疯狂访问内存势必会大大增长代码执行时间……至于java,用javap -c main反汇编了一下,确实是有看到iloadistore的命令(我不会jvm的汇编,这两句应该就是从内存里加载到寄存器和从寄存器存储到内存的指令吧),看起来为什么java访问内存的速度比cpp快这么多呢,我也不知道哈哈哈哈哈,可能需要更深入地了解java的内存模型之类的,另外这两个指令是不是总是会导致jvm去访问内存呢?还是可能只是会访问寄存器?显然如果是后者,速度必然比前者快得多,然而这个问题我也不知道答案。

不会写64位的arm汇编程序,不然我可以试试用汇编疯狂交换两个寄存器的值会不会比java版的还快。

总结

你要是说在都不优化的情况下同等代码java跑得比cpp或c快我是不信的,一个半解释执行的程序干得过编译执行的程序我也是不信的。我认为,导致等效的java程序跑得比记者cpp程序还快的原因应该就是编译器(速度全靠一手编译器)。至于上面的结果,我无可奉告也不知道如何解释,虽然至少递归方面cpp胜出了emmm,才疏学浅,没有办法究其原因

技不如人,甘拜下风
ggwp

又在手机上小测试了一下,一加5T,骁龙835CPU(ARM架构),终端模拟器使用Termux,编译器使用clangecj,java虚拟机使用安卓自带的dalvikvm,开启了jit,结果java跑了86秒,cpp跑了26秒。

编译cpp代码的命令(这里的g++实际上还是clang

g++ test.cc -o test -O2

编译java代码使用的命令参考这里

运行命令参数加了-Xusejit:true-Xusejit:1也试过了,结果也是86秒。

不会写 C++ 的人写出来的 C++ 确实可以比 Java 慢,甚至比没有任何优化直接解释执行的 Java 慢。
但是会写 C++ 的人永远可以写出完虐解释执行的 Java 的代码,但是 JIT 的 Java 还是可以比 C++ 快,因为它能做一些依赖运行时才能知道的信息才能做的优化。这也是现在 C++ 编译器进化的方向,一个是加入运行时信息的优化,一个是 link time 优化。

归根结底,比较两个语言的速度是耍流氓,只能比写代码的人的质量和编译器的质量。

题主的问题应该恰是一个运行时掌握的信息能进行更多优化的例子。

帮题主重写了下 C++ 和 Java 的代码:

测试环境:x86_64 架构,C++/LLVM 6.0,Java/HotSpot 18.3

C++ 版用时 0.008s (time 命令)
Java 版用时 0.045340767000000004s (System.nanoTime)

可见 C++ 版算上前后的时间还是比 Java 快一个数量级。

(即便 Java 版在计时前运行同样地代码上万次来预热,多次运行结果也没有变化,因此测量的结果是科学的。)

附代码:

#include <iostream>

using namespace std;

constexpr int N = 100000;
bool C[N + 5];

int main() {
  ios_base::sync_with_stdio(0);
  cin.tie(0);

  int c = 0;

  for (int i = 2; i < N; ++i) {
    if (!C[i]) {
      for (int j = i + i; j < N; j += i) {
        C[j] = true;
      }

      int z = 0;
      for (int k = i; k != 0; ) {
        z *= 10;
        z += k % 10;
        k /= 10;
      }

      if (z == i) {
        cout << ++c << ':' << i << '\n';
      }
    }
  }
}
class Two {

  static final int N = 100000;
  static final boolean[] C = new boolean[N + 5];

  public static void main(String[] args) {
    long start = System.nanoTime();

    int c = 0;

    for (int i = 2; i < N; ++i) {
      if (!C[i]) {
        for (int j = i + i; j < N; j += i) {
          C[j] = true;
        }

        int z = 0;
        for (int k = i; k != 0; ) {
          z *= 10;
          z += k % 10;
          k /= 10;
        }

        if (z == i) {
          System.out.println(++c + ":" + i);
        }
      }
    }

    System.out.println("time: " + (System.nanoTime() - start) * 1e-9 + "s");
  }
}

1) 看下这个https://benchmarksgame-team.p...,这个基本上Cpp无一落败
2) 除了-O2外,还有-O3,还有其他的编译参数,请参见GCC手册
3) JVM默认是用空间换时间的,所以这么对比不是很适合

你的代码,我在未优化一行代码的情况下,使用-O3来测试(实际上release都是-O3),运算3次都比Java快,这中间还包括加载程序启动的时间,怎么得出结论会比java慢呢。

当然,C++对程序员要求很高,不了解内存模型、编译原理什么的,是很难写出高质量的C++的,在这一点上,java就好很多

最近买了一个树莓派3B+,特意跑了下这个程序,从性能上看,C++比java在Arm上略快,优势不明显,另外写了一个rust版本的代码,算法上未优化,性能跟C++接近,在release情况下比Java略快。

fn main() {
    let input_num=100001;
    let mut pp_count =0;
    for  each in 2..input_num {
        let mut factorization_lst=0;
        for  factor in 1..each+1 {
            if each%factor==0 &&!(factor>each/factor) {
                factorization_lst += 1;
            }
        }
        if factorization_lst==1
        {
            let mut antitone =0;
            let mut each_cpy =each;
            while each_cpy != 0
            {
                antitone=antitone*10+each_cpy%10;
                each_cpy/=10;
            }
            if antitone==each
            {
               pp_count += 1;
               println!("{}:{}", pp_count, each);
            }
        }
    }
}

从CPU上来,基本上在运行期这3个程序都是跑满单核的(树莓派3B+有4个core),但内存上来看,C++和rust有明显优势,大概为java版本的1/10.
这个测试从测试结果来看,这几个语言的运行性能差异没那么大,分析了下有几个原因
1) 对于int数据类型,在java C++ rust里都是原生类型,原生的在计算时差别不大,对于java来说,如果是Integer可能有性能损耗。
2) 只跑了一个核,没有多核之间的数据传递等
3) 没有用到递归、值传递、引用传递、值拷贝等特性,差异不大。

结论: java是一个性价比比较好的语言,运行性能上或许不是最优,但开发效率很好,不像其他的语言要考虑跨平台移植问题、编译参数问题等。
PS 未来看好rust

新手上路,请多包涵

测试了下,在我的笔记本上跑,c++21秒(没开O2),java22秒,基本差不多。c++开了O2的话17秒。
可能是树莓派的c++编译器有毒。。。

LeetCode上很多题的最佳解法,java运行的甚至比C快,也一直很疑惑。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题