it-swarm.com.ru

Переключение контекста намного медленнее в новых ядрах Linux

Мы планируем обновить ОС на наших серверах с Ubuntu 10.04 LTS до Ubuntu 12.04 LTS. К сожалению, кажется, что задержка запуска потока, который стал работоспособным, значительно увеличилась с ядра 2.6 до ядра 3.2. На самом деле, в цифры латентности, которые мы получаем, трудно поверить.

Позвольте мне быть более конкретным о тесте. У нас есть программа, которая запускает два потока. Первый поток получает текущее время (в тиках, использующих RDTSC), а затем сигнализирует переменную условия один раз в секунду. Второй поток ожидает переменную условия и просыпается, когда она получает сигнал. Затем он получает текущее время (в тиках, использующих RDTSC). Разница между временем во втором потоке и временем в первом потоке вычисляется и отображается на консоли. После этого второй поток снова ожидает условную переменную. Это будет сигнализировано снова первым потоком приблизительно после второго прохождения.

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

В ядре 2.6.32 эта задержка составляет порядка 2,8-3,5 мкс, что является разумным. В ядре 3.2.0 эта задержка увеличилась примерно до 40-100 мкс. Я исключил любые различия в оборудовании между двумя хостами. Они работают на идентичном оборудовании (процессоры с двумя сокетами X5687 {Westmere-EP}, работающие на частоте 3,6 ГГц с отключенным гиперпоточностью, быстрым шагом и всеми состояниями C). Тестовое приложение изменяет сродство потоков, чтобы они запускались на независимых физических ядрах одного и того же сокета (т. Е. Первый поток запускается на ядре 0, а второй поток запускается на ядре 1), поэтому не происходит отскока потоков на ядра или подпрыгивание/связь между розетками.

Единственное различие между двумя хостами состоит в том, что на одном из них работает Ubuntu 10.04 LTS с ядром 2.6.32-28 (блок быстрого переключения контекста), а на другом - последняя версия Ubuntu 12.04 LTS с ядром 3.2.0-23 (медленный контекст). коробка переключения). Все настройки BIOS и аппаратные средства идентичны.

Были ли какие-либо изменения в ядре, которые могли бы объяснить это смешное замедление того, сколько времени потребуется для запуска потока по расписанию?

Обновление: Если вы хотите запустить тест на своем хосте и сборке Linux, я имею отправил код в Pastebin для ваше прочтение Компилировать с:

g++ -O3 -o test_latency test_latency.cpp -lpthread

Запустите с (при условии, что у вас есть хотя бы двухъядерный процессор):

./test_latency 0 1 # Thread 1 on Core 0 and Thread 2 on Core 1

Обновление 2 : после долгих поисков по параметрам ядра, сообщениям об изменениях в ядре и личным исследованиям я выяснил, в чем проблема, и опубликовал решение как ответ на этот вопрос.

96
Michael Goldshteyn

Решение проблемы производительности пробуждения в последних ядрах связано с переключением на драйвер intel_idle cpuidle из acpi_idle, драйвера, используемого в старых ядрах. К сожалению, драйвер intel_idle игнорирует пользовательскую конфигурацию BIOS для C-состояний и исполняет свою мелодию . Другими словами, даже если вы полностью отключите все состояния C в BIOS вашего компьютера (или сервера), этот драйвер все равно будет принудительно включать их в течение периодов кратковременного бездействия, которые почти всегда происходят, если только синтетический тест производительности не потребляет все ядра (например, стресс) ) бежит. Вы можете отслеживать переходы состояний C, а также другую полезную информацию о частотах процессора, используя замечательный Google инструмент i7z на большинстве совместимых аппаратных средств.

Чтобы увидеть, какой драйвер cpuidle активен в вашей настройке, просто перейдите в файл current_driver в разделе cpuidle/sys/devices/system/cpu следующим образом:

cat /sys/devices/system/cpu/cpuidle/current_driver

Если вы хотите, чтобы ваша современная ОС Linux имела минимально возможную задержку переключения контекста, добавьте следующие параметры загрузки ядра, чтобы отключить все эти функции энергосбережения:

В Ubuntu 12.04 вы можете сделать это, добавив их в запись GRUB_CMDLINE_LINUX_DEFAULT в /etc/default/grub и затем запустив update-grub. Параметры загрузки для добавления:

