这是一段用C++写的计算十万以内的回文素数算法。
#include
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<
}
}
}
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,已经尽可能关掉所有后台应用,在我看来是相当稳定的测试环境。)上面进行测试。用来测试的软件有两个在手机上运行的IDEAIDE (用来编译Java代码)和CIDE (用来编译C++代码,编译器为aarch64的gcc7.2)。
由于在CIDE无法显示程序执行时间,因此这次在C++代码也加入了计时。
#include
#include
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
#include
#include
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++。
想知道原因。