5 августа 2009 г.

JStackAlloc

Пока изучал JBullet наткнулся там на интересную вещь. Автор довольно прямолинейно портировал С-шный Bullet, и наткнулся на проблемы с производительностью из-за большого количества создаваемых объектов типа Vector3d, Matrix4d и тому подобных легковесных "value objects" -- в С++ они, понятно, создавались на стеке. В итоге он сделал финт ушами -- создал небольшую библиотечку JStackAlloc, которая эмулирует выделение объектов на стеке для джавы.

В коде мы пишем такую штуку:
public static Vector3f average(Vector3f v1, Vector3f v2, Vector3f out) {
     out.add(v1, v2);
     out.scale(0.5f);
     return out;
}

public static void test() {
     Vector3f v1 = Stack.alloc(Vector3f.class);
     v1.set(0f, 1f, 2f);

     Vector3f v2 = Stack.alloc(v1);
     v2.x = 10f;

     Vector3f avg = average(v1, v2, Stack.alloc(Vector3f.class));
}


* This source code was highlighted with Source Code Highlighter.


после чего над скомпилированным кодом выполняется небольшой ant-task, который, используя ASM модифицирует байт-код класса, превращая его в что-то вроде

public static void test() {
     $Stack stack = $Stack.get();
     stack.pushVector3f();
     try {
         Vector3f v1 = stack.getVector3f();
         v1.set(0f, 1f, 2f);

         Vector3f v2 = stack.getVector3f(v1);
         v2.x = 10f;

         Vector3f avg = average(v1, v2, stack.getVector3f());
     }
     finally {
         stack.popVector3f();
     }
}


* This source code was highlighted with Source Code Highlighter.


Внутри $Stack для каждого потока (ThreadLocal) заводится обычный стек для каждого типа объектов. В общем-то ничего особо нового, просто самые тупые ручные операции (не забыть на каждый alloc сделать push в начале метода, pop в конце, обернуть в finally), кроме того порядком загрязняющие код делаются в автоматическом режиме на стадии компиляции, и в исходном коде не заметны. По словам Джезека (автора) он таким образом сумел получить приемлемую производительность для своей библиотеки.

Интересно это потому, что я, честно говоря, давно уже отказался от мысли делать пулы мелких объектов ради облегчения жизни GC. Еще несколько лет назад, когда был введен generation GC, сановские инженеры писали, что делать такого не следует, мол, аллокация в джаве и так быстрее некуда. По сути, куча в джаве почти всегда "стековая" -- т.е. она растет только вверх, все новые объекты в Эдеме выделяются подряд, один за другим, пока свободное место в Эдеме не закончится -- и тогда запускается GC. Т.е. стоимость аллокации - это, грубо говоря, стоимость изменения указателя на "вершину" кучи. А сборка мусора для короткоживущих объектов очень дешева -- как раз потому, что ее специально оптимизировали под этот случай. А вот если вы используете пулы, то хранимые в них объекты переживают несколько "короткоживущих" поколений, и попадают в "долгоживущее" поколение, что плохо потому, что над ним уже работает "честный" но тормозной сборщик мусора, и чем оно больше -- тем дольше он будет работать. Они меня убедили, и я не использовал пулы для экономии памяти. А вот оказывается, что это может дать какой-то эффект.

С другой стороны, в 1.6.0_14 появилась экспериментальная поддержка escape analysis, который должен разбираться как раз вот с такими вот случаями временных объектов в рамках одного метода. Так что, скорее всего, библиотечка немного запоздала. Ее бы во времена 1.5...

5 комментариев:

  1. Полагаю, проблема в том, что слишком большое количество этих мелких объектов переживают тот момент, когда заканчивается память в Eden space. Хорошо, что этот JStackAlloc почти элегантно решает проблему, но было бы любопытно потестировать зависимость производительности от размера Eden. Или попробовать вставить ручной его сброс по завершении "блока" операций с мелкими объектами, если это возможно.

    ОтветитьУдалить
  2. Ну, сейчас я бы во-первых убедился бы, что код без пула все еще работает медленно -- потому что escape analyze допилили к 7-ке, и вполне возможно, часть проблем ушла бы сама. В-вторы -- поиграться с настройками GC действительно очень даже опция. Но тут проблема в том, что JBullet это библиотека, а не приложение -- возможно, оптимальные для нее настройки GC будут не оптимальны для другой библиотеки в составе того же приложения... Хороший тон все-таки, чтобы библиотека работала приемлемо при стандартных настройках GC.

    Мне лично видится, что это проблема именно переписывания Сишного кода на джаве. То есть этот JStackAlloc я бы воспринимал как некий хелпер для транслятора C++ -> Java. Я почти уверен, что, пишись эта библиотека изначально на джаве, человеком, аналогичным по уровню подготовки авторам Bullet -- проблемы бы не было бы изначально, ибо код изначально писался бы держа в уме возможности и особенности выделения и сборки памяти в джаве.

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

    ОтветитьУдалить
  3. Отличный ответ, не могу не согласиться.

    ОтветитьУдалить
  4. Ссылка на скачивание JStackAlloc указанная в начале статьи не рабочая. Где можно скачать JStackAlloc?

    ОтветитьУдалить
    Ответы
    1. Поиск по сайту jvm-gaming.org дает вот этот тред https://jvm-gaming.org/t/jstackalloc-stack-allocation-of-value-objects-in-java/31983. К сожалению ссылки на архив, которые там есть, действительно не работают (хотя ссылки на джавадок все еще работают).

      Честно говоря, сейчас уже немного поздно для откапывания стюардессы: с одной стороны есть уже low pause GC типа шенанды, с другой -- records/value types уже буквально перед носом.

      Удалить