Java Arraylist确实比C 向量慢得多

Is Java ArrayList really this much slower than C++ vector?

本文关键字:向量 Arraylist Java      更新时间:2023-10-16

我不想启动另一个关于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,约762 mb。

接下来,我运行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崩溃了。它使用了我的机器的所有核心(在一个顺序程序中!(,在占用2.2GB的同时跑了17分钟。我的机器有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看起来比现在更糟?

谢谢

未运行的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中对其进行编码的正确方法。肯定有一种更好的方法(我尝试了一下(,但是代码可能会变得丑陋。没有价值类型的Java在此类计算上很糟糕。恕我直言,它几乎在其他任何地方都擅长(个人意见(。

等效的C 代码将使用vector<Point*>。如果执行此操作,您的代码会变慢且内存更大,但仍然比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程序员。他们可能需要一两天才能获得良好的表现。一个好的图书馆也可以做。使用List<Point>在Java 10之前(或11,或...(。