【问题标题】:Is Java ArrayList really this much slower than C++ vector?Java ArrayList 真的比 C++ 向量慢这么多吗?
【发布时间】:2023-04-10 10:10:02
【问题描述】:

我不想再引发一场关于 Java 还是 C++ 是总体上更好的语言的无意义的争论。我想知道我为特定任务所做的比较是否公平,测量的数据是否正确。

我们需要决定下一个项目是使用 Java 还是 C++。我在 C++ 阵营,但我想为我的案例提供可靠的论据。我们的应用比较特殊,有以下需求:

  • 程序必须运行得相当快并且内存效率相当高。我们不关心最后 20% 的表现。但是,10 倍的性能差异是无法展示的。
  • 我们有很多阵列。我们预先不知道它们的大小。因此,数组可以在平均 O(1) 的运行时间内增长,这一点很重要。
  • 数组中的元素由少量基本数据类型组成。典型的例子是整数或浮点数的元组。
  • 数组可能会变大。 10^6 个元素是标准的。我们有包含 10^7 个元素的应用程序,支持 10^8 会很棒。

我用 C++ 和 Java 实现了一个玩具程序。首先,我介绍一下 C++ 版本:

#include <iostream>
#include <vector>
#include <cstdlib>
using namespace std;

struct Point{
        float x, y;
};

int main(int argc, char*argv[]){
        int n = atoi(argv[1]);

        vector<Point>arr;

        for(int i=0; i<n; ++i){
                Point p;
                p.x = i;
                p.y = i+0.5f;
                arr.push_back(p);
        }

        float dotp = 0;
        for(int i=0; i<n; ++i)
                dotp += arr[i].x * arr[i].y;

        cout << dotp << endl;
}

接下来是做同样事情的Java版本:

import java.util.*;

class Point{
        public float x, y;
}

class Main{
        static public void main(String[]args){
                int n = Integer.parseInt(args[0]);

                ArrayList<Point> arr = new ArrayList<Point>();

                for(int i=0; i<n; ++i){
                        Point p = new Point();
                        p.x = i;
                        p.y = i+0.5f;
                        arr.add(p);
                }

                float dotp = 0;
                for(int i=0; i<n; ++i)
                        dotp += arr.get(i).x * arr.get(i).y;

                System.out.println(dotp);
        }
}

我使用命令行将元素的数量传递给程序,以防止优化器在编译期间执行程序。计算的值没有用。唯一有趣的问题是程序运行速度有多快以及它们使用了多少内存。我从 C++ 开始:

