私たちが知っているように、Java は作成時に常に配列を初期化します。つまりnew int[1000000]
、常にすべての要素が 0 の配列を返します。オブジェクト配列では必須であることは理解していますが、プリミティブ配列 (ブール値を除く) ではほとんどの場合、初期値は気にしません。
この初期化を回避する方法を知っている人はいますか?
私はいくつかの調査を行いました。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);
私のPCでは、バイト[1]で1ミリ秒未満、バイト[1000000]で約500ミリ秒になります。私には印象的ですね。
2) JDK には配列を埋めるための高速 (ネイティブ) メソッドがありません。
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);
}
覚えていれば、1ミリ秒未満かかります。新しいバイト[1000000]の場合、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);
10msかかります。
5) C++ には malloc と alloc があり、malloc はメモリ ブロックを割り当てるだけで、calloc もゼロで初期化します。
cpp
...
JNIEXPORT void JNICALL Java_Test_malloc(JNIEnv *env, jobject obj, jint n) {
malloc(n);
}
ジャワ
private native static void malloc(int n);
for (int i = 0; i < 500; i++) {
malloc(1000000);
}
結果 malloc - 78 ミリ秒。calloc - 468 ミリ秒
結論
私たちはそれを変更できませんが、オラクルは変更できます。JLS で何も変更する必要はありません。次のように java.lang.reflect.Array にネイティブ メソッドを追加するだけです。
public static native xxx[] newUninitialziedXxxArray(int サイズ);
すべてのプリミティブ数値型 (byte - double) および char 型。java.util.Arrays のように、JDK 全体で使用できます。
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);
...
おそらくそうあるべきなので、これを答えに移します。
Javaの「配列」は、あなたが思っているものではありません。スタックまたはヒープ上の連続したメモリのチャンクへの単なるポインタではありません。
Java の配列は、他のすべてのもの (プリミティブを除く) と同じようにオブジェクトであり、ヒープ上にあります。呼び出すnew int[100000]
と、他のすべてのオブジェクトと同じように新しいオブジェクトが作成され、初期化されます。
JLS は、これに関するすべての特定の情報を提供します。
http://docs.oracle.com/javase/specs/jls/se5.0/html/arrays.html
いいえ。配列の「初期化」を避けることはできません。それはJavaの仕組みではありません。初期化されていないヒープ メモリなどというものはありません。初期化されていないメモリにアクセスできないようにするため、多くの人がこれを「機能」と呼んでいます。
Java 9 は実際にjdk.internal.misc.Unsafe.allocateUninitializedArray
メソッドを介してこれを公開し始めます。実際には JDK.Unsupported モジュール宣言が必要です。