声明: 本文主要用于揭示 C++ 和 Java 在某些方面的性能缺陷, 目的在于如何改进和避免这些性能陷阱, 有些结果并不意味着 C++ 的性能很差, 理论上C++有各种高级写法能让任何程序都达到性能最大化, 不可能比Java慢, 不过绝大部分人写C++都达不到这样的层次, 所以这里只以接近Java的普通C++写法来对比. 欢迎理性评论, 不欢迎无脑黑.
前两期传送点:
《Go vs Java 的一些性能对比》见此链接: Go vs Java 的一些性能对比
《C# vs Java 的一些性能对比》见此链接: C# vs Java 的一些性能对比
本期依然测C#那一期的4个方向的微测试(第2个测试由于争议比较大,只测试后来改进的2a版本)
测试系统: Win10 64-bit; Intel I5-4430 (3GHz)
C++版本: mingw gcc 8.2.0 (64-bit) (编译参数: -m64 -Ofast -lto -s)
Java版本: OpenJDK 11.0.1 (64-bit)
测试说明: Java使用默认参数启动,每个程序均运行3次,取时间最短值,时间包括整个进程的生命周期(启动和预热的影响几乎可忽略)
测试1 (频繁递归函数调用)
C++版本:
#include <stdio.h>
long long fib(long long n) {
return n <= 2 ? 1 : fib(n - 1) + fib(n - 2);
}
int main() {
return printf("%lld\n", fib(45));
}
Java版本:
package bench;
public class Bench1 {
static long fib(long n) {
return n <= 2 ? 1 : fib(n - 1) + fib(n - 2);
}
public static void main(String[] args) {
System.out.println(fib(45));
}
}
输出结果: 1134903170
性能结果: C++: 2.4秒 Java: 3.2秒
解释说明: 此测试主要考察函数调用开销以及小函数内联(包括递归内联)的能力. 看起来C++的优化能力很强, Java默认的优化能力不足, 如果加入JVM配置参数”-XX:MaxRecursiveInlineLevel=4″, 结果是2.7秒, 不算JVM启动和JIT预热开销的话, 性能跟C++是持平的.
测试2a (频繁接口调用)
C++版本:
#include <stdio.h>
struct I {
virtual void f() = 0;
virtual long long get() = 0;
};
class S1 : public I {
long long a;
public:
virtual void f() override {
a++;
}
virtual long long get() override {
return a;
}
};
class S2 : public I {
long long a;
public:
virtual void f() override {
a--;
}
virtual long long get() override {
return a;
}
};
int main() {
I* ia[3] = { new S1(), new S2(), new S1() };
for(long long j = 0; j < 1000000000; j++)
ia[j % 3]->f();
return printf("%lld\n", ia[0]->get() + ia[1]->get() + ia[2]->get());
}
Java版本:
package bench;
public class Bench2a {
interface I {
void f();
long get();
}
static class S1 implements I {
long a;
@Override
public void f() {
a++;
}
@Override
public long get() {
return a;
}
}
static class S2 implements I {
long a;
@Override
public void f() {
a--;
}
@Override
public long get() {
return a;
}
}
public static void main(String[] args) {
I[] ia = new I[] { new S1(), new S2(), new S1() };
for(long j = 0; j < 10_0000_0000; j++)
ia[(int)(j % 3)].f();
System.out.println(ia[0].get() + ia[1].get() + ia[2].get());
}
}
输出结果: 333333334
性能结果: C++: 2.0秒 Java: 3.0秒
解释说明: 此测试主要考察接口调用的开销, 比普通调用增加了一些间接性(动态分派). 如果不使用接口,并把循环中的动态分派改为switch手动分派,C++的运行时间就缩短到1.3秒了,Java是2.7秒(不过不太稳,有时会跑成3.2秒). 另外C++如果不带”-lto”编译则需5.9秒, 可见C++的动态分派在链接时可以确定有哪些分派而做了些偏静态分派的优化.
测试3 (频繁内存分配/垃圾回收)
C++版本:
#include <memory.h> #include <stdio.h>
struct E {
int a;
E(int aa) : a(aa) {}
};
int main() {
static const long long ARRAY_COUNT = 10000L;
static const long long TEST_COUNT = ARRAY_COUNT * 100000L;
E** es = new E*[ARRAY_COUNT];
memset(es, 0, sizeof(E*) * ARRAY_COUNT);
for(long long i = 0; i < TEST_COUNT; i++) {
long long idx = i * 123456789L % ARRAY_COUNT;
E* e = es[idx];
if(e)
delete e;
es[idx] = new E((int)i);
}
long long n = 0;
for(long long i = 0; i < ARRAY_COUNT; i++) {
E* e = es[i];
if(e)
n += e->a;
}
return printf("%lld\n", n);
}
Java版本:
package bench;
public class Bench3 {
static class E {
int a;
E(int a) {
this.a = a;
}
}
public static void main(String[] args) {
final long ARRAY_COUNT = 10000L;
final long TEST_COUNT = ARRAY_COUNT * 10_0000L;
E[] es = new E[(int)ARRAY_COUNT];
for(long i = 0; i < TEST_COUNT; i++)
es[(int)(i * 123456789L % ARRAY_COUNT)] = new E((int)i);
long n = 0;
for(long i = 0; i < ARRAY_COUNT; i++) {
E e = es[(int)i];
if(e != null)
n += e.a;
}
System.out.println(n);
}
}
输出结果: 9999949995000
性能结果: C++: 55秒 Java: 11秒
解释说明: 此测试主要考察堆内存的分配/释放和GC的综合吞吐量性能. 这里C++性能低是很正常也容易解释的, JVM的分配和释放机制跟C++完全不同, JVM更像是在内存池中的分配和释放, 而C++的通用内存分配和释放通常性能都不是最佳, 遇到这种需求应该自行实现或引用内存池机制, 性能不会比Java差的, 只是写法比Java复杂一些, 可以看出C++更适合专业级别的人员使用.
测试4 (频繁lambda调用)
C++版本:
#include <stdio.h> #include <functional>
int main() {
long long a = 0;
std::function<void()> ia[3] = {
[&]() { a++; },
[&]() { a--; },
[&]() { a++; },
};
for(long long j = 0; j < 1000000000; j++)
ia[j % 3]();
return printf("%lld\n", a);
}
Java版本:
package bench;
public class Bench4 {
long a;
public static void main(String[] args) {
Bench4 b = new Bench4();
Runnable[] ia = new Runnable[] {
() -> b.a++,
() -> b.a--,
() -> b.a++,
};
for(long j = 0; j < 10_0000_0000; j++)
ia[(int)(j % 3)].run();
System.out.println(b.a);
}
}
输出结果: 333333334
性能结果: C++: 2.6秒 Java: 7.2秒
解释说明: 这次测试凸显了Java的软肋了. C++的lambda调用的性能应该算是正常发挥,与直接调用函数的开销差不多. 而Java的lambda调用开销比较大,而且Java对lambda闭包的支持也不如C++(这就是为什么需要把a包装成对象的原因). Java慢的主要原因尚需进一步分析, 目前多次测试的现象是反复动态分派3个以上同接口的类型就会出现性能降低, 目前怀疑是JVM总是尝试去内联但总是遇到内联预测类型不符导致逆优化的开销.
PS: 考虑到公平性, C++和Java测试程序中的类型和流程基本一致, 如有异议请在评论中不吝指出.