Pull to refresh
8K+
13
Denis Tishkov@DenisT

Специалист по производительности торговых систем

18
Rating
2
Subscribers
Send message

Честно - мусора будет много, итоговые типы получаются длинными и не читаемыми, но во многих местах, где это имело смысл, подложены 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 (эмулирется нагрузка и непрерывная работа).

>> реактивность от которой отказались, как я понял, именно в угоду читаемости.
я бы сказал, что «не перешли», а не «отказались».
1) На виртуалках, планируем переезд на реальное железо
2) Где-то 16GB живых объектов, в основном это статические данные для расчетов.

Если уточните для каких целей спрашиваете, может быть смогу более развернуто ответить
хорошие новости, спасибо)
  1. Shenandoah очень хотим попробовать, но у нас есть ограничения по версии jdk, которую можем использовать в продакшене, мы пока на 11, как только появится возможность перейти на 14 Java или выше, обязательно попробуем.
    Zgc нет не пробовали.
  2. С GraalVm, в бесплатной версии не оптимально сделано выполнение simd вычислений, что для нас важно, а платную версию не было возможности протестировать.

Кстати, если вам интересна тема ухищрений на Java для высокопроизводительных вычислений, то возможно будет интересен доклад https://youtu.be/QV-ue1YMdds

Я не знаком с Clojure, но мне сложно представить конецепцию проще актора для параллельных вычислений — сообщение на входе — сообщение на выходе:)
>> 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.
обязательно поделимся:)
Нет не сравнивали, хотя идея хорошая, спасибо!

Цифр не хватает, какая разница по скорости на практике получается.

Спасибо за результаты. Подскажите, а чей код в msort.h и rsort.h, Ваш?
1

Information

Rating
493-rd
Location
Москва, Москва и Московская обл., Россия
Registered
Activity