intel_idle.max_cstate=0 processor.max_cstate=0 idle=poll

Вот ужасные подробности о том, что делают три варианта загрузки:

Установка intel_idle.max_cstate в ноль либо вернет ваш драйвер cpuidle в acpi_idle (по крайней мере, согласно документации опции), либо полностью отключит его. В моем ящике он полностью отключен (то есть отображение файла current_driver в /sys/devices/system/cpu/cpuidle приводит к выводу none). В этом случае второй вариант загрузки, processor.max_cstate=0, не нужен. Однако в документации говорится, что установка max_cstate в ноль для драйвера intel_idle должна вернуть ОС к драйверу acpi_idle. Поэтому я на всякий случай вставляю второй вариант загрузки.

Опция processor.max_cstate устанавливает максимальное состояние C для драйвера acpi_idle равным нулю, возможно, отключая его. У меня нет системы, на которой я могу это проверить, потому что intel_idle.max_cstate=0 полностью отключает драйвер cpuidle на всем доступном мне оборудовании. Однако, если ваша установка вернула вас из intel_idle в acpi_idle только с первой опцией загрузки, пожалуйста, дайте мне знать, если вторая опция, processor.max_cstate, сделала то, что было задокументировано, чтобы сделать в комментариях, чтобы я мог обновить этот ответ.

Наконец, последний из трех параметров idle=poll - это настоящая сила. Это отключит C1/C1E, что удалит последний оставшийся бит задержки за счет гораздо большего энергопотребления, поэтому используйте его только тогда, когда это действительно необходимо. Для большинства это будет излишним, поскольку задержка C1 * не так уж велика. Используя мое тестовое приложение, работающее на оборудовании, которое я описал в первоначальном вопросе, задержка возросла с 9 до 3 человек. Это, безусловно, является значительным сокращением для приложений, чувствительных к высокой задержке (например, финансовая торговля, высокоточная телеметрия/отслеживание, сбор данных с высокой частотой и т.д.), Но, возможно, не стоит потраченного удара электроэнергии для подавляющего большинства настольные приложения. Единственный способ узнать наверняка - это описать улучшение производительности вашего приложения по сравнению с фактическим увеличением потребления энергии/тепла вашего оборудования и взвесить компромиссы.

Update:

После дополнительного тестирования с различными параметрами idle=* я обнаружил, что установка idle в mwait, если поддерживается вашим оборудованием, является гораздо лучшей идеей. Похоже, что использование инструкций MWAIT/MONITOR позволяет процессору вводить C1E без каких-либо заметных задержек, добавляемых к времени пробуждения потока. С idle=mwait вы получите более низкую температуру процессора (по сравнению с idle=poll), меньшую потребляемую мощность и при этом сохраняете отличные низкие задержки в цикле простоя опроса. Поэтому мой обновленный рекомендуемый набор параметров загрузки для низкой задержки пробуждения потока процессора, основанный на следующих выводах:

intel_idle.max_cstate=0 processor.max_cstate=0 idle=mwait

Использование idle=mwait вместо idle=poll также может помочь в инициировании Turbo Boost (помогая процессору оставаться ниже его TDP [Thermal Design Power]) и гиперпоточности (для которой MWAIT является идеальным механизмом, не потребляющим всего физического ядра, пока в в то же время избегая более высоких состояний C). Однако это еще предстоит доказать в ходе испытаний, что я буду продолжать делать.

Обновление 2:

Параметр mwait idle был удален из более новых ядер 3.x (спасибо пользователю ck_ за обновление). Это оставляет нам два варианта:

idle=halt - должен работать так же, как mwait, но проверьте, чтобы убедиться, что это так с вашим оборудованием. Инструкция HLT практически эквивалентна MWAIT с подсказкой состояния 0. Проблема заключается в том, что для выхода из состояния HLT требуется прерывание, а запись в память (или прерывание) можно использовать для выхода из MWAIT. государство. В зависимости от того, что ядро ​​Linux использует в цикле ожидания, это может сделать MWAIT потенциально более эффективным. Итак, как я сказал, test/profile и посмотрите, соответствует ли он вашим потребностям в задержке ...

а также

idle=poll - Высочайшая производительность, за счет мощности и тепла.

