Java очень быстрая, если не создавать много объектов

Исследование влияния создания даже крошечных объектов на производительность и сборку мусора.

В этой статье рассматривается бенчмарк передачи событий по TCP/IP со скоростью 4 миллиарда событий в минуту с использованием пакета net.openhft.chronicle.wire.channel в Chronicle Wire (с открытым исходным кодом) и почему мы стремимся избегать создания объектов. 

Одной из ключевых оптимизаций является почти полное отсутствие мусора. Создание объектов должно быть очень дешевой операцией, и сборка мусора очень короткоживущих объектов также очень дешева. Действительно ли отказ от создания объектов имеет такое большое значение? Какое значение имеет один небольшой объект, создаваемый на событие (44 байта) для производительности в тесте на пропускную способность, где паузы GC амортизируются?

Хотя создание максимально эффективно, оно не позволяет избежать нехватки памяти в кэшах L1/L2 ваших процессоров, и, когда многие ядра заняты, они конкурируют за память в общем кэше L3.

Полученные результаты

Бенчмарк на Ryzen 5950X с Ubuntu 22.10.

* Для 16 клиентов событие отправляется в обоих направлениях. 

Average Latency (Средняя задержка) = 2 * 16 / throughput (пропускная способность) 

ns - наносекунды, M events/с - миллион событий в секунду
* Для 16 клиентов событие отправляется в обоих направлениях.  Average Latency (Средняя задержка) = 2 * 16 / throughput (пропускная способность) ns - наносекунды, M events/с - миллион событий в секунду

Одно дополнительное создание объекта для каждого события добавляет 166 нс или около того. Это похоже не так много, однако в этом высоко оптимизированном контексте с высокой пропускной способностью это снижает производительность на 25%. Режим по умолчанию для чтения событий в Chronicle Wire заключается в повторном использовании одного и того же объекта для одного и того же типа события каждый раз при десериализации. Это обеспечивает простую стратегию объединения объектов, позволяющую избежать повторного создания объектов. Если эти данные должны быть сохранены, они должны быть сначала скопированы, потому что объекты используются повторно, чтобы уменьшить количество создаваемых объектов.

Только 0,3% времени было потрачено на сборщике мусора

Общее время, проведенное в GC, составило около 170 миллисекунд в минуту или 0,3% времени. Время уходит именно на создание, а не на очистку этих очень недолговечных объектов.

Скорость создания и средняя задержка

Бенчмарк, который просто создает недолговечные объекты TopOfBook на нескольких процессорах, дает аналогичный результат. Это говорит о том, что скорость создания новых объектов быстро насыщается даже при небольшом количестве ядер, что увеличивает среднюю задержку при увеличении количества потоков. Это для того же маленького 44-байтового объекта.

На Ryzen 5950X с Ubuntu 21.10, Java 17.0.4.1

Бенчмарк

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

public class EchoTopOfBookHandler implements TopOfBookHandler {
   private TopOfBookListener topOfBookListener;

   @Override
   public void topOfBook(TopOfBook topOfBook) {
       if (ONE__NEW_OBJECT)
           topOfBook = topOfBook.deepCopy();
       topOfBookListener.topOfBook(topOfBook);
   }

В этом случае deepCopy() создает новый TopOfBook и устанавливает все поля.

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

public interface TopOfBookListener {
   void topOfBook(TopOfBook topOfBook);
}

Программное обеспечение с низкой задержкой может быть очень быстрым, но при этом сложным в работе, что замедляет разработку. Другими словами, часто для создания программного обеспечения с низкой задержкой разработчики используют низкоуровневые методы, которые трудно читать и поддерживать. Эти накладные расходы могут замедлить вашу разработку. С Chronicle Wire ваши структуры данных легко читаются и отлаживаются, при этом производительность не снижается.

Используя события, смоделированные в YAML, мы можем поддерживать поведенческо-ориентированную разработку микросервиса.

Для целей тестирования ваши данные могут быть представлены в простом формате yaml, как показано ниже:

# This is the in.yaml for the microservice of topOfBook that was described above. 
# first top-of-book
---
topOfBook: {
 sendingTimeNS: 2022-09-05T12:34:56.789012345,
 symbol: EUR/USD,
 ecn: EBS,
 bidPrice: 0.9913,
 askPrice: 0.9917,
 bidQuantity: 1000000,
 askQuantity: 2500000
}
...
# second top-of-book
---
topOfBook: {
 sendingTimeNS: 2022-09-05T12:34:56.789123456,
 symbol: EUR/USD,
 ecn: EBS,
 bidPrice: 0.9914,
 askPrice: 0.9918,
 bidQuantity: 1500000,
 askQuantity: 2000000
}
...

Код

Доступен здесь.

Эта библиотека используется в  Chronicle Services. 

Заключение

Java может быть очень быстрой, однако стоит избегать создания объектов. 

Стоимость создания объектов может быть намного выше, чем стоимость их очистки, если они очень недолговечны.