查看: 198|回复: 22

C++ vs Java 的一些性能对比

[复制链接]

5

主题

9

帖子

18

积分

新手上路

Rank: 1

积分
18
发表于 2023-6-30 12:45:49 | 显示全部楼层 |阅读模式
声明: 本文主要用于揭示 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次,取时间最短值,时间包括整个进程的生命周期(启动和预热的影响几乎可忽略)
<hr/>测试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++是持平的.
<hr/>测试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++的动态分派在链接时可以确定有哪些分派而做了些偏静态分派的优化.
<hr/>测试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;
                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++更适合专业级别的人员使用.
<hr/>测试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测试程序中的类型和流程基本一致, 如有异议请在评论中不吝指出.
回复

使用道具 举报

2

主题

7

帖子

8

积分

新手上路

Rank: 1

积分
8
发表于 2023-6-30 12:46:18 | 显示全部楼层
测试3中,可以附带一个简单的C++内存池来比较,预料不错的话,带上池子的C++还是比java慢些
回复

使用道具 举报

2

主题

11

帖子

13

积分

新手上路

Rank: 1

积分
13
发表于 2023-6-30 12:46:46 | 显示全部楼层
嗯, 我在解释里也说明了, 这个测试只是参考就好, 实际遇到这样的需求肯定要用内存池改造的, 当然Java也可以用内存池, 如果只是比两个内存池的性能感觉就没什么意义了.
回复

使用道具 举报

1

主题

6

帖子

10

积分

新手上路

Rank: 1

积分
10
发表于 2023-6-30 12:47:45 | 显示全部楼层
额,你可能误会了,其实我的意思是,这个case的差距并不是内存池导致的
回复

使用道具 举报

8

主题

16

帖子

34

积分

新手上路

Rank: 1

积分
34
发表于 2023-6-30 12:47:53 | 显示全部楼层
或者说并不主要是内存池的原因
回复

使用道具 举报

2

主题

9

帖子

10

积分

新手上路

Rank: 1

积分
10
发表于 2023-6-30 12:48:39 | 显示全部楼层
我知道你的意思, C++的默认分配和释放是精确时机的, 跟Java的内存管理不一样, 相比之下Java更像已有了一个"内存池", 只是没有精确释放时机所以性能高. 这样看的话, 这个测试对比就感觉有点不公平, 因此只用来参考一下默认new的差别就好.
回复

使用道具 举报

1

主题

6

帖子

6

积分

新手上路

Rank: 1

积分
6
发表于 2023-6-30 12:49:28 | 显示全部楼层
c++ 如果需要频繁分配和销毁内存导致性能降低,需要重载 new 和 delete 操作符,或完全由自己接管操作系统的内存分配和释放,c++最方便的是哪里慢优化哪里,可以精确控制。
回复

使用道具 举报

3

主题

8

帖子

12

积分

新手上路

Rank: 1

积分
12
发表于 2023-6-30 12:49:59 | 显示全部楼层
测试2a里C++这么慢我是没想到,按道理只是一次间接跳转啊。不知道是CPU分支预测跟不上还是没有内联导致函数调用开销太大……
回复

使用道具 举报

1

主题

7

帖子

10

积分

新手上路

Rank: 1

积分
10
发表于 2023-6-30 12:50:27 | 显示全部楼层
我觉得 C++ 优势之一就是可以尽量减少类对象的动态分配。用接近 Java 的方式去写反而变得复杂了。
回复

使用道具 举报

0

主题

7

帖子

9

积分

新手上路

Rank: 1

积分
9
发表于 2023-6-30 12:51:15 | 显示全部楼层
嗯...主要是C++有各种优化和折腾的潜力, 只要不缺时间精力.
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表