93
Michael Goldshteyn

Возможно, что стало медленнее, так это futex, строительный блок для условных переменных. Это пролит некоторый свет:

strace -r ./test_latency 0 1 &> test_latency_strace & sleep 8 && killall test_latency

затем

for i in futex nanosleep rt_sig;do echo $i;grep $i test_latency_strace | sort -rn;done

который покажет микросекунды, взятые для интересных системных вызовов, отсортированные по времени.

На ядре 2.6.32

$ for i in futex nanosleep rt_sig;do echo $i;grep $i test_latency_strace | sort -rn;done
futex
 1.000140 futex(0x601ac4, FUTEX_WAKE_OP_PRIVATE, 1, 1, 0x601ac0, {FUTEX_OP_SET, 0, FUTEX_OP_CMP_GT, 1}) = 1
 1.000129 futex(0x601ac4, FUTEX_WAKE_OP_PRIVATE, 1, 1, 0x601ac0, {FUTEX_OP_SET, 0, FUTEX_OP_CMP_GT, 1}) = 1
 1.000124 futex(0x601ac4, FUTEX_WAKE_OP_PRIVATE, 1, 1, 0x601ac0, {FUTEX_OP_SET, 0, FUTEX_OP_CMP_GT, 1}) = 1
 1.000119 futex(0x601ac4, FUTEX_WAKE_OP_PRIVATE, 1, 1, 0x601ac0, {FUTEX_OP_SET, 0, FUTEX_OP_CMP_GT, 1}) = 1
 1.000106 futex(0x601ac4, FUTEX_WAKE_OP_PRIVATE, 1, 1, 0x601ac0, {FUTEX_OP_SET, 0, FUTEX_OP_CMP_GT, 1}) = 1
 1.000103 futex(0x601ac4, FUTEX_WAKE_OP_PRIVATE, 1, 1, 0x601ac0, {FUTEX_OP_SET, 0, FUTEX_OP_CMP_GT, 1}) = 1
 1.000102 futex(0x601ac4, FUTEX_WAKE_OP_PRIVATE, 1, 1, 0x601ac0, {FUTEX_OP_SET, 0, FUTEX_OP_CMP_GT, 1}) = 1
 0.000125 futex(0x7f98ce4c0b88, FUTEX_WAKE_PRIVATE, 2147483647) = 0
 0.000042 futex(0x601b00, FUTEX_WAKE_PRIVATE, 1) = 1
 0.000038 futex(0x601b00, FUTEX_WAKE_PRIVATE, 1) = 1
 0.000037 futex(0x601b00, FUTEX_WAKE_PRIVATE, 1) = 1
 0.000030 futex(0x601b00, FUTEX_WAKE_PRIVATE, 1) = 1
 0.000029 futex(0x601b00, FUTEX_WAKE_PRIVATE, 1) = 0
 0.000028 futex(0x601b00, FUTEX_WAKE_PRIVATE, 1) = 1
 0.000027 futex(0x601b00, FUTEX_WAKE_PRIVATE, 1) = 1
 0.000018 futex(0x7fff82f0ec3c, FUTEX_WAKE_PRIVATE, 1) = 0
nanosleep
 0.000027 nanosleep({1, 0}, {1, 0}) = 0
 0.000019 nanosleep({1, 0}, {1, 0}) = 0
 0.000019 nanosleep({1, 0}, {1, 0}) = 0
 0.000018 nanosleep({1, 0}, {1, 0}) = 0
 0.000018 nanosleep({1, 0}, {1, 0}) = 0
 0.000018 nanosleep({1, 0}, {1, 0}) = 0
 0.000018 nanosleep({1, 0}, 0x7fff82f0eb40) = ? ERESTART_RESTARTBLOCK (To be restarted)
 0.000017 nanosleep({1, 0}, {1, 0}) = 0
