Pull to refresh

Comments 61

Много ли кейсов где подобные оптимизации (обязательное приведение Integer -> int, геморрой с кастомными коллекциями и тд) могут реально дать ощутимый бус производительности?

На тему изменяемых (mutable) объектов было бы круто упомянуть про то, что работа с неизменяемыми (immutable) объектами будет оптимизирована на уровне языка

Вообще, люто не хватает сравнения перформанса, HashMap и Int2IntCounterMap, на каких-то +- реальных примерах, потому что сейчас проблема кажется несколько надуманной
Помимо описанного профита для работы кэша процессора, стоит еще упомянуть очевидную нативную поддержку. Сумма примитивов — ~1 операция. Сумма программных типов — несколько проверок и, возможно, кастомный размер + кастомная математика. Ваш кэп.
В интернете есть довольно много подобных бенчмарков. Например вот. Нужно ли тянуть в ваш проект дополнительные зависимости — это вам решать. Тем не менее, если есть задача избавиться от лишнего мусора, то выбора не остается.
не понимаю, почему заминусовали ваш комментарий.
Полностью согласен, что некоторые техники достаточно спорные и без внятных бенчмарков выглядят как слова на ветер.

Например, как вы и заметили, виртуальная машина заточена на оптимизацию работы с быстро умирающими immutable обьектами, и наоборот — работа с mutable приводит ко многим накладным расходам как, на пример, очистка таких долгоживущих обьектав в более дорогих стадиях сборщика мусора (я не говорю уже о парралельных потоках, для которых mutable обьекты могут сильно снизить производительноть и добавить сложности с синхронизацией). В общем, такие советы нужно использовать очень осторожно, чтобы не убить свое приложение в плане производительности.
очистка таких долгоживущих обьектав в более дорогих стадиях сборщика мусора

А их не надо очищать, такие объекты должны жить пока живет JVM, в этом-то и смысл.
Вам не сравнение перформанса нужно, а простой алгоритм:
1) поймите для себя — тормозит ли ваше приложение. Если не тормозит вообще, то и проблемы нет… При этом приложение может жрать 16 ядер и потреблять 60 гигов хипа и запросы минутами обрабатывать, но пользователя это абсолютно устраивает (например — запросы приходят от крона ночью, а сервак один фиг в это время простаивает)… А может приложение отрабатывать за 300мс на 200мб хипа, но очень сильно дофига тормозить, т.к. это высокоскоростная торговля.
2) Если тормозит — запустите профилировщик (тот же jfr отлично справляется) и посмотрите на горячие методы… Может там сетевой активности много или где-то пузырьковая сортировка вручную написана.
3) Если видите, что всё «ровно» и, особенно, если операции с картами занимают заметный процент времени (или GC часто и с аппетитом работает, сжирая хотя-бы 5% CPU) — попробуйте поменять мапу, добавить ForkJoinPool на много потоков, добавить кэши и вот эту вот всю стандартную фигню из зелёной зоны оптимизации приложений.
4) Если не помогло и это — обратитесь к специалисту
UFO just landed and posted this here
В чем конкретно заключаются байки?

Обертки приводят к аллокации памяти (обычно). Аллокация приводит к GC (периодически) и залипанию приложения на пару миллисекунд. В некоторых (очень специфичных) приложениях это залипание неприемлемо.

Что из вышеперечисленного неправда?
Да, никаких новых/экзотических фич языка тут нет, обычная Java. Я даже Unsafe ни разу не упомянул. Идея поста в том, чтобы показать как выглядит стиль garbage free программирования. В таком стиле написано довольно мало библиотек. И нужно это далеко не во всех ситуациях. Тем не менее, такой стиль является насущной необходимостью в случае если SLA для worst case latency на весь сетевой стек и бизнес логику системы составляет десятки микросекунд.
Спасибо большое что поделились опытом.

