有没有办法创建一个未初始化的基本数据类型数组?

12

众所周知,Java在创建数组时总是会进行初始化。即new int[1000000]始终返回一个所有元素都等于0的数组。我了解这对于对象数组来说是必要的,但对于原始数组(除了可能是布尔类型)而言,在大多数情况下我们并不关心初始值。

有人知道如何避免这种初始化吗?


4
他希望它的工作方式类似于 C 语言,在那里,你需要自己初始化内存,否则就会出现垃圾值(即内存中存在的任何值)。Java 避免了这种情况,因为数组实际上是一个对象而不仅仅是一些连续的内存,因此在分配数组时会对内存进行初始化。 - Brian Roach
2
@EvgeniyDorofeev - 不,它不是。在Java中,数组是堆上的对象,而不仅仅是指向堆/栈上某些内存的指针。这只是不同的东西。 - Brian Roach
7
如果你立即写入数组内容,JVM可能已经足够智能,注意到这一点并省略了初始化,这并不会让我感到惊讶。 - Louis Wasserman
1
只需创建一个大数组,使用 java.util.Arrays.fill(char[] a, char val) 将其填充为 0,并测量时间,以发现初始化时间与将数组填充为有意义数据所需的时间相比微不足道。 - Alexei Kaigorodov
NettyIO在这个问题上有一些信息。他们的I/O速率非常高(见Twitter等),所以归零操作会产生显著的吞吐量影响。他们找到了一种使用sun.misc.Unsafe创建自己的内存缓冲区的方法,这样就不会有效地"memset(0)"。如果您有疯狂的性能要求,请阅读更多相关信息。 - kevinarpe
显示剩余6条评论
4个回答

19

我进行了一些调查。在Java中没有合法的方法可以创建未初始化的数组。即使JNI NewXxxArray也会创建已初始化的数组。因此,无法准确知道数组清零的成本。尽管如此,我进行了一些测量:

1)创建1000个字节数组,其大小不同。

        long t0 = System.currentTimeMillis();
        for(int i = 0; i < 1000; i++) {
//          byte[] a1 = new byte[1];
            byte[] a1 = new byte[1000000];
        }
        System.out.println(System.currentTimeMillis() - t0);

在我的电脑上,byte[1]只需要<1毫秒,而byte [1000000]大约需要500毫秒。听起来很不错。

2)我们在JDK中没有一个快速(本地)填充数组的方法,Arrays.fill 太慢了,所以让我们至少看一下使用本地System.arraycopy复制1000个1000000大小的数组需要多长时间。

    byte[] a1 = new byte[1000000];
    byte[] a2 = new byte[1000000];
    for(int i = 0; i < 1000; i++) {
        System.arraycopy(a1, 0, a2, 0, 1000000);
    }

它是700毫秒。

这让我相信a)创建长数组很昂贵,b)它似乎很昂贵是因为无用的初始化。

3)让我们看看sun.misc.Unsafe http://www.javasourcecode.org/html/open-source/jdk/jdk-6u23/sun/misc/Unsafe.html。它受到外部使用的保护,但不是太多。

    Field f = Unsafe.class.getDeclaredField("theUnsafe");
    f.setAccessible(true);
    Unsafe unsafe = (Unsafe)f.get(null);

这里是内存分配测试的成本

    for(int i = 0; i < 1000; i++) {
        long m = u.allocateMemory(1000000);
    }

如果您还记得,创建new byte [1000000]只需要不到1毫秒,但它需要500毫秒。

4)Unsafe没有直接处理数组的方法。 它需要知道类字段,但是反射在数组中不显示任何字段。 关于数组内部的信息并不多,我猜它取决于JVM /平台。 尽管如此,它像任何其他Java对象一样,包括头+字段。 在我的PC / JVM上,它看起来像

header - 8 bytes
int length - 4 bytes
long bufferAddress - 8 bytes

现在,我将使用Unsafe创建byte[10],分配一个10字节的内存缓冲区,并将其用作我的数组元素:

    byte[] a = new byte[10];
    System.out.println(Arrays.toString(a));
    long mem = unsafe.allocateMemory(10);
    unsafe.putLong(a, 12, mem);
    System.out.println(Arrays.toString(a));

