Данный простой пример запускает два Java-процесса на Linux-машине. Один процесс посылает сообщения, а второй получает эти сообщения через UDP-протокол через сетевой loopback-интерфейс. Пример призван показать, что привязка процесса к ядру процессора (affinity) позволяет увеличить производительность процесса, уменьшить разброс (jitter) значений latency. В конце статьи я объясняю, почему это происходит.
Исходники примеров взяты из статьи Матрина Томпсона. Статья написана в 2011 году. Мартин позже поменял исходники, а текст статьи не поменял, пришлось чуть-чуть помучиться с настройками и с исходным кодом, чтобы все заработало.
Для демонстрации данного примера требуется один многоядерный компьютер, который даже может быть не подключен к сети. Ниже приведенные шаги проверены на стандартном дистрибутиве CentOS 8, запущенном на виртуальной машине в VirtualBox, созданном с помощью Vagrant. Тестирование проводилось на рядовом десктопном 4-х ядерном процессоре Intel Core i5-6400T @ 2.20GHz. Для сборки проекта использовались OpenJDK 11.0.9.1 2020-11-04 LTS и Gradle 6.3.
Настройки сетевого интерфейса
Проверяем, что loopback-интерфейс есть, и что он настроен. Раньше для этого использовалась команда ifconfig, но она входит в пакет net-tools, который с недавних пор уже не устанавливается в CentOS по умолчанию. В качестве замены используется команда ip addr show:
# ip addr show 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000 link/ether 08:00:27:68:19:7b brd ff:ff:ff:ff:ff:ff inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic noprefixroute enp0s3 valid_lft 86160sec preferred_lft 86160sec inet6 fe80::d2b6:49e0:19bb:b5d9/64 scope link noprefixroute valid_lft forever preferred_lft forever
Из результатов исполнения команды видим, что у нас на компьютере присутствует loopback-интерфес и его имя — lo.
Запуск программ и замеры
Запускаем первый класс – MultiCastReceiver (Получатель). Передаем ему в качестве параметра только имя dummy-интерфейса lo:
# java MultiCastReceiver lo
Получатель находится в ожидании и говорит, что пока ничего не получил:
Received 0 messages in 1112ms Received 0 messages in 1016ms Received 0 messages in 1005ms Received 0 messages in 1008ms Received 0 messages in 1001ms Received 0 messages in 1004ms Received 0 messages in 1023ms Received 0 messages in 1002ms...
Запускаем второй класс – MultiCastSender (Отправитель). Передаем ему в качестве параметра имя loopback-интерфейса lo и сколько сообщений надо отправить:
# java MultiCastSender lo 20000000
Отправитель, начинает отправлять сообщения и выводит отчеты:
Sent 82044 messages in 1001ms Sent 85544 messages in 1002ms Sent 80738 messages in 1016ms Sent 69781 messages in 1023ms Sent 74374 messages in 1001ms Sent 88204 messages in 1002ms ...
Теперь получатель должен показывать, что он получает сообщения:
Received 63650 messages in 1009ms Received 49127 messages in 1001ms Received 49191 messages in 1011ms Received 54849 messages in 1000ms Received 56022 messages in 1001ms Received 55658 messages in 1002ms Received 62746 messages in 1039ms ...
Теперь то же самое с taskset
Запускаем те же программы через taskset, привязывая каждый процесс к определенному CPU. Счет CPU ведется с 0:
# taskset -c 0 java MultiCastReceiver lo
# taskset -c 1 java MultiCastSender lo 20000000
С помощью pidstat убедимся, что каждый процесс теперь работает на своем отдельном CPU (ядре):
# pidstat -t -p 3451 CPU Command 0 java 0 |__java 0 |__java 0 |__VM Thread 0 |__Reference Handl 0 |__Finalizer 0 |__Signal Dispatch 0 |__C2 CompilerThre 0 |__C1 CompilerThre 0 |__Sweeper thread 0 |__Service Thread 0 |__VM Periodic Tas 0 |__Common-Cleaner 0 |__Thread-0 pidstat -t -p 3438 CPU Command 1 java 1 |__java 1 |__java 1 |__VM Thread 1 |__Reference Handl 1 |__Finalizer 1 |__Signal Dispatch 1 |__C2 CompilerThre 1 |__C1 CompilerThre 1 |__Sweeper thread 1 |__Service Thread 1 |__VM Periodic Tas 1 |__Common-Cleaner 1 |__Thread-0
Собираем статистику и…
Для оценки результатов достаточно скопировать из консоли вывод программы MultiCastReceiver в Excel и построить график по второму столбцу. А потом свести два графика в один.

График розового цвета показывает количество сообщений, обработанное без утилиты taskset, синий график — с помощью taskset. Обратите внимание на разброс значений (jitter) в розовом графике и на практически постоянное значение (за редким исключением) в синем графике. Хочу отметить, что здесь мы не оцениваем производительность, мы оцениваем разброс производительности. Чем объясняется столь разительная разница в результатах?
… размышляем
Как видим из вышеприведенного примера с помощью утилиты taskset мы привязали каждую программу к определенному ядру (affinity) и тем самым добились уменьшения нестабильности времени выполнения нашей программы (jitter), а значит повысили предсказуемость ее поведения.
Привязывать поток (процесс) к ядру можно и программным способом. Помимо утилиты taskset более тонко affinity потоков приложения можно настроить
- с помощью утилит isolcpus и cgroups, либо
- программно через JNI/C++: например, как это сделано в OpenHFT/ThreadAffinity.
Следует помнить, что мало привязать нужный процесс (поток) к ядру, надо еще сделать так, чтобы другие процессы (потоки) не занимали это ядро. Этот прием называется изоляцией ядра (core isolation). С его помощью конкретное ядро процессора скрывается от планировщика и отдается полностью во владение определенного процесса (потока). Планировщик операционной системы не «видит» данного ядра и не выделяет его другим потокам и процессам. Так можно еще больше снизить latency и ее разброс.
Итоги
В данной статье я объясняю, как с помощью утилиты taskset можно прикрепить приложение к определенному ядру многоядерного процессора, и показываю, как и почему это улучшает производительность многопоточного приложения.
Что почитать еще
- Книги о «железе» позволят понять, как устроены современные процессоры и в целом вся аппаратная часть компьютерной системы;
- Книги о многопоточности расскажут о нюансах создания многопоточных приложений на Java;
- Статья Оптимизации на всех уровнях системы расскажет, где еще кроются ресурсы улучшения производительности;
- Библиотека документов о performance
- Бесплатного супа больше не будет. Почему все программисты просто обязаны овладеть многопоточным программированием.