Со мной не обязательно соглашаться, но почти все приемы в статье так или иначе это полумеры, отсрочка неизбежного. Если приложение/сервис многопоточный, то как не выкручивайся, но все равно где-то потечет либо свой код, либо внутри чьей-то библиотеки, потоки будут загаживать друг друга и будет miss-hell, сборщик может вести себя непредсказуемо и прочие прелести. Можно продолжать бороться, понимая стоимость такой борьбы как в цене разработчиков, так и в цене потенциальных ошибок.

В итоге я пришел к схеме, когда приложение/сервис приходится дробить на типы критичности кода в «деньгах». То, что менее критично, как в плане low latency внутри приложения и связь с внешним миром, так и в плане быстроты реакции систем мани-менеджмента, внутреннего «клиринга» активов по портфелям — остается на Scala/Java под дополнительным мониторингом, как внутренним (надежный код, доп. логи в несколько типов хранилищ, кросс-мониторинг между такими хранилищами), так и внешним (мониторинг процессов, работоспособность/доступность портов, тестирование правильности работы firewalld/iptables и различных AC-систем на их основе). То, что критично для принятия решения менее минуты — где возможно, переписывается на С++ и Rust (очень аккуратно).

Как бы ни хотелось единообразия технологического стека, но, увы, при тестировании и моделировании этот порог в минуту очень важен (не только для HFT) и для многих торговых алгоритмов поведения стоимость реакции на нестандартное поведение/обработку ошибок почти всегда по деньгам превышает комиссии/своп/депозитарий по торговым инструментам. А для тех торговых систем, где по изначальной идее доходность сильно зажата и имеет смысл только на большом объеме средств (market making, вынужденные тесты ликвидности для поквартальной переоценки балансов хеджфондов) — такая дополнительная обработка ошибок и нестандартных ситуаций, а также соответствующая экономия уже становится сравнима с доходностью таких систем.

Так что у любого кода, где есть прямая денежная ответственность — всегда путь к максимально жесткой оптимизации и изоляции. Вплоть до своих специализированных FPGA-чипов для ускорения работы сетевого стека при работе с серверами бирж (но это сугубо поляна HFT-шников и арбитражеров) с вшитыми наборами обработки наиболее критичных ситуаций и оповещением по вторичным каналам.

Далее по пути определения стоимости ошибок нужно будет вводить в код дополнительные метрики/сборку логов куда-то в быстрое хранилище (память) и также после запуска торговой системы в боевой режим на реальные деньги, помимо постоянного бэктеста логики самой торговой системы, нужно будет также мониторить частоту и тип ошибок (бэктест нестандартного поведения приложения/сервиса + бэктест внешнего мира). В некоторых ситуациях резкого снижения ликвидности такие системы мониторинга могут сэкономить огромные деньги, также сэкономить затраты на юридическое сопровождение процесса торговли и урегулирования проблем с поставщиками услуг.

