Докуметация Cтарт Статьи Форум Лента Вход
Не официальное русскоязычное сообщество
Главная
    Статьи
        Переводы с других сайтов
            LWJGL Вики
                1.3 FAQ Память

1.3 FAQ Память

Опубликованно: 23.07.2018, 21:39
Последняя редакция, Andry: 09.08.2018 6:33
Эта страница обязательна для всех пользователей LWJGL.

Почему LWJGL использует буферы так много?

LWJGL требует использования памяти вне-кучи при передаче данных в родные библиотеки. Аналогично, любые буферы, возвращаемые из родных библиотек, всегда возвращаются в память вне-кучи. Это не ограничение LWJGL. Есть две проблемы с Java объектами и массивами, которые живут в куче JVM:

  • Невозможно управлять компоновкой Java объектов. Различные JVM и различные настройки JVM создают очень разные компоновки полей. С другой стороны, родные библиотеки ожидают данные с очень точно определенными компоновками.
  • Любой Java объект или массив может быть перемещен GC в любое время, одновременно с выполнением вызова родного метода. Все методы JNI выполняются из безопасной точки(safepoint), поэтому по определению не должны обращаться к данным кучи.

Стандартный подход:

  1. Использование JNI функций для доступа к Java объектам, что очень медленно.
  2. Использование JNI функций для «привязки» Java массивов (Get/ReleasePrimitiveArrayCritical или Hotspot Critical Natives), что также неэффективен по нескольким причинам.

С другой стороны, LWJGL предназначен для использования на прямую (вне-кучи) с java.nio буфер классами для передачи данных в и из родного кода. ByteBuffer и другие классы не являются наилучшей из возможных абстракцией для данных вне-кучи, и их API не идеален, но это единственный официально поддерживаемый способ доступа к данным вне-кучи на Java.

Самый простой способ того как стоит рассматривать ByteBuffer, это рассматривать его как обертку поверх родного C указателя, плюс длина массива (buffer.capacity()). LWJGL сопоставляет примитивные типы C с соответствующим классом в java.nio. Массивы указателей сопоставляются в классе org.lwjgl.PointerBuffer. Указатели на структуры сопоставляются с соответствующим структуре классом. Указатели на структуры массивов сопоставляются с соответствующим <StructClass>.Buffer классом. PointerBuffer и структура Buffer классов имеют API, очень похожий на java.nio буферы.

Какой java.nio.ByteOrder следует использовать?

Порядок байтов буфера должен быть задан в ByteOrder.nativeOrder(). Это в основном требуется для правильного кросс-платформенного поведения. Это также приводит к лучшей производительности.

Все экземпляры буфера, созданные LWJGL, всегда задаются в родном порядке байтов.

Как распределить и освободить буферы?

После ознакомления с приведенными выше сопоставлениями, следующим шагом будет научиться обрабатывать распределение таких буферов. Это критическая проблема, и LWJGL предлагает несколько вариантов. Варианты перечисленные ниже, из более-менее эффективных. Каждый раз, когда вы принимаете решение о том, как обрабатывать распределение, вы должны рассмотреть первый вариант. Если это неприменимо, тогда рассмотрите второй вариант и так далее.

1. Распределение стека

Java не поддерживает явное распределение стека из Java объектов, и очевидно не поддерживает распределение стека вне-кучи. В C это очень просто: вы объявляете переменную внутри функции и распределяете это стек. Когда функция возвращает, память переменной автоматически утилизируется (и без накладных расходов). На Java этому нет эквивалента.

Точно так же в Java нельзя использовать родные функции, которые ожидают или возвращает структуру по значению. Такие функции в привязках LWJGL обернуты и отображаются с помощью параметра указатель-к-структуре или возвращаемого значения.

Это проблема, потому что очень часто требуется небольшое, короткоживущее распределение при вызове родных функций. Например, создание объекта вершинного буфера в OpenGL в C:

GLuint vbo;
glGenBuffers(1, &vbo); // очень просто

и с LWJGL:

IntBuffer ip = ...; // здесь нужно 4-byte буфера
glGenBuffers(ip);
int vbo = ip.get(0);

Реальное распределение IntBuffer в приведенном выше примере, независимо от реализации, будет намного более неэффективным, чем указатель стека в эквивалентном C коде.

Обычный ответ на эту проблему в LWJGL 2 и других Java-библиотеках заключается в том, чтобы один раз распределить буфер, кэшировать его и повторно использовать во многих вызовах методов. Это невероятно неудовлетворительное решение:

  • Это приводит к уродливому коду и пустой трате памяти.
  • Чтобы избежать пустой траты памяти, обычно используются статические буферы.
  • Использование статических буферов приводит либо к ошибкам параллелизма, либо к снижению производительности (из-за синхронизации).