rt_sig
 0.000045 rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
 0.000040 rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
 0.000038 rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
 0.000035 rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
 0.000034 rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
 0.000033 rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
 0.000032 rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
 0.000032 rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
 0.000031 rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
 0.000031 rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
 0.000028 rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
 0.000028 rt_sigaction(SIGRT_1, {0x37f8c052b0, [], SA_RESTORER|SA_RESTART|SA_SIGINFO, 0x37f8c0e4c0}, NULL, 8) = 0
 0.000027 rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
 0.000027 rt_sigaction(SIGRTMIN, {0x37f8c05370, [], SA_RESTORER|SA_SIGINFO, 0x37f8c0e4c0}, NULL, 8) = 0
 0.000027 rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
 0.000025 rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
 0.000025 rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
 0.000023 rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
 0.000023 rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
 0.000022 rt_sigprocmask(SIG_UNBLOCK, [RTMIN RT_1], NULL, 8) = 0
 0.000022 rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
 0.000021 rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
 0.000021 rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
 0.000021 rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
 0.000021 rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
 0.000021 rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
 0.000019 rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0

На ядре 3.1.9

$ for i in futex nanosleep rt_sig;do echo $i;grep $i test_latency_strace | sort -rn;done
futex
 1.000129 futex(0x601764, FUTEX_WAKE_OP_PRIVATE, 1, 1, 0x601760, {FUTEX_OP_SET, 0, FUTEX_OP_CMP_GT, 1}) = 1
 1.000126 futex(0x601764, FUTEX_WAKE_OP_PRIVATE, 1, 1, 0x601760, {FUTEX_OP_SET, 0, FUTEX_OP_CMP_GT, 1}) = 1
 1.000122 futex(0x601764, FUTEX_WAKE_OP_PRIVATE, 1, 1, 0x601760, {FUTEX_OP_SET, 0, FUTEX_OP_CMP_GT, 1}) = 1
 1.000115 futex(0x601764, FUTEX_WAKE_OP_PRIVATE, 1, 1, 0x601760, {FUTEX_OP_SET, 0, FUTEX_OP_CMP_GT, 1}) = 1
 1.000114 futex(0x601764, FUTEX_WAKE_OP_PRIVATE, 1, 1, 0x601760, {FUTEX_OP_SET, 0, FUTEX_OP_CMP_GT, 1}) = 1
 1.000112 futex(0x601764, FUTEX_WAKE_OP_PRIVATE, 1, 1, 0x601760, {FUTEX_OP_SET, 0, FUTEX_OP_CMP_GT, 1}) = 1
 1.000109 futex(0x601764, FUTEX_WAKE_OP_PRIVATE, 1, 1, 0x601760, {FUTEX_OP_SET, 0, FUTEX_OP_CMP_GT, 1}) = 1
 0.000139 futex(0x3f8b8f2fb0, FUTEX_WAKE_PRIVATE, 2147483647) = 0
 0.000043 futex(0x601720, FUTEX_WAKE_PRIVATE, 1) = 1
 0.000041 futex(0x601720, FUTEX_WAKE_PRIVATE, 1) = 1
 0.000037 futex(0x601720, FUTEX_WAKE_PRIVATE, 1) = 1
 0.000036 futex(0x601720, FUTEX_WAKE_PRIVATE, 1) = 1
 0.000034 futex(0x601720, FUTEX_WAKE_PRIVATE, 1) = 1
 0.000034 futex(0x601720, FUTEX_WAKE_PRIVATE, 1) = 1
nanosleep
 0.000025 nanosleep({1, 0}, 0x7fff70091d00) = 0
 0.000022 nanosleep({1, 0}, {0, 3925413}) = ? ERESTART_RESTARTBLOCK (Interrupted by signal)
 0.000021 nanosleep({1, 0}, 0x7fff70091d00) = 0
 0.000017 nanosleep({1, 0}, 0x7fff70091d00) = 0
 0.000017 nanosleep({1, 0}, 0x7fff70091d00) = 0
 0.000017 nanosleep({1, 0}, 0x7fff70091d00) = 0
 0.000017 nanosleep({1, 0}, 0x7fff70091d00) = 0
 0.000017 nanosleep({1, 0}, 0x7fff70091d00) = 0