В итоге безобидная ТС с простеньким на вид алгоритмом принятия решений в 1-2к строк обрастает «броней» от внешнего мира и внутренним иммунитетом. И вот уже 150к+ строк, несколько языков, свои сервера для обслуживания как самой системы, так и надежности систем мониторинга и оповещения по различным степеням критичности событий.
К direct buffer ОЧЕНЬ неудобно обращаться, а работа с обычными объектами очень хорошо оптимизируется JVM.
Это отличный подход, так можно делать. Пользуясь случаем не могу не прорекламировать отличный инструмент для этого: SBE. К сожалению, это не всегда удобно. Пример: объект, который мы хотим переиспользовать хранит ссылки на другие объекты. В этом случае придется придумывать разные варианты, которые могут быть менее предпочтительны. 1) Сериализовать в этот же буфер объект, на который ссылаемся, и работать с локальной копией. Это иногда имеет смысл, но не всегда, особенно если состояние объекта может меняться извне. 2) Можно хранить объекты, на которые мы ссылаемся, в неком массиве/списке, а в буфер записывать только индекс в этом массиве. В этом случае у нас нет ссылки на сам этот массив/список в буфере, и мы должны либо делать его статическим, либо ссылка на него должна быть доступна из контекста.
Да, описанных мер не достаточно. По факту, приходится отказываться от использования на критическом пути практически всех привычных для большинства разработчиков библиотек. Конечно, это сильно ограничивает. Мы тоже пришли к тому, что весь критический путь вынесен в одну или несколько JVM, в которых мусорить практически запрещено. Вся остальная обвязка, не критичная к latency, вынесена в отдельные JVM. Там можно и мусорить, и блокироваться сколько влезет.
P.S. Минута — очень длинный промежуток времени. Это уже территория high throughput.
А не проще разрешить GC, которые вносят более-мнее предсказуемые задержки и тонко настроить их через ключи (рекомендуемое/максимальное время блокировки и т.п.) и потом ещё и принудительно запускать, когда задачи позволяют.
У там совсем всегда что-ли миллисекунды гарантировать нужно? Тогда уж нужно другой язык искать.
У меня промышленный софт отлично через Java управляется в приличном realtime.
Наш ориентир по latency — 50 мкс. Насчет другого языка — да, можно писать на нативном языке. Но у java есть неоспоримые плюсы: удобные фреймворки для тестирования кода, удобные IDE для разработки, куча библиотек для всего чего угодно, которыми вполне можно пользоваться вне критического пути, высокая скорость разработки. По факту обо всех тех же вещах придется думать и при программировании на условных плюсах. malloc на критическом пути в любом случае не вызывается, так что придется в подобных вещах упражняться.
Ну, тогда Java не ваш язык же! Хотя, если вам нужно среднее значение, то, мне кажется, что оно достижимо тонкой настройкой сборщика мусора и минимальными извращениями с экономией памяти. Правда, это от количества объектов и общего количества используемой памяти зависит.
Ну, тогда Java не ваш язык же!

Ну с чего же вдруг так? На Java есть высокопроизводительные системы. И качественно работают. На JavaOne ребята выступали, которые на Java писали биржевой аггрегатор.
И ни чего. Работало с нужными им временными задержками. Просто готовить надо уметь.
50 мкс это среднее, максимум или какой-то перцентиль?
Если быть точным, наш текущий таргет: 50мкс — медиана, 100мкс — 99.99%.
В первую очередь важно не среднее значение, а правый хвост распределения. К сожалению, настройкой GC обойтись не получалось, поэтому пришлость начать экономить на аллокациях.
Ваши исследования подтверждены бенчмарками или вы предполагаете, что если будет меньше аллокаций, то будет работать быстрей?
Почему спрашиваю, когда то проверял скорость объектного пула для небольших буферов в 200-300 байт против создания новых, итого пул работал не на много, на около 5%, но медленней.
тут вопрос не в том, что медленнее, а в том, что предсказуемей, т.е. если на критическом пути market date'ы плодить объекты, то GC рано или поздно случится. Получается мы теряем некоторый throughput ради предсказуемого правого хвоста распределения.

IO ещё нехило аллоцирует, конкретно — HashSet внутри Selector’a


Из способов борьбы я знаю: хак из Агроны (подменяющий этот HashSet через reflection), отказ от селектора (если сокетов не очень много), написание своей JNI-обёртки над epoll и сокетами.


Используете что-то из этого?

Мы не напрямую с IO работаем, так что впервые про это слышу. Но скорее всего наша либа использует что-то в этом роде.

Reflection — такой себе вариант: существенное жертвование производительностью в method.invoke ради уменьшения количества аллокаций в memory heap.


Method.invoke в числодробилках дает существенную просадку, которая хорошо видна в профилировщике. У меня на алгоритмах обхода достаточно большого ациклического графа (количество нод > 10 000, количество ребер > 1 000 000) отказ от вызова method.invoke на каждой ноде дал суммарный прирост производительности ~30% (даже с увеличенной нагрузкой на гц)

Да, reflection медленный, но он используется один раз, при создании селектора. Селекторов создаётся мало, обычно один на thread.

Интересный момент, как влияет использование try-with-resources на скорость работы сгенерированного кода. В С++ компиляторах раньше при включении исключений некоторые оптимизации просто отключались.