Ответ LWJGL 3 это API org.lwjgl.system.MemoryStack. Он был разработан для использования со статическим импортом и блоками try-with-resources. Вышеприведенный пример:

int vbo;
try (MemoryStack stack = stackPush()) {
    IntBuffer ip = stack.callocInt(1);
    glGenBuffers(ip);
    vbo = ip.get(0);
} // стек автоматически выталкивается, ip-память автоматически утилизируется

Это, очевидно, более многословное, но имеет следующие преимущества:

  • Обычно требуется более одного распределения, но шаблон try-with-resources остается неизменным.
  • Семантика вышеуказанного кода полностью соответствует требованиям. Память стека является поточно-локальным, подобно реальному потоку потока С.
  • Производительность идеальна. Push и pop стека это простые bumps указателя, а распределение экземпляра IntBuffer либо устраняется с помощью анализом выхода кода, либо обрабатывается следующим minor/eden GC циклом (супер эффективно).
Примечание 1: Размер стека по умолчанию — 64kb. Его можно изменить с помощью -Dorg.lwjgl.system.stackSize или Configuration.STACK_SIZE.
Примечание 2: Структуры и структура буферов также могут быть распределены на MemoryStack.
Примечание 3. Статический, поточно-локальный API MemoryStack, это просто удобно. Существует дополнительный API, который позволяет создавать и/или использовать экземпляры MemoryStack, как вы сочтете нужным.

2. MemoryUtil (malloc/free)

Иногда распределение стека не может быть использовано. Память, которая должна быть распределена, слишком велика или распределяется долгое время. В таких случаях следующим наилучшим вариантом является явно заданное управление памятью. Либо через API org.lwjgl.system.MemoryUtil, либо конкретным распределителем памяти (в настоящее время в LWJGL доступны: stdlib, jemalloc). Пример:

ByteBuffer buffer = memAlloc(2 * 1024 * 1024); // 2MB
// использовать буфер...
memFree(buffer); // освободить, когда больше не требуется
Примечание 1: Как и в C, пользователь отвечает за освобождение памяти, распределённой с помощью malloc, используя free.
Примечание 2: API для стандартных функций calloc, realloc и aligned_alloc также доступен.
Примечание 3: Объекты Java, распределённые с явно заданными функциями управления памятью, также подлежат избеганию анализу.

3. BufferUtils (ByteBuffer.allocateDirect)

Иногда API явно заданного управления памятью также не может быть использован. Возможно, данное конкретное распределение трудно отслеживать не усложняя код, или возможно не удастся точно узнать, когда оно больше не требуется. Такие случаи являются законными кандидатами на использование org.lwjgl.BufferUtils. Этот класс существовал в более старых версиях LWJGL с тем же API. Он использует ByteBuffer.allocateDirect что бы делать распределения, которые имеют одно важное преимущество: пользователю не нужно явно освобождать память кучи, это делает GC автоматически.

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

  • Он медленный, намного медленнее, чем вызов raw malloc. Много накладных расходов над функцией, которая уже и без того медленная.
  • Он плохо масштабируется при одновременном распределении.
  • Он произвольно ограничивает объем распределямой памяти (-XX:MaxDirectMemorySize).
  • Подобно массивам Java, выделенная память всегда обнуляется. Это не обязательно плохо, но возможность выбора будет лучше.
  • Невозможно освободить выделенную память по требованию (без JQK-специальных рефлексивных хаков). Вместо этого используется reference queue, которая обычно требует двух циклов GC для освобождения родной памяти. Это может привести к ошибкам OOM при нагрузках.
Примером LWJGL, в котором используется BufferUtils, является распределение памяти, которое поддерживает поточно-локальные экземпляры MemoryStack. Это долгое живущее распределение, которое должно быть освобождено, когда поток умирает, поэтому мы позволяем GC позаботиться о нём.
  1. Используйте org.lwjgl.system.MemoryStack, и если это невозможно…
  2. Используйте org.lwjgl.system.MemoryUtil, и если это невозможно…
  3. Использовать org.lwjgl.BufferUtil

Я хотел бы узнать больше, есть ещё что-то для меня?

Да, прочитайте Управление Памятью в блоге LWJGL 3.


Переведено для jmonkeyengine.ru, оригинал
Автор перевода: Andry

Добавить комментарий

jMonkeyEngine.ru © 2017. Все права сохранены.