rt_sig
 0.000045 rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
 0.000044 rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
 0.000043 rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
 0.000040 rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
 0.000038 rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
 0.000037 rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
 0.000036 rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
 0.000036 rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
 0.000035 rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
 0.000035 rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
 0.000035 rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
 0.000035 rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
 0.000034 rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
 0.000031 rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
 0.000027 rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
 0.000027 rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
 0.000027 rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
 0.000027 rt_sigaction(SIGRT_1, {0x3f892067b0, [], SA_RESTORER|SA_RESTART|SA_SIGINFO, 0x3f8920f500}, NULL, 8) = 0
 0.000026 rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
 0.000026 rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
 0.000025 rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
 0.000024 rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
 0.000023 rt_sigprocmask(SIG_UNBLOCK, [RTMIN RT_1], NULL, 8) = 0
 0.000023 rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
 0.000022 rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
 0.000021 rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
 0.000019 rt_sigaction(SIGRTMIN, {0x3f89206720, [], SA_RESTORER|SA_SIGINFO, 0x3f8920f500}, NULL, 8) = 0

Я нашел это 5-летний отчет об ошибке , который содержит тест производительности "пинг-понг", который сравнивает

  1. однопоточный мьютекс libpthread
  2. переменная условия libpthread
  3. простые старые сигналы Unix

Я должен был добавить

#include <stdint.h>

для того, чтобы скомпилировать, что я сделал с этой командой

g++ -O3 -o condvar-perf condvar-perf.cpp -lpthread -lrt

На ядре 2.6.32

$ ./condvar-perf 1000000
NPTL
mutex                 elapsed:    29085 us; per iteration:   29 ns / 9.4e-05 context switches.
c.v. ping-pong test   elapsed:  4771993 us; per iteration: 4771 ns / 4.03 context switches.
signal ping-pong test elapsed:  8685423 us; per iteration: 8685 ns / 4.05 context switches.

На ядре 3.1.9

$ ./condvar-perf 1000000
NPTL
mutex                 elapsed:    26811 us; per iteration:   26 ns / 8e-06 context switches.
c.v. ping-pong test   elapsed: 10930794 us; per iteration: 10930 ns / 4.01 context switches.
signal ping-pong test elapsed: 10949670 us; per iteration: 10949 ns / 4.01 context switches.

Я пришел к выводу, что между ядром 2.6.32 и 3.1.9 переключение контекста действительно замедлилось, хотя и не так сильно, как вы наблюдаете в ядре 3.2. Я понимаю, что это еще не ответит на ваш вопрос, я буду продолжать копать.

Правка: Я обнаружил, что изменение приоритета процесса в реальном времени (оба потока) повышает производительность на 3.1.9, чтобы соответствовать 2.6.32. Тем не менее, установка того же приоритета на 2.6.32 замедляет его ... пойди разберись - я посмотрю подробнее.

Вот мои результаты сейчас:

На ядре 2.6.32

$ ./condvar-perf 1000000
NPTL
mutex                 elapsed:    29629 us; per iteration:   29 ns / 0.000418 context switches.
c.v. ping-pong test   elapsed:  6225637 us; per iteration: 6225 ns / 4.1 context switches.
signal ping-pong test elapsed:  5602248 us; per iteration: 5602 ns / 4.09 context switches.
$ chrt -f 1 ./condvar-perf 1000000
NPTL
mutex                 elapsed:    29049 us; per iteration:   29 ns / 0.000407 context switches.
c.v. ping-pong test   elapsed: 16131360 us; per iteration: 16131 ns / 4.29 context switches.
signal ping-pong test elapsed: 11817819 us; per iteration: 11817 ns / 4.16 context switches.
$ 

На ядре 3.1.9

$ ./condvar-perf 1000000
NPTL
mutex                 elapsed:    26830 us; per iteration:   26 ns / 5.7e-05 context switches.
c.v. ping-pong test   elapsed: 12812788 us; per iteration: 12812 ns / 4.01 context switches.
signal ping-pong test elapsed: 13126865 us; per iteration: 13126 ns / 4.01 context switches.
$ chrt -f 1 ./condvar-perf 1000000
NPTL
mutex                 elapsed:    27025 us; per iteration:   27 ns / 3.7e-05 context switches.
c.v. ping-pong test   elapsed:  5099885 us; per iteration: 5099 ns / 4 context switches.
signal ping-pong test elapsed:  5508227 us; per iteration: 5508 ns / 4 context switches.
$ 
8
amdn

Вы также можете увидеть процессоры, нажимающие вниз в более поздних процессах и ядрах Linux из-за драйвера pstate, который отделен от c-состояний. Так что кроме того, чтобы отключить это, вам нужен следующий параметр ядра:

intel_pstate=disable

0
Kyle Brandt