它会打印

[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[8, 15, -114, 24, 0, 0, 0, 0, 0, 0]
您可以看到数组的数据未被初始化。 现在我将更改数组的长度(尽管它仍指向10个字节的内存)。
    unsafe.putInt(a, 8, 1000000);
    System.out.println(a.length);

它显示1000000。这只是为了证明这个想法可行。

现在进行性能测试。我将创建一个空的字节数组a1,分配一个1000000字节的缓冲区,将此缓冲区分配给a1,并设置a1.length = 10000000。

    long t0 = System.currentTimeMillis();
    for(int i = 0; i < 1000; i++) {
        byte[] a1 = new byte[0];
        long mem1 = unsafe.allocateMemory(1000000);
        unsafe.putLong(a1, 12, mem);
        unsafe.putInt(a1, 8, 1000000);
    }
    System.out.println(System.currentTimeMillis() - t0);

它需要10毫秒。

5) C++中有malloc和alloc两种分配内存的方式,malloc只是分配内存块,而calloc会将其初始化为0。

cpp

...
JNIEXPORT void JNICALL Java_Test_malloc(JNIEnv *env, jobject obj, jint n) {
     malloc(n);
} 

Java

private native static void malloc(int n);

for (int i = 0; i < 500; i++) {
    malloc(1000000);
}

结果 malloc - 78 毫秒; calloc - 468 毫秒

结论

  1. 似乎Java数组的创建速度很慢是因为无用的元素清零。
  2. 我们不能改变它,但是Oracle可以。 不需要在JLS中更改任何内容,只需向java.lang.reflect.Array添加本机方法,如

    public static native xxx[] newUninitialziedXxxArray(int size);

对于所有的原始数值类型(byte- double)和char类型都可以使用它。 它可以在整个JDK中使用,像在java.util.Arrays中一样。

    public static int[] copyOf(int[] original, int newLength) {
        int[] copy = Array.newUninitializedIntArray(newLength);
        System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
        ...

或者 java.lang.String

   public String concat(String str) {
        ...   
        char[] buf = Array.newUninitializedCharArray(count + otherLen);
        getChars(0, count, buf, 0);
        ...

也许这只是事后诸葛亮,但这个答案除了毫无价值的测量和错误的结论外,几乎没有什么内容。一些要点:没有预热,使用currentTimeMillis测量纳秒事件,将allocate的返回值(即块地址)放入数组中,并得出结论它是“未初始化”的... - Marko Topolnik
1
在英特尔上,块初始化和复制被积极优化,JIT(依赖于SIMD指令)积极优化数组初始化。对于大块的吞吐量约为30 GB/s(或者当我在Core i7上测量时是这样的)。这意味着您失去了1微秒来初始化30,000字节 - 有时很重要,但通常不是。它绝对限制了专用优化的有用性范围。 - Marko Topolnik

4

我将把这个问题转移到答案中,因为它可能应该是这样。

在Java中,“Array”不是你想象的那样。它不仅仅是指向栈或堆上连续内存块的指针。

在Java中,数组是一个对象,就像其他所有东西(除了原始类型)一样,它位于堆上。当您调用new int [100000]时,您正在创建一个新对象,就像每个其他对象一样,并进行初始化等操作。

JLS提供了有关此的所有具体信息:

http://docs.oracle.com/javase/specs/jls/se5.0/html/arrays.html

所以,不。你不能避免“初始化”数组。这就是Java的工作方式。没有未初始化的堆内存;许多人称其为“特性”,因为它防止您访问未初始化的内存。

2

Java 9实际上通过jdk.internal.misc.Unsafe.allocateUninitializedArray方法开始暴露这一点。它实际上需要JDK.Unsupported模块声明


0

我可以想象,在某些数据结构或算法中,新建int[n]的O(n)成本可能是一种负担。

在Java中,实现一个大小为n的原始数组的摊销O(1)内存分配成本的方法是使用对象池或其他策略回收已分配的数组。回收的数组可以被视为下一次分配时的“未初始化”状态。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接