$ g++ --version
g++ (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4
$ g++ -O3 test.cpp -o test
$ /usr/bin/time ./test 1000000
3.33381e+17
0.01user 0.00system 0:00.02elapsed 100%CPU (0avgtext+0avgdata 10084maxresident)k
0inputs+0outputs (0major+2348minor)pagefaults 0swaps
$ /usr/bin/time ./test 10000000
3.36984e+20
0.08user 0.01system 0:00.09elapsed 100%CPU (0avgtext+0avgdata 134380maxresident)k
0inputs+0outputs (0major+4074minor)pagefaults 0swaps
$ /usr/bin/time ./test 100000000
2.42876e+23
0.77user 0.09system 0:00.87elapsed 99%CPU (0avgtext+0avgdata 1050400maxresident)k
0inputs+0outputs (0major+6540minor)pagefaults 0swaps

“用户”时间是程序运行的时间。对于 10^6 个元素,它运行了 0.01 秒,对于 10^7 个元素为 0.08 秒,对于 10^8 个元素为 0.77 秒。 “maxresident”是内核给程序的物理内存量,以千字节为单位。 10^6 为 10 MB,10^7 为 132 MB,10^8 为 1 GB。

内存消耗听起来不错。具有 x 个元素的数组需要 sizeof(float)*2*x=8*x 字节的内存。 10^6 元素大约 8MB,10^7 大约 76MB,10^8 大约 762MB。

接下来,我运行 Java 程序:

$ javac -version
javac 1.6.0_41
$ javac Main.java
$ java -version
java version "1.7.0_131"
OpenJDK Runtime Environment (IcedTea 2.6.9) (7u131-2.6.9-0ubuntu0.14.04.2)
OpenJDK 64-Bit Server VM (build 24.131-b00, mixed mode)
$ /usr/bin/time java Main 1000000
3.33381168E17
0.16user 0.00system 0:00.09elapsed 173%CPU (0avgtext+0avgdata 79828maxresident)k
0inputs+64outputs (0major+4314minor)pagefaults 0swaps
$ /usr/bin/time java Main 10000000
3.3698438E20
5.23user 0.18system 0:02.07elapsed 261%CPU (0avgtext+0avgdata 424180maxresident)k
0inputs+64outputs (0major+13508minor)pagefaults 0swaps
$ /usr/bin/time java Main 100000000
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at Main.main(Main.java:14)
Command exited with non-zero status 1
3840.72user 13.06system 17:11.79elapsed 373%CPU (0avgtext+0avgdata 2281416maxresident)k
0inputs+1408outputs (0major+139893minor)pagefaults 0swaps

对于 10^6 个元素,它需要 0.16 秒和 78 MB。对于 10^7 个元素,它需要 5.23 秒和 414 MB。我试图为 10^8 个元素运行该程序,但 Java 崩溃了。它使用了我机器的所有内核(在一个顺序程序中!)并运行了 17 分钟,同时占用了 2.2GB。我的机器有 8 GB 内存。

对于 10^6 个元素,C++ 的速度是 0.16 / 0.01 = 16 倍,并且需要 78/10 = 7.8 倍的内存。对于 10^7 个元素,C++ 的速度是 5.23/0.08 = 65 倍,并且需要 414/132 = 3.1 倍的内存。 Java 没有在具有 10^8 个元素的测试实例上完成,而 C++ 程序在不到一秒的时间内完成。

对于 10^6,Java 似乎易于管理,但并不理想。对于 10^7 和 10^8,这是绝对不行的。我预计 C++ 相对于 Java 有轻微的性能优势,但不会有这么大的优势。

最可能的解释是我的测试方法有误,或者我的 Java 代码存在不明显的性能瓶颈。另一种解释是 OpenJDK JVM 明显落后于其他供应商的 JVM。

请向我解释为什么 Java 在这个基准测试中表现如此糟糕。我是如何无意中让 Java 看起来比实际更糟糕的?

谢谢

【问题讨论】:

  • 使 C++ 版本快得多的一件事是 Point 对象直接存储在向量的堆分配中,而在 Java 版本中,每个 Point对象需要完全独立的堆分配。我建议使用带有struct Point(不是class Point)和List&lt;Point&gt; 的C# 进行另一个基准测试,因为这更接近于C++ 代码中发生的情况——Point 对象将被分配在单个数组中作为单独的堆对象。
  • 这里一个非常明显和很大的区别是vector&lt;Point&gt; 只是一堆点,而ArrayList&lt;Point&gt; 是一堆对点的引用。您可以使用int[]/float[]/etc(仅使用原语数组)来模拟直接数据,但当然您必须手动增长它..
  • @TrevorHart 不,这根本不是真的。一个好的 JIT 可以在许多工作流中击败静态编译的代码。 C++ 提供控制 性能,而不是性能。甚至配置文件引导的优化仍然是静态编译,并且仅针对某些参数进行优化,因为 JIT 可以即时适应不同的工作流程。
  • @KlitosKyriacou 这就是“摊销”的意思。因为向量的增长因子,它仍然是常数时间。
  • 是的,对大型结构类对象的大型数组的高性能需求是 imo 最弱的 Java 用例(以及任何其他不支持内部结构数组的语言)。是的,vector&lt;unique_ptr&lt;Point&gt;&gt; 产生了一个类似于 Java 的内部 C++ 代表,但关键是你不能在 Java 中描述 vector&lt;Point&gt; 的等价物。在其他领域,Java 有优势,但如果这个用例对您的应用程序至关重要,那么您应该很容易赢得争论。

标签: java c++ arrays performance arraylist


【解决方案1】:

JIT 未运行因此解释了一小部分影响,但不是主要的减速。

是的,Java 的启动速度很慢,因为 JIT,它需要一些时间才能全速运行。

但你描述的表现是灾难性的,还有另一个原因:你写的

它使用了我机器的所有内核(在一个顺序程序中!)

这一定是垃圾收集器。 GC 运行困难意味着您的内存不足。在我的机器上,时间是

  28.689 millis for 1 M pairs
 143.104 millis for 10 M pairs
3100.856 millis for 100 M pairs
  10.404 millis for 1 M pairs
 113.054 millis for 10 M pairs
2528.371 millis for 100 M pairs

这仍然很痛苦,但可能是一个可用的起点。观察到第二次运行速度更快,因为它得到了更好的优化。请注意,这不是 Java 基准测试的编写方式!


内存消耗的原因是您有一个List对象引用,其中包含两个浮点数,而不是一对浮点数的vector。每个引用都会增加 4 或 8 个字节的开销,每个对象都会增加更多。此外,每次访问都有一个间接访问。

如果内存很重要,那么这不是用 Java 编写代码的正确方法。肯定有更好的方法(我试了一下),但代码可能会变得丑陋。没有value types 的Java 很讨厌这样的计算。恕我直言,它几乎在其他任何地方都表现出色(个人意见)。

等效的 C++ 代码将使用 vector&lt;Point*&gt;。如果你这样做,你的代码会变得更慢并且内存更大,但仍然比 Java 好(对象头开销)。


我重写了代码,因此它使用PointArray 将两个彼此相邻的浮点数存储在一个数组中。在没有测量任何东西的情况下,我声称内存消耗现在大致相同。现在是时候了

  31.505 millis for 1 M pairs
 232.658 millis for 10 M pairs
1870.664 millis for 100 M pairs
  17.536 millis for 1 M pairs
 219.222 millis for 10 M pairs
1757.475 millis for 100 M pairs

还是太慢了。我猜,这是绑定检查,在 Java 中无法关闭(除非您解析为 Unsafe)。通常,JIT(即时编译器)可以将它们移出循环,这使得它们的成本可以忽略不计。

这也可能是我的缓慢(因子 1.5)数组调整大小(IIRC vector 使用因子 2)。或者,当您快要完成时调整数组的大小时,您会感到遗憾。由于您没有对数组做任何事情,因此它可能很重。

无论如何,当您想要快速处理基元数组时,您至少需要一名优秀的 Java 程序员。他们可能需要一两天的时间才能获得良好的表现。一个好的图书馆也可以。在 Java 10(或 11,或...)之前使用 List&lt;Point&gt; 效率太低了。

【讨论】:

  • 我运行了一个 JMH 基准测试,其中我使用了一个专门的 PointList,它仅将组件存储为 floats,并且在 n=10^7 上得到了 102 毫秒。不过,带有 g++ 6.3.0 的 C++ 仍然获胜,只有 0ms。值类型在这种情况下非常有用。
  • 如果你想进一步惩罚 C++ —— 或者让测试更相似,如果你愿意 —— 然后给 Point 类一个 vtable 以及(添加一个空的虚拟析构函数会做)。
  • @JornVernee 0ms 听起来像是死代码优化。或者使用一个有趣的公式来计算结果。
  • 如果他用 java 和 C++ 进行相同的测试时内存不足,这也是他决策过程中需要注意的重要事项。
  • 调优 JVM 是运行任何 Java 应用程序的关键部分......当他们说“一次编写,随处运行”时,他们忘记告诉你。您正在寻找 -Xms -Xmx 这是可以指定为 123M 或 123G 的起始内存和最大内存。当您知道它将立即增长到某个基线大小时,增加起始内存有助于摆脱打嗝。还要确保您使用的是 64 位 java 运行时。
猜你喜欢
  • 2012-10-10
  • 2011-07-16
  • 2021-09-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-03-13
相关资源
最近更新 更多