Comments 61
На тему изменяемых (mutable) объектов было бы круто упомянуть про то, что работа с неизменяемыми (immutable) объектами будет оптимизирована на уровне языка
Вообще, люто не хватает сравнения перформанса, HashMap и Int2IntCounterMap, на каких-то +- реальных примерах, потому что сейчас проблема кажется несколько надуманной
Полностью согласен, что некоторые техники достаточно спорные и без внятных бенчмарков выглядят как слова на ветер.
Например, как вы и заметили, виртуальная машина заточена на оптимизацию работы с быстро умирающими immutable обьектами, и наоборот — работа с mutable приводит ко многим накладным расходам как, на пример, очистка таких долгоживущих обьектав в более дорогих стадиях сборщика мусора (я не говорю уже о парралельных потоках, для которых mutable обьекты могут сильно снизить производительноть и добавить сложности с синхронизацией). В общем, такие советы нужно использовать очень осторожно, чтобы не убить свое приложение в плане производительности.
1) поймите для себя — тормозит ли ваше приложение. Если не тормозит вообще, то и проблемы нет… При этом приложение может жрать 16 ядер и потреблять 60 гигов хипа и запросы минутами обрабатывать, но пользователя это абсолютно устраивает (например — запросы приходят от крона ночью, а сервак один фиг в это время простаивает)… А может приложение отрабатывать за 300мс на 200мб хипа, но очень сильно дофига тормозить, т.к. это высокоскоростная торговля.
2) Если тормозит — запустите профилировщик (тот же jfr отлично справляется) и посмотрите на горячие методы… Может там сетевой активности много или где-то пузырьковая сортировка вручную написана.
3) Если видите, что всё «ровно» и, особенно, если операции с картами занимают заметный процент времени (или GC часто и с аппетитом работает, сжирая хотя-бы 5% CPU) — попробуйте поменять мапу, добавить ForkJoinPool на много потоков, добавить кэши и вот эту вот всю стандартную фигню из зелёной зоны оптимизации приложений.
4) Если не помогло и это — обратитесь к специалисту
Обертки приводят к аллокации памяти (обычно). Аллокация приводит к GC (периодически) и залипанию приложения на пару миллисекунд. В некоторых (очень специфичных) приложениях это залипание неприемлемо.
Что из вышеперечисленного неправда?
Со мной не обязательно соглашаться, но почти все приемы в статье так или иначе это полумеры, отсрочка неизбежного. Если приложение/сервис многопоточный, то как не выкручивайся, но все равно где-то потечет либо свой код, либо внутри чьей-то библиотеки, потоки будут загаживать друг друга и будет miss-hell, сборщик может вести себя непредсказуемо и прочие прелести. Можно продолжать бороться, понимая стоимость такой борьбы как в цене разработчиков, так и в цене потенциальных ошибок.
В итоге я пришел к схеме, когда приложение/сервис приходится дробить на типы критичности кода в «деньгах». То, что менее критично, как в плане low latency внутри приложения и связь с внешним миром, так и в плане быстроты реакции систем мани-менеджмента, внутреннего «клиринга» активов по портфелям — остается на Scala/Java под дополнительным мониторингом, как внутренним (надежный код, доп. логи в несколько типов хранилищ, кросс-мониторинг между такими хранилищами), так и внешним (мониторинг процессов, работоспособность/доступность портов, тестирование правильности работы firewalld/iptables и различных AC-систем на их основе). То, что критично для принятия решения менее минуты — где возможно, переписывается на С++ и Rust (очень аккуратно).
Как бы ни хотелось единообразия технологического стека, но, увы, при тестировании и моделировании этот порог в минуту очень важен (не только для HFT) и для многих торговых алгоритмов поведения стоимость реакции на нестандартное поведение/обработку ошибок почти всегда по деньгам превышает комиссии/своп/депозитарий по торговым инструментам. А для тех торговых систем, где по изначальной идее доходность сильно зажата и имеет смысл только на большом объеме средств (market making, вынужденные тесты ликвидности для поквартальной переоценки балансов хеджфондов) — такая дополнительная обработка ошибок и нестандартных ситуаций, а также соответствующая экономия уже становится сравнима с доходностью таких систем.
Так что у любого кода, где есть прямая денежная ответственность — всегда путь к максимально жесткой оптимизации и изоляции. Вплоть до своих специализированных FPGA-чипов для ускорения работы сетевого стека при работе с серверами бирж (но это сугубо поляна HFT-шников и арбитражеров) с вшитыми наборами обработки наиболее критичных ситуаций и оповещением по вторичным каналам.
Далее по пути определения стоимости ошибок нужно будет вводить в код дополнительные метрики/сборку логов куда-то в быстрое хранилище (память) и также после запуска торговой системы в боевой режим на реальные деньги, помимо постоянного бэктеста логики самой торговой системы, нужно будет также мониторить частоту и тип ошибок (бэктест нестандартного поведения приложения/сервиса + бэктест внешнего мира). В некоторых ситуациях резкого снижения ликвидности такие системы мониторинга могут сэкономить огромные деньги, также сэкономить затраты на юридическое сопровождение процесса торговли и урегулирования проблем с поставщиками услуг.
В итоге безобидная ТС с простеньким на вид алгоритмом принятия решений в 1-2к строк обрастает «броней» от внешнего мира и внутренним иммунитетом. И вот уже 150к+ строк, несколько языков, свои сервера для обслуживания как самой системы, так и надежности систем мониторинга и оповещения по различным степеням критичности событий.
P.S. Минута — очень длинный промежуток времени. Это уже территория high throughput.
У там совсем всегда что-ли миллисекунды гарантировать нужно? Тогда уж нужно другой язык искать.
У меня промышленный софт отлично через Java управляется в приличном realtime.
Почему спрашиваю, когда то проверял скорость объектного пула для небольших буферов в 200-300 байт против создания новых, итого пул работал не на много, на около 5%, но медленней.
IO ещё нехило аллоцирует, конкретно — HashSet внутри Selector’a
Из способов борьбы я знаю: хак из Агроны (подменяющий этот HashSet через reflection), отказ от селектора (если сокетов не очень много), написание своей JNI-обёртки над epoll и сокетами.
Используете что-то из этого?
Reflection — такой себе вариант: существенное жертвование производительностью в method.invoke ради уменьшения количества аллокаций в memory heap.
Method.invoke в числодробилках дает существенную просадку, которая хорошо видна в профилировщике. У меня на алгоритмах обхода достаточно большого ациклического графа (количество нод > 10 000, количество ребер > 1 000 000) отказ от вызова method.invoke на каждой ноде дал суммарный прирост производительности ~30% (даже с увеличенной нагрузкой на гц)
Не думали о более радикальных подходах, ломающих стереотипы? Epsilon GC. Далее, как это применить для ваших нужд, подумайте самостоятельно.
Вот интересно было бы понять, — когда происходит вычисление адреса и когда нет. Сколько тактов занимает вычисление адреса? В каких случаях происходит прямое обращение, как в С++?
2) Есть подозрения, что если переместить сборщик мусора в режим ядра ring0, то можно делать более эффективный сбор мусора, так как некоторые фишки реализованы на уровне оборудования. Например dirty bit на странице памяти реализован на уровне оборудования и говорит, было ли изменение куска памяти или нет.
Это не так, начиная где-то с Java 1.4 (а может и раньше).
Объектная ссылка всегда содержит прямой указатель на объект. Когда GC проходит по графу объектов, он также обновляет ссылки (кроме ссылок из мусора, естественно).
Возможно немного оффтоп, но очень уж интересно. Почему в Java типичные коллекшены или списки (не встроенные массивы) используют обьектные Integer, а не примитивные?
Столкнулся с необходимостью уменьшить мусор, когда работал с OpenCV на Android. Там применяется нативная библиотека, которая биндится через JNI, соответственно, управление ресурсами полностью ручное. Хип на андроиде не очень большой, а изображения с камеры и при обработке довольно крупные, так что рассчитывать на GC уже нельзя. В частности, был такой баг: если телефон положить на стол камерой вниз, программа падала через секунд 20, а при обычной эксплуатации работала намного дольше. Из-за полностью чёрного кадра скорость работы возрастала в разы (с 2-3 FPS до 30, максимум для камеры), и память быстро исчерпывалась, программа иногда молча, а иногда с трейсом, закрывалась (падала по OOM в нативном коде).
Дошёл сам до того же решения, что и автор тут советует — все матрицы в final-полях, делаю .release()
как только массив перестаёт быть нужен. Интересно, что сам по себе .release()
вовсе не спасает от OOM, если продолжать на каждый кадр создавать новые объекты. А вот с переиспользованием всё работает как надо. Немного страшновато было лишь то, что обработка шла в отдельном потоке, дабы не затормаживать отрисовку картинки с камеры, а строгой синхронизации никакой по сути не было. Только future, который отмечал, что распознавание завершено, так что можно загрузить следующую картинку и запустить новый таск (а я не знаю, как правила happens-before работают с нативной памятью, вдруг задание переменной из одного потока не успеет «протечь» в другой?). Но вроде проблем так и не возникло.
Это не совсем так. В текущей имплементации ThreadLocals реально хранятся локально в каждом треде как Thread.threadLocals: Map<ThreadLocal, Object>. Доступ к ним действительно очень быстрый, однако есть проблемы с очисткой — если не был въявную вызван ThreadLocal.remove(), то слот может болтаться в Thread-е до следующего рехэша.
Добрый день, действительно ли использование объектных пулов даёт в вашем проекте ощутимый прирост производительности? Вопрос задаю в связи с всплывшим в памяти докладом Алексея Кудрявцева "Computer Science ещё жива", в котором утверждается, что от объектных пулов он отказался после переезда на "восьмёрку".
Вот тут этот момент в докладе: https://youtu.be/Ra2RSsyO4XU?t=2097
И не надо путать высоконагруженные и low-latency приложения.
пытаются на C писать сложную многопоточную бизнес логику или, наоборот, на Java пытаются написать low latency кодЧто делать, если нужна сложная многопоточная бизнес-логика с 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)
Даёт возможность ошибаться, что метод возвращает изменённую копию, а не изменяет объект на входе.
Разумеется, это уже общетеоретический разговор не имеющий отношения к самой статье.
Какое время отклика требовалось в системах, над которыми вы работали?
Кстатит в джаве тоже уже подумывают о реализации структур. Долготдумают. Возможно допилят
How low can you go? Ultra low latency Java in the real world — Daniel Shaya — YouTube
Там автор презентации утверждает что на джаве и на c++ модно достичь latency в 10 микросекунд. С джавой пидется поизвращаться чтобы в коде не было мусора и никакого gc. То есть так как написано в данной статье. На джаве писать лучше потому что такой же код на c++ придется писать очень осторожно и легко можно сделать ошибок. А в финансовой сфере цена ошибок очень велика.
Как не мусорить в Java