Отключение при включении исключений. А неплохая скороговорка получается...

Не думали о более радикальных подходах, ломающих стереотипы? Epsilon GC. Далее, как это применить для ваших нужд, подумайте самостоятельно.

Java11 еще слишком молода чтобы на ней выкатывать критический код на продакшен. Банки все еще на java8 болтаются. А что то вообще на java7 до сих пор. Эпсилон крут в осоебнных случаях. Если код нетмусорит в хип то эпсилон — самое то. Тестировал на qa результаты отличные. Но если где то утечка, обидно ж будет вылетесь в середине торгового дня с outofmemoryerror?
осоебнных

Опечатка соответствует характеру этих случаев?)
1)Изза того, что данные перемещаются в памяти при сборке мусора, то заранее не известен адрес, где объект располагает данные. Каждый раз, когда вы обращаетесь к данным, то происходит не прямое обращение к памяти, а сначала вычисляется адрес данных.

Вот интересно было бы понять, — когда происходит вычисление адреса и когда нет. Сколько тактов занимает вычисление адреса? В каких случаях происходит прямое обращение, как в С++?

2) Есть подозрения, что если переместить сборщик мусора в режим ядра ring0, то можно делать более эффективный сбор мусора, так как некоторые фишки реализованы на уровне оборудования. Например dirty bit на странице памяти реализован на уровне оборудования и говорит, было ли изменение куска памяти или нет.
>> 1)Изза того, что данные перемещаются в памяти при сборке мусора, то заранее не известен адрес, где объект располагает данные. Каждый раз, когда вы обращаетесь к данным, то происходит не прямое обращение к памяти, а сначала вычисляется адрес данных.

Это не так, начиная где-то с Java 1.4 (а может и раньше).
Объектная ссылка всегда содержит прямой указатель на объект. Когда GC проходит по графу объектов, он также обновляет ссылки (кроме ссылок из мусора, естественно).

Возможно немного оффтоп, но очень уж интересно. Почему в Java типичные коллекшены или списки (не встроенные массивы) используют обьектные Integer, а не примитивные?

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

Столкнулся с необходимостью уменьшить мусор, когда работал с OpenCV на Android. Там применяется нативная библиотека, которая биндится через JNI, соответственно, управление ресурсами полностью ручное. Хип на андроиде не очень большой, а изображения с камеры и при обработке довольно крупные, так что рассчитывать на GC уже нельзя. В частности, был такой баг: если телефон положить на стол камерой вниз, программа падала через секунд 20, а при обычной эксплуатации работала намного дольше. Из-за полностью чёрного кадра скорость работы возрастала в разы (с 2-3 FPS до 30, максимум для камеры), и память быстро исчерпывалась, программа иногда молча, а иногда с трейсом, закрывалась (падала по OOM в нативном коде).


Дошёл сам до того же решения, что и автор тут советует — все матрицы в final-полях, делаю .release() как только массив перестаёт быть нужен. Интересно, что сам по себе .release() вовсе не спасает от OOM, если продолжать на каждый кадр создавать новые объекты. А вот с переиспользованием всё работает как надо. Немного страшновато было лишь то, что обработка шла в отдельном потоке, дабы не затормаживать отрисовку картинки с камеры, а строгой синхронизации никакой по сути не было. Только future, который отмечал, что распознавание завершено, так что можно загрузить следующую картинку и запустить новый таск (а я не знаю, как правила happens-before работают с нативной памятью, вдруг задание переменной из одного потока не успеет «протечь» в другой?). Но вроде проблем так и не возникло.

UFO just landed and posted this here
ThreadLocal вносит свои накладные расходы, лучше уж держать всю логику в одном потоке.
UFO just landed and posted this here
ThreadLocal в java — это по факту нечто вроде Map<ThreadID, Object>, там довольно увесистая логика с отрабатывает каждый раз.

Это не совсем так. В текущей имплементации ThreadLocals реально хранятся локально в каждом треде как Thread.threadLocals: Map<ThreadLocal, Object>. Доступ к ним действительно очень быстрый, однако есть проблемы с очисткой — если не был въявную вызван ThreadLocal.remove(), то слот может болтаться в Thread-е до следующего рехэша.

Да, действительно, я ошибся. Ну так или иначе мы используем не стандартный ThreadLocal, а самодельную реализацию на основе сгенерированного кода, которая судя по jmh бенчмаркам раза в два быстрее. Можно будет про нее отдельно написать как-нибудь.

Добрый день, действительно ли использование объектных пулов даёт в вашем проекте ощутимый прирост производительности? Вопрос задаю в связи с всплывшим в памяти докладом Алексея Кудрявцева "Computer Science ещё жива", в котором утверждается, что от объектных пулов он отказался после переезда на "восьмёрку".


Вот тут этот момент в докладе: https://youtu.be/Ra2RSsyO4XU?t=2097

UFO just landed and posted this here
C++ никак не поможет против вытесняющей многозадачности. Решение этой проблемы совершенно одинаковое, что на C++, что на java.
И не надо путать высоконагруженные и low-latency приложения.
пытаются на C писать сложную многопоточную бизнес логику или, наоборот, на Java пытаются написать low latency код
Что делать, если нужна сложная многопоточная бизнес-логика с low-latency?
> на Java пытаются написать low latency код.
Не тоьько пытаются. Но и пишут. И достигают этой самой low-latency
void divide(int value, int divisor, IntPair outResult) {
   outResult.x = value / divisor;
   outResult.y = value % divisor;
}

Очень плохой пример, т.к. функция, которая изменяет входной аргумент, должна отдавать ссылку на измененный объект, а не просто изменять без возврата. String transform(String text).
(по «Чистому коду»)
Код пишется для людей в первую очередь, а не для машины.
Код пишется для людей в первую очередь, а не для машины.


Вся статья примерно о том, что в некоторых случаях пишется именно для машины.
Какбы да, ваш подход удобнее, тем что позволяет делать так:
divide(42, 9, new IntPair()).getX()

Но когда обычно в проекте используются иммутабельные объекты, что-то вроде
JSONObject transform(JSONObject json)

Даёт возможность ошибаться, что метод возвращает изменённую копию, а не изменяет объект на входе.

Разумеется, это уже общетеоретический разговор не имеющий отношения к самой статье.
Спасибо, познавательная статья!
Какое время отклика требовалось в системах, над которыми вы работали?
По факту все что меньше 1мс требует примерно такого подхода.
У вас время отклика 100 микросекунд ка цель. Бывает меньше биывает больше. Боремся чтобы было меньше
А вы не задумывались о правильности выбора технологического стека под вашу задачу? Основные проблемы, которые вы описываете, в родственном Java дотнете решаются использованием структур. Там вы можете не ограничиваться примитивами, а писать свои типы, которыми не управляет GC в общем случае.
Да, задумывались. Дело ведь не только в языке, но и в рантайме. JVM под Linux довольно неплохо оптимизированы. Как обстоят дела у проекта Mono я не особо в курсе.
Если нужен Linux, то хуже Java и, главное, нет стабильности. Что-то где-то обмновил и вся картина поменялась.
С дотнетом очень плохо на линуксе. А виндовс в продакшене для торговли уже никто не использует.

Кстатит в джаве тоже уже подумывают о реализации структур. Долготдумают. Возможно допилят
Рекомендую: www.youtube.com/watch?v=BD9cRbxWQx8
How low can you go? Ultra low latency Java in the real world — Daniel Shaya — YouTube

Там автор презентации утверждает что на джаве и на c++ модно достичь latency в 10 микросекунд. С джавой пидется поизвращаться чтобы в коде не было мусора и никакого gc. То есть так как написано в данной статье. На джаве писать лучше потому что такой же код на c++ придется писать очень осторожно и легко можно сделать ошибок. А в финансовой сфере цена ошибок очень велика.
Sign up to leave a comment.

Articles