Java чистое существование большого массива вызывает огромные скачки памяти - PullRequest
3 голосов
/ 30 апреля 2020

Я сейчас пишу шахматный движок UCI. Внутри моего движка у меня есть большая таблица транспонирования, которая отображает ключи на значения.

При реализации этого я уже имел в виду память и не хотел создавать новые объекты при помещении чего-либо в таблицу, поэтому я заполнил вся таблица с пустыми объектами для начала.

Код выглядит следующим образом:

private TranspositionEntry[] entries;
private int size;

private int maxSize;
private long hashMask;

public TranspositionTable(int keyBits){
    this.maxSize = (int)(Math.pow(2, keyBits));
    this.hashMask =  maxSize - 1;
    this.entries =  new TranspositionEntry[maxSize];
    for(int i = 0; i < maxSize; i++){
        this.entries[i] = new TranspositionEntry(0,0,0,0,0, new Move(0, 0, 0, 0));
    }
}

private int index(long zobrist){
    return (int)(zobrist & hashMask);
}

public boolean contains(long key){
    int index = index(key);
    return entries[index].getZobrist() != 0;
}

public void clear(){
    for(int i = 0; i < maxSize; i++){
        this.entries[i].setZobrist(0);
    }
}

public int size(){
    return size;
}

public TranspositionEntry get(long key){
    System.out.println("I was called");
    int index = index(key);
    if(entries[index].getZobrist() == 0) return null;
    return entries[index];
}

public void put(long key, double eval, int depthLeft, int node_tpe, int color, Move move){
    int index = index(key);

    System.out.println("I was called");

    TranspositionEntry en = entries[index];

    if(en.getZobrist() == 0) size++;

    en.setVal(eval);
    en.setDepthLeft(depthLeft);
    en.setNode_type(node_tpe);
    en.setColor(color);

    en.getBestMove().setType(move.getType());
    en.getBestMove().setFrom(move.getFrom());
    en.getBestMove().setTo(move.getTo());
    en.getBestMove().setPieceFrom(move.getPieceFrom());
    en.getBestMove().setPieceTo(move.getPieceTo());
}

Это очень очень простой c алгоритм хеширования, но он не имеет отношения к этому вопросу , Я проверил, сколько памяти занимает этот объект, запустив этот код:

InstrumentationAgent.printMemoryOverview();
TranspositionTable<TranspositionEntry> entryTranspositionTable = new TranspositionTable<>(20);
InstrumentationAgent.printMemoryOverview();
System.out.println(entryTranspositionTable);

, который дал мне следующий вывод:

Used Memory   : 7 MB
Free Memory   : 483 MB
Total Memory  : 491 MB
Max Memory    : 7268 MB

Used Memory   : 100 MB
Free Memory   : 390 MB
Total Memory  : 491 MB
Max Memory    : 7268 MB

ai.tools.transpositions.TranspositionTable@1b6d3586

Так что, как вы можете видеть, сама эта таблица потребляет около 100MB памяти.


Теперь перейдем к интересной части:

Когда я запускаю свой код для поиска лучшего хода для данной позиции, Я обычно сначала создаю таблицу транспонирования (или очищаю ее, если она уже существует). Но я отключил ALL доступ к таблице транспозиции, кроме size(). Как вы можете видеть выше, в таблице есть get/put методы с print -составлением, и они не вызываются в любое время.

VisualVM выдает мне этот вывод памяти при поиске позиции, где транспонирование Таблица была создана при запуске, но не используется в любое время.

enter image description here

Как видите, использование памяти очень высокое для начала с и сходится к довольно низкому уровню. Сначала я думал, что сам поиск потребляет столько памяти в начале (что не имеет смысла). Поэтому я запустил точно такой же код , за исключением this._transpositionTable = new TranspositionTable(20);

Вывод выглядит так:

enter image description here

Как видите, сам поиск делает НЕ причиной этих скачков памяти.

Поэтому мои вопросы таковы:

  1. Почему существует чистое существование неиспользуемого массива вызывая эти всплески памяти и особенно только в начале кода
  2. Есть ли решение этой проблемы?

Это очень важно для меня, чтобы решить эту проблему, потому что при тестировании, Мне нужно запустить много двигателей одновременно, поэтому проблема с памятью. Я очень очень рад за любую помощь или совет!

Привет, Финн

РЕДАКТИРОВАТЬ 1:

Добавление кода TranspositionEntry:

private double val;

private long zobrist;

private int depthLeft;

private int node_type;
private int color;
private Move bestMove;


public TranspositionEntry(long zobrist, double val, int depthLeft, int node_type, int color, Move bestMove) {
    this.val = val;
    this.zobrist = zobrist;
    this.depthLeft = depthLeft;
    this.node_type = node_type;
    this.color = color;
    this.bestMove = bestMove;
}

РЕДАКТИРОВАТЬ 2

Я нашел решение этой проблемы. Пики, кажется, не происходят, если максимальное пространство кучи ограничено для начала. Я добавил xmx1024M, и это, кажется, решить проблему.

1 Ответ

2 голосов
/ 30 апреля 2020

Если огромная часть вашей таблицы не заполнена (скажем, 80%), вы должны создавать (и удалять) записи по требованию для экономии памяти. Объекты в Java существуют независимо от того, ссылаются они на них в массиве (или другом объекте) или нет, до тех пор, пока сборщик мусора не очистит их, после того как последняя неслабая ссылка была очищена и последний поток потерял доступ к объекту. , И даже в этом случае сборщику мусора может потребоваться много времени для очистки объекта, в зависимости от его настройки. Прежде всего, JVM не хочет возвращать память, когда-то выделенную, потому что перераспределение это, как правило, дорого. Однако все вышеперечисленное зависит от реализации.

Конкретное предложение, чтобы заполнить вашу таблицу, написать метод ensureEntry, чтобы создать объект в вашей таблице по требованию, поэтому вам не нужно его инициализировать, при запуске:

public TranspositionEntry ensureEntry(int index) {
    TranspositionEntry entry = this.entries[index];
    if (entry == null) {
        entry = new TranspositionEntry(0, 0, 0, 0, 0, new Move(0, 0, 0, 0));
        this.entries[index] = entry;
    }
    return entry;
}

Такой метод чрезвычайно легок в отношении времени выполнения, потому что компилятор, скорее всего, его встроит, и он многое сделает для решения проблемы с памятью. Просто используйте его для доступа к вашей таблице.

Если вам нужно снова удалить записи из таблицы, то напишите аналогичный метод для записи в вашу таблицу, а если «нулевой регистр» равен true, то установите индекс массива до null. Не бойтесь null! Если все сделано правильно, нулевые ссылки ваши друзья. Только не забывайте, что они есть.

И обращаясь к вопросу «Почему пустой массив занимает место?»

Массив объектов (в отличие от массива примитивов). типы) выделяет достаточно памяти vm для хранения всех ссылок, которые она может содержать, но не места для реальных объектов. Так что если вы создаете массив для хранения 100 Strings, то ваш массив принимает размер in-memomy своих собственных накладных расходов + 100 ссылок на объекты (есть ли они там или нет). Однако размер ссылки равен ОС и ВМ c и может варьироваться, вам нужно присвоить не менее 32 бит и (обычно) не более 64 бит на ссылку объекта. Таким образом, массив String размера 100 выделяет 100 * 64 + служебных битов пространства.

...