簡體   English   中英

我可以在java中連續分配對象嗎?

[英]Can I allocate objects contiguously in java?

假設我有一大堆相對較小的對象,我需要經常迭代。
我想通過提高緩存性能來優化我的迭代,所以我想在內存上連續分配對象 [而不是引用],這樣我就可以減少緩存未命中數,並且整體性能可能會更好。

在C ++中,我可以只分配一個對象數組,它會按照我的意願分配它們,但在java中 - 在分配數組時,我只分配引用,並且一次只分配一個對象。

我知道如果我“一次性”分配對象[一個接一個],jvm 最有可能將對象分配為盡可能連續,但如果內存是碎片的話可能還不夠。

我的問題:

  1. 有沒有辦法告訴jvm在開始分配對象之前對內存進行碎片整理? 是否足以確保[盡可能]連續分配對象?
  2. 這個問題有不同的解決方案嗎?

新對象正在伊甸園空間中創建。 伊甸園空間永遠不會分散。 GC后它總是空的。

你遇到的問題是當執行GC時,對象可以隨機排列在內存中,甚至可以按照相反的順序排列。

解決方法是將字段存儲為一系列數組。 我稱之為基於列的表而不是基於行的表。

而不是寫作

class PointCount {
    double x, y;
    int count;
}

PointCount[] pc = new lots of small objects.

使用基於列的數據類型。

class PointCounts {
    double[] xs, ys;
    int[] counts;
}

要么

class PointCounts {
    TDoubleArrayList xs, ys;
    TIntArrayList counts;
}

陣列本身可以在多達三個不同的位置,但數據總是連續的。 如果您對字段子集執行操作,這甚至可以稍微提高效率。

public int totalCount() {
   int sum = 0;
   // counts are continuous without anything between the values.
   for(int i: counts) sum += i;
   return i;
}

我使用的解決方案是避免GC開銷,因為大量數據是使用接口來訪問直接或內存映射的ByteBuffer

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class MyCounters {
    public static void main(String... args) {
        Runtime rt = Runtime.getRuntime();
        long used1 = rt.totalMemory() - rt.freeMemory();
        long start = System.nanoTime();
        int length = 100 * 1000 * 1000;
        PointCount pc = new PointCountImpl(length);
        for (int i = 0; i < length; i++) {
            pc.index(i);
            pc.setX(i);
            pc.setY(-i);
            pc.setCount(1);
        }
        for (int i = 0; i < length; i++) {
            pc.index(i);
            if (pc.getX() != i) throw new AssertionError();
            if (pc.getY() != -i) throw new AssertionError();
            if (pc.getCount() != 1) throw new AssertionError();
        }
        long time = System.nanoTime() - start;
        long used2 = rt.totalMemory() - rt.freeMemory();
        System.out.printf("Creating an array of %,d used %,d bytes of heap and tool %.1f seconds to set and get%n",
                length, (used2 - used1), time / 1e9);
    }
}

interface PointCount {
    // set the index of the element referred to.
    public void index(int index);

    public double getX();

    public void setX(double x);

    public double getY();

    public void setY(double y);

    public int getCount();

    public void setCount(int count);

    public void incrementCount();
}

class PointCountImpl implements PointCount {
    static final int X_OFFSET = 0;
    static final int Y_OFFSET = X_OFFSET + 8;
    static final int COUNT_OFFSET = Y_OFFSET + 8;
    static final int LENGTH = COUNT_OFFSET + 4;

    final ByteBuffer buffer;
    int start = 0;

    PointCountImpl(int count) {
        this(ByteBuffer.allocateDirect(count * LENGTH).order(ByteOrder.nativeOrder()));
    }

    PointCountImpl(ByteBuffer buffer) {
        this.buffer = buffer;
    }

    @Override
    public void index(int index) {
        start = index * LENGTH;
    }

    @Override
    public double getX() {
        return buffer.getDouble(start + X_OFFSET);
    }

    @Override
    public void setX(double x) {
        buffer.putDouble(start + X_OFFSET, x);
    }

    @Override
    public double getY() {
        return buffer.getDouble(start + Y_OFFSET);
    }

    @Override
    public void setY(double y) {
        buffer.putDouble(start + Y_OFFSET, y);
    }

    @Override
    public int getCount() {
        return buffer.getInt(start + COUNT_OFFSET);
    }

    @Override
    public void setCount(int count) {
        buffer.putInt(start + COUNT_OFFSET, count);
    }

    @Override
    public void incrementCount() {
        setCount(getCount() + 1);
    }
}

使用-XX:-UseTLAB運行-XX:-UseTLAB選項(以獲得准確的內存分配大小)打印

創建一個100,000,000的數組,使用了12,512個字節的堆,並花了1.8秒來設置和獲取

作為它的off堆,它幾乎沒有GC影響。

遺憾的是,沒有辦法確保在Java中創建/停留在相鄰的內存位置。

但是,按順序創建的對象很可能最終彼此相鄰(當然這取決於實際的VM實現)。 我很確定虛擬機的編寫者知道地方性是非常需要的,並且不會隨意散布隨機散布的對象。

垃圾收集器在某些時候可能會移動對象 - 如果您的對象是短暫的,那應該不是問題。 對於長壽命對象,它取決於GC如何實現移動幸存者對象。 再一次,我認為編寫GC的人已經在這個問題上花了一些心思,並且會以一種不會使局部性變得不可避免的方式執行復制。

顯然沒有任何上述假設的保證,但由於我們無論如何都無法做任何事情,所以不要擔心:)

你可以在java源代碼級別做的唯一事情是有時避免對象的組合 - 而是你可以“內聯”你通常放在復合對象中的狀態:

class MyThing {
    int myVar;
    // ... more members

    // composite object
    Rectangle bounds;
}

代替:

class MyThing {
    int myVar;
    // ... more members

    // "inlined" rectangle
    int x, y, width, height;
}

當然,這會使代碼的可讀性降低,並且可能會復制大量代碼。

通過訪問模式對類成員進行排序似乎有輕微的影響(我注意到在重新排序某些聲明后,基准代碼片段略有改動),但我從未打擾過驗證它是否真實。 但如果虛擬機沒有對成員進行重新排序,那將是有意義的。

在同一主題上,(從性能視圖)能夠將現有基本數組重新解釋為另一種類型(例如,將int []轉換為float [])也是很好的。 雖然你在這里,為什么不為工會成員呢? 我確定。 但我們必須放棄許多平台和架構獨立性來換取這些可能性。

在Java中不起作用。 迭代不是增加指針的問題。 根據物理存儲對象的堆上的位置沒有性能影響。

如果您仍希望以C / C ++方式處理此問題,請將Java數組視為指向結構的指針數組。 循環遍歷數組時,分配實際結構的位置並不重要,而是循環遍歷指針數組。

我會放棄這種推理。 這不是Java的工作方式,也是次優化。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM