这是一段用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);
}
}
执行结果:
同样的算法,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的执行速度。(从797到110)
看了一下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++。
想知道原因。
我在学校的老服务器上跑了一遍你给的代码,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,
更多详情日后再说,先扫墓去了以下是测试代码递归求斐波那契数列
测试代码
测试命令
结果
那个prompt是用powerline-shell
反正是要烧CPU,就没用动态规划了
(其实是忘了怎么动态规划了)。更多的5次重复实验
总结
可以初步推断,简单的递归cpp完胜java(除了“简单的”,还需要补充哪些限制,欢迎评论)。
然后我又测试了疯狂swap两个int
反复交换变量值
测试代码
注意:为什么不优化了呢,因为我发现优化的话cpp会偷懒,它知道实际上疯狂swap两个没用的东西又没输出等于啥都没干,所以会直接退出程序(O2优化),或空循环(O1优化)
测试命令
结果
总结
看不太懂ATT风格的汇编代码,关闭了优化之后,cpp编译出来的二进制文件中多出了
str
(store register?)指令,疯狂访问内存势必会大大增长代码执行时间……至于java,用javap -c main
反汇编了一下,确实是有看到iload
和istore
的命令(我不会jvm的汇编,这两句应该就是从内存里加载到寄存器和从寄存器存储到内存的指令吧),看起来为什么java访问内存的速度比cpp快这么多呢,我也不知道哈哈哈哈哈,可能需要更深入地了解java的内存模型之类的,另外这两个指令是不是总是会导致jvm去访问内存呢?还是可能只是会访问寄存器?显然如果是后者,速度必然比前者快得多,然而这个问题我也不知道答案。不会写64位的arm汇编程序,不然我可以试试用汇编疯狂交换两个寄存器的值会不会比java版的还快。
总结
你要是说在都不优化的情况下同等代码java跑得比cpp或c快我是不信的,一个纯半解释执行的程序干得过编译执行的程序我也是不信的。我认为,导致等效的java程序跑得比
记者cpp程序还快的原因应该就是编译器(速度全靠一手编译器)。至于上面的结果,我无可奉告也不知道如何解释,虽然至少递归方面cpp胜出了emmm,才疏学浅,没有办法究其原因又在手机上小测试了一下,一加5T,骁龙835CPU(ARM架构),终端模拟器使用Termux,编译器使用
clang
和ecj
,java虚拟机使用安卓自带的dalvikvm
,开启了jit
,结果java跑了86秒,cpp跑了26秒。编译cpp代码的命令(这里的
g++
实际上还是clang
)编译java代码使用的命令参考这里
运行命令参数加了
-Xusejit:true
,-Xusejit:1
也试过了,结果也是86秒。