Честно - мусора будет много, итоговые типы получаются длинными и не читаемыми, но во многих местах, где это имело смысл, подложены static_assert, например, в метод get. Если вы пытаетесь взять из коллекции типов то, что туда не клали - сообщение об ошибке явно об этом скажет и стек трейс вызовов будет вполне собираем.
Намного сложнее разобраться, почему в коллекции нет того типа, который, как вы ожидаете, должен там быть. Такое возможно, если в проекте выстроена сложная цепочка маппингов (активно используете метод transform). Тогда отлаживать нужно шаблоны, которые выводятся на этапе компиляции, а не рантайм с привычным стектрейсом. К счастью clangd-based IDE хорошо раскручивают шаблоны и автодополнение по ним нормально работает. Например, набрав engine.get<FrameBuffer>(). IDE корректно подсветит доступное поле width . Также хорошо помогают юнит-тесты.
Все это плата за перенос рантайм ошибок на этап компиляции. Например, если DI не подтянул какой-то сервис в рантайме, то вы узнаете об этом только собрав приложение и запустив его, потом просидите с дебаггером разбираясь, почему DI не отработал как нужно. С предложенным подходом в статье ошибка проявится во время сборки и разбор проблем с кодом просто перейдет в другую плоскость.
Это особенность реализации consteval интерпретатора GCC (часть рефлексии связана с consteval), который выделяет память пропорционально объёму вычислений и не освобождает выделенную память до конца сборки, Clang работает по-другому: потребляет константную память на consteval вычислениях. Видимо, архитектурные особенности.
Перепроверил: замер был действительно на рефлексии, а не провалился на шаблоны, тесты тоже все проходят
Спасибо за замечание, я действительно упустил, что GCC 16 уже поддерживает рефлексию. Добавил UPD с замерами и пуш в проект с поддержкой GCC. Эти замеры, конечно, подрывают посыл статьи, но, надеюсь, добавляют ценности))
1) Еще нет такой информации, но вообще гипервизор должен давать не существенный оверхед, проблема скорее в том, что железо шарится между несколькими виртуалками
2) В 2 раза не пробовали увеличить и возможности такой пока нет, но мы исходим из того, что при текущем allocation rate = 10 MB/sec, свободных 4-х GB должно хватать. Мы возлагаем надежды на Shenandoah, возможно поможет, если из коробки не поможет, будем тюнить его как сможем
>> почему бы тогда не поднять несколько экземпляров сервиса и не пусть трафик через балансировщик
мы примерно так и делаем, есть несколько одинаковых расчетных сервисов, на уровне нашего клиента (джарника с grpc) принимается решение, к какому инстансу подключиться
Спасибо большое за примеры! Очень полезное дополнение.
>> а вы его обозвали недостатком!
я скорее про сравнение с синхронным кодом:
res1 = calc1()
res2 = calc2()
sendResult(res1 + res2)
В случае если переписать на сообщения, то код станет чуть размазаннее, хотя для вашего примера согласен, что акторы удобнее.
Я правильно понимаю, что в вашем коде акторы создаются динамически, на разные состояния (во втором примере)? Существенный ли оверхед на это? (мы стараемся акторы динамически не создавать и кешировать их)
Я специально в статье привел пример CalculationActor, т.к. в нем есть код по обработке входящих сообщений в createReceive() и onCalculate, и отправки результата в том же onCalculate методом sender.tell.
Код клиента можно посмотреть здесь — в нем отличие в методе preStart, где планируется отправка сообщений каждые 100 ms (эмулирется нагрузка и непрерывная работа).
>> реактивность от которой отказались, как я понял, именно в угоду читаемости.
я бы сказал, что «не перешли», а не «отказались».
Shenandoah очень хотим попробовать, но у нас есть ограничения по версии jdk, которую можем использовать в продакшене, мы пока на 11, как только появится возможность перейти на 14 Java или выше, обязательно попробуем.
Zgc нет не пробовали.
С GraalVm, в бесплатной версии не оптимально сделано выполнение simd вычислений, что для нас важно, а платную версию не было возможности протестировать.
Кстати, если вам интересна тема ухищрений на Java для высокопроизводительных вычислений, то возможно будет интересен доклад https://youtu.be/QV-ue1YMdds
>> 6 раз независимо друг от друга
Да, правильно, расчеты независимые. 1,5 ms на один расчет. И мы, конечно же, выполняем эти расчеты параллельно.
>> Для параллельного вычисления изначально и задействовали akka. Так?
Сами расчеты, два вложенных цикла, как раз то место, где akka не используется. Итерации по циклам разбиваются на батчи и обсчитываются параллельно на ForkJoinPool. На выходе получается 0,3 ms вместо 1,5. Почему не akka? Это как раз тот кейс, где акка не очень удобна. Нужно через сообщения выстраивать машину состояний и реагировать в зависимости от посчитанных значений, ForkJoinPool в этом месте удобнее.
>> Или есть какие-то другие веские причины, чтобы использовать akka?
Не очень понял, вопрос относится непосредственно к расчетам или к проекту в целом? Про расчеты написал выше, а про проект, почему акка написано в разделе «Преимущества акторов»
Спасибо за отзыв
1. Конкретно Caffeine явно не используем. В описании к нему написано, что он используется в Akka и Spring. Если так, тогда используем неявно. А вообще, вместо разрозненных кешей мы используем спапшет данных. Снапшет — это уже преобразованные, предагрегированные данные и разложенные по обычным HashMap для быстрого поиска. Снапшет меняется только несколько раз в день, поэтому там достаточно обычных HashMap. Real-time данные раскладываем в ConcurrentHashMap
2. Нет, напрямую off-heap memory не используем. Для экономии на GC используем пулы объектов, про это напишу ниже. Статья как раз о том, что без сильных ухищрений с GC от Java можно получить хорошую производительность. Лично мое мнение, использование напрямую off-heap memory не через фреймворки и библиотеки очень опасно для проекта, начиная с того, что Unsafe рано или поздно задеприкейтят и заканчивая core дампами в продакшене. Если в проекте много мест где хочется использовать Unsafe, тогда, скорее всего, Java не лучший выбор для проекта, возможно стоит рассмотреть связку С/С++ + JNI или вынести высокопроизводительный код в отдельный процесс на том же С/С++.
3. Да используем. В расчетах с матрицами. Каждая строка матрицы это массив и эти массивы переиспользуются в расчетах. Для кеширования используем очередь из JCTools. Потоки берут преаллоцированные массивы из этой очереди и кладут их обратно после завершения вычислений.
4. Скорее особенность скиллсета разработчиков. На скалу переходить не планировали, связка Akka + Java вполне устраивает.
Немного обобщу свои ответы: в нашем проекте мы особо не экономим на аллокациях небольших объектов и позволяем гарбедж коллектору их собирать, кешируем только массивы. Работаем с данными либо через большой снапшет, либо через небольшие кеши поверх ConcurrentHashMap. Не прибегая к особым ухищрениям мы сохраняем для себя удобство разработки на Java, но жертвуем тем, что обслуживание части запросов все-таки выпадает на GC паузы (не больше 5%) и обслуживаются не за 3-4 ms а за 150-200 ms. Для нас это осознанный tradeoff.
Честно - мусора будет много, итоговые типы получаются длинными и не читаемыми, но во многих местах, где это имело смысл, подложены static_assert, например, в метод get. Если вы пытаетесь взять из коллекции типов то, что туда не клали - сообщение об ошибке явно об этом скажет и стек трейс вызовов будет вполне собираем.
Намного сложнее разобраться, почему в коллекции нет того типа, который, как вы ожидаете, должен там быть. Такое возможно, если в проекте выстроена сложная цепочка маппингов (активно используете метод
transform). Тогда отлаживать нужно шаблоны, которые выводятся на этапе компиляции, а не рантайм с привычным стектрейсом. К счастью clangd-based IDE хорошо раскручивают шаблоны и автодополнение по ним нормально работает. Например, набравengine.get<FrameBuffer>().IDE корректно подсветит доступное полеwidth. Также хорошо помогают юнит-тесты.Все это плата за перенос рантайм ошибок на этап компиляции. Например, если DI не подтянул какой-то сервис в рантайме, то вы узнаете об этом только собрав приложение и запустив его, потом просидите с дебаггером разбираясь, почему DI не отработал как нужно. С предложенным подходом в статье ошибка проявится во время сборки и разбор проблем с кодом просто перейдет в другую плоскость.
Это особенность реализации consteval интерпретатора GCC (часть рефлексии связана с consteval), который выделяет память пропорционально объёму вычислений и не освобождает выделенную память до конца сборки, Clang работает по-другому: потребляет константную память на consteval вычислениях. Видимо, архитектурные особенности.
Перепроверил: замер был действительно на рефлексии, а не провалился на шаблоны, тесты тоже все проходят
Это личный проект, не перевод.
Спасибо за замечание, я действительно упустил, что GCC 16 уже поддерживает рефлексию. Добавил UPD с замерами и пуш в проект с поддержкой GCC. Эти замеры, конечно, подрывают посыл статьи, но, надеюсь, добавляют ценности))
2) В 2 раза не пробовали увеличить и возможности такой пока нет, но мы исходим из того, что при текущем allocation rate = 10 MB/sec, свободных 4-х GB должно хватать. Мы возлагаем надежды на Shenandoah, возможно поможет, если из коробки не поможет, будем тюнить его как сможем
мы примерно так и делаем, есть несколько одинаковых расчетных сервисов, на уровне нашего клиента (джарника с grpc) принимается решение, к какому инстансу подключиться
>> а вы его обозвали недостатком!
я скорее про сравнение с синхронным кодом:
res1 = calc1()
res2 = calc2()
sendResult(res1 + res2)
В случае если переписать на сообщения, то код станет чуть размазаннее, хотя для вашего примера согласен, что акторы удобнее.
Я правильно понимаю, что в вашем коде акторы создаются динамически, на разные состояния (во втором примере)? Существенный ли оверхед на это? (мы стараемся акторы динамически не создавать и кешировать их)
Код клиента можно посмотреть здесь — в нем отличие в методе preStart, где планируется отправка сообщений каждые 100 ms (эмулирется нагрузка и непрерывная работа).
>> реактивность от которой отказались, как я понял, именно в угоду читаемости.
я бы сказал, что «не перешли», а не «отказались».
2) Где-то 16GB живых объектов, в основном это статические данные для расчетов.
Если уточните для каких целей спрашиваете, может быть смогу более развернуто ответить
Zgc нет не пробовали.
Кстати, если вам интересна тема ухищрений на Java для высокопроизводительных вычислений, то возможно будет интересен доклад https://youtu.be/QV-ue1YMdds
Да, правильно, расчеты независимые. 1,5 ms на один расчет. И мы, конечно же, выполняем эти расчеты параллельно.
>> Для параллельного вычисления изначально и задействовали akka. Так?
Сами расчеты, два вложенных цикла, как раз то место, где akka не используется. Итерации по циклам разбиваются на батчи и обсчитываются параллельно на ForkJoinPool. На выходе получается 0,3 ms вместо 1,5. Почему не akka? Это как раз тот кейс, где акка не очень удобна. Нужно через сообщения выстраивать машину состояний и реагировать в зависимости от посчитанных значений, ForkJoinPool в этом месте удобнее.
>> Или есть какие-то другие веские причины, чтобы использовать akka?
Не очень понял, вопрос относится непосредственно к расчетам или к проекту в целом? Про расчеты написал выше, а про проект, почему акка написано в разделе «Преимущества акторов»
1. Конкретно Caffeine явно не используем. В описании к нему написано, что он используется в Akka и Spring. Если так, тогда используем неявно. А вообще, вместо разрозненных кешей мы используем спапшет данных. Снапшет — это уже преобразованные, предагрегированные данные и разложенные по обычным HashMap для быстрого поиска. Снапшет меняется только несколько раз в день, поэтому там достаточно обычных HashMap. Real-time данные раскладываем в ConcurrentHashMap
2. Нет, напрямую off-heap memory не используем. Для экономии на GC используем пулы объектов, про это напишу ниже. Статья как раз о том, что без сильных ухищрений с GC от Java можно получить хорошую производительность. Лично мое мнение, использование напрямую off-heap memory не через фреймворки и библиотеки очень опасно для проекта, начиная с того, что Unsafe рано или поздно задеприкейтят и заканчивая core дампами в продакшене. Если в проекте много мест где хочется использовать Unsafe, тогда, скорее всего, Java не лучший выбор для проекта, возможно стоит рассмотреть связку С/С++ + JNI или вынести высокопроизводительный код в отдельный процесс на том же С/С++.
3. Да используем. В расчетах с матрицами. Каждая строка матрицы это массив и эти массивы переиспользуются в расчетах. Для кеширования используем очередь из JCTools. Потоки берут преаллоцированные массивы из этой очереди и кладут их обратно после завершения вычислений.
4. Скорее особенность скиллсета разработчиков. На скалу переходить не планировали, связка Akka + Java вполне устраивает.
Немного обобщу свои ответы: в нашем проекте мы особо не экономим на аллокациях небольших объектов и позволяем гарбедж коллектору их собирать, кешируем только массивы. Работаем с данными либо через большой снапшет, либо через небольшие кеши поверх ConcurrentHashMap. Не прибегая к особым ухищрениям мы сохраняем для себя удобство разработки на Java, но жертвуем тем, что обслуживание части запросов все-таки выпадает на GC паузы (не больше 5%) и обслуживаются не за 3-4 ms а за 150-200 ms. Для нас это осознанный tradeoff.
Цифр не хватает, какая разница по скорости на практике получается.