9 ноября 2011 г.

Задачка с stack overflow

Ways to improve performance consistency -- некий Peter Lawrey -- довольно продвинутый чувак, если судить по его блогу -- написал message-passing штукакенцию, отдаленно напоминающую дизруптор -- в том смысле, что используется что-то вроде массива как носитель, и потоки передают друг другу сообщения, извещая друг друга мембарами. Правда, в качестве массива он использует ByteBuffer.allocateDirect(). Смысл этого для меня пока не очень понятен, но такова уж его идея.

Проблема у мужика в том, что результаты бенчмарков у него скачут примерно вдвое-втрое. И он спрашивает сообщество "WTF, gays?".

Причем его более тщательные исследования намекают, что скачки производительности происходят после срабатывания GC. Это как бы намекает, что дело тут в memory layout-е, который может меняться после дефрагментации памяти в ходе сборки мусора.

С одной стороны, ситуация похожа на ту, что я наблюдал в своих экспериментах. С другой -- в его коде пишутся не ссылки, а просто числа (ByteBuffer только их и принимает) -- а для long[] в качестве носителя у меня результаты вполне себе повторяемые, скачков в разы там нет. Это несколько удивляет.

Моя текущая версия происходящего состоит в том, что Питер использует для привязки к ядрам taskset. Но taskset привязывает процесс целиком. Это значит, что никак не фиксируется, как раскиданы по ядрам потоки внутри процесса. GC запускается, скорее всего, в отдельном потоке -- и создает нифиговую пиковую нагрузку -- при этом в момент его запуска собственно бенчмаркирующие потоки приостановлены (stop-the-world). Очень логично, что в этот момент ОС решедулит потоки, так, что ничего не делающие потоки скидываются на одно ядро, а потоку GC, аццки вкалывающему, выделяется ядро в безраздельное пользование. Соответственно, после того, как сборщик мусора успокоился, ОС почему-то не возвращает потоки назад, на отдельные ядра. Тут возникает вопрос "почему", но мне кажется, что это сценарий реалистичный -- обычно такие важные алгоритмы пишутся довольно консервативно, в духе "нет веских причин -- ничего не трогай". Т.е. если нет веских причин, перебрасывать потоки между ядрами шедулер не будет -- противное может привести к тому, что в каких-то ситуациях ему придется постоянно перекидывать потоки, что, понятно, хреново.

Жду от мужика отзыва -- мои сервера сейчас слишком заняты, чтобы я себе мог позволить погонять бенчмарки самому.

UPD: Питер отписался по результатам. Мое предположение оказалось неверным -- потоки не решедулятся (UPD2: на самом деле, они как раз решедулятся -- но это не оказывает существенного влияния на производительность). Причина оказалась (по его словам) в том, что буфер и счетчики в определенных случаях оказывались лежащими последовательно в памяти. Поскольку паддинг у его счетчиков сделан только после используемого поля value, то буфер, идущий в памяти перед счетчиками может легко заползти на их строку (я писал про такой сценарий в статье про false sharing). Вот и получается фигня всякая...

2 комментария:

  1. А он точно спрашивает "WTF, gays?", а не "WTF, guys?" :)

    Думаю, что он использует ByteBuffer.allocateDirect(), чтобы как раз GC не сказывалось, ведь так он вне хипа будет выделять память. И еще я где-то читал, что доступ в direct memory быстрее нежели в heap, хотя самому мне такое сложно представить. Надо будет microbenchmark на это дело сделать что ли...

    ОтветитьУдалить
  2. Да кто ж скажет точно, что этот нехристь там на самом деле спрашивает? ;)

    С одной стороны да -- мужик автор довольно большой уже либы на google code, которая работает со всякими вариантами off-heap-memory.

    С другой -- мне не очень понятно, как в данном случае выделение буфера вне кучи может уменьшить нагрузку на GC -- ведь буфер-то получается практически перманентным, он быстро застрянет в oldGen, и провоцировать запуск GC точно не будет.
    И мне так же не понятно, как доступ к direct memory может быть быстрее heap. То есть для массивов примитивных типов там ничего более, чем проверки на границы индекса не добавляется -- но проверки будут и с drect memory. C другой же стороны -- накладные расходы на сериализацию сообщений, мне кажется, сами по себе немалые.

    В общем, этот момент мне пока не понятен

    ОтветитьУдалить