Главная » Хабрахабр » [Перевод] Почему процессоры Skylake иногда работают в 2 раза медленнее

[Перевод] Почему процессоры Skylake иногда работают в 2 раза медленнее

Мне сообщили, что на новых компьютерах некоторые регрессиионные тесты стали медленнее. Обычное дело, такое бывает. Неправильная конфигурация где-то в Windows или не самые оптимальные значения в BIOS. Но в этот раз нам никак не удавалось найти ту самую «сбитую» настройку. Поскольку изменение значительное: 9 против 19 секунд (на графике синий — это старое железо, а оранжевый — новое), то пришлось копать глубже.

Падение производительности с 9,1 до 19,6 секунд определённо можно назвать значительным. Мы провели дополнительные проверки со сменой версий тестируемых программ, Windows и настроек BIOS. Но нет, результат не изменился. Единственная разница проявлялась только на разных процессорах. Ниже представлен результат на новейшем CPU.

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

Если вы покупаете новейшее железо, то получите процессор с архитектурой Skylake. Xeon Gold работает на другой архитектуре под названием Skylake, общей для новых процессоров Intel с середины 2017 года. Это хорошие машины, но, как показали тесты, новизна и быстрота — не одно и то же.

Проведём тест на старом и новом оборудовании и получим примерно такое: Если больше ничего не помогает, то надо использовать профайлер для глубокого исследования.

Отрицательная разница в таблице соответствует увеличению потребления CPU в более медленном тесте. Вкладка в Windows Performance Analyzer (WPA) показывает в таблице разницу между Trace 2 (11 с) и Trace 1 (19 с). SpinWait. Если посмотреть на самые значительные различия в потреблении CPU, то мы увидим AwareLock::Contention, JIT_MonEnterWorker_InlineGetThread_GetThread_PatchLabel и ThreadNative. пер.], когда потоки борются за блокировки. Всё указывает на «спиннинг» в CPU [спиннинг (spinning) — циклическая попытка получить блокировку, прим. Усиленная конкуренция за блокировки означает, что нечто в нашем программном обеспечении замедлилось и удержало блокировку, что как следствие привело к усилению спиннинга в CPU. Но это ложный след, потому что спиннинг не является основной причиной снижения производительности. Хотя это не логично, но я вернулся к увеличению нагрузки на CPU в различных методах. Я проверял время блокировки и другие ключевые показатели, такие как показатели диска, но не удалось найти ничего значимого, что могло бы объяснить снижение производительности.

В WPA есть столбцы file # и line #, но они работают только с приватными символами, которых у нас нет, потому что это код . Было бы интересно найти, где именно застревает процессор. Следующее лучшее, что мы можем сделать — получить адрес dll, где находится инструкция под названием Image RVA. NET Framework. Если загрузить эту dll в отладчик и сделать

u xxx.dll+ImageRVA

то мы должны увидеть инструкцию, которая сжигает большинство циклов CPU, потому что это будет единственный «горячий» адрес.

Изучим этот адрес различными методами Windbg:

AwareLock::Contention+0x135:
00007ff8`0535565b f00f4cc6 lock cmovl eax,esi
00007ff8`0535565f 2bf0 sub esi,eax
00007ff8`05355661 eb01 jmp clr! 0:000> u clr.dll+0x19566B-10
clr! AwareLock::Contention+0x144 (00007ff8`0535566e)
00007ff8`05355669 f390 pause
00007ff8`0535566b ebf7 jmp clr! AwareLock::Contention+0x13f (00007ff8`05355664)
00007ff8`05355663 cc int 3
00007ff8`05355664 83e801 sub eax,1
00007ff8`05355667 7405 je clr! AwareLock::Contention+0x13f (00007ff8`05355664)

И различными методами JIT:

JIT_MonEnterWorker_InlineGetThread_GetThread_PatchLabel+0x124:
00007ff8`051c27f1 5e pop rsi
00007ff8`051c27f2 c3 ret
00007ff8`051c27f3 833d0679930001 cmp dword ptr [clr!g_SystemInfo+0x20 (00007ff8`05afa100)],1
00007ff8`051c27fa 7e1b jle clr! 0:000> u clr.dll+0x2801-10
clr! JIT_MonEnterWorker_InlineGetThread_GetThread_PatchLabel+0x132 (00007ff8`051c27ff)
JIT_MonEnterWorker_InlineGetThread_GetThread_PatchLabel+0x14a (00007ff8`051c2817)
00007ff8`051c27fc 418bc2 mov eax,r10d
00007ff8`051c27ff f390 pause
00007ff8`051c2801 83e801 sub eax,1
00007ff8`051c2804 75f9 jne clr!

В одном случае горячий адрес является инструкцией jump, а в другом случае это вычитание. Теперь у нас есть шаблон. Разные методы выполняют одну и ту же инструкцию процессора, который по какой-то причине выполняется очень долго. Но обеим горячим инструкциям предшествует одна и та же общая инструкция pause. Давайте замерим скорость выполнения инструкции pause и посмотрим, правильно ли мы рассуждаем.

CPU

pause в наносекундах

Xeon E5 1620v3 3,5 ГГц

4

Xeon® Gold 6126 @ 2,60 ГГц

43

Pause в новых процессорах Skylake выполняется на порядок дольше. Конечно, что угодно может стать быстрее, а иногда и немного медленнее. Но в десять раз медленнее? Это скорее похоже на баг. Небольшой поиск в интернете об инструкции паузы приводит к руководству Intel, где явно упоминаются микроархитектура Skylake и инструкция паузы:

Есть даже страница с указанием времени выполнения почти всех инструкций процессора. Нет, это не ошибка, это документированная функция.

  • Sandy Bridge     11
  • Ivy Bridege         10
  • Haswell               9
  • Broadwell            9
  • SkylakeX             141

Здесь указано количество циклов процессора. Для вычисления фактического времени нужно разделить количество циклов на частоту процессора (обычно в ГГц) и получить время в наносекундах.

NET на последнем железе, то они могут работать намного медленнее. Это означает, что если запустить сильно многопоточные приложения на . Проблему исправили в . Кто-то уже заметил это и ещё в августе 2017 года зарегистрировал баг. 1 и . NET Core 2. 8 Preview. NET Framework 4.

[495945, mscorlib.dll, Баг] Улучшенный spin-wait в нескольких примитивах синхронизации для лучшего выполнения на Intel Skylake и более поздних микроархитектурах.

Но поскольку до выхода .NET 4.8 ещё год, я попросил бэкпортировать исправления, чтобы .NET 4.7.2 вернулся к нормальной скорости на новых процессорах. Поскольку взаимоисключающая блокировка (спинлок) есть во многих частях .NET, то следует отследить увеличенную нагрузку на CPU при работе Thread.SpinWait и других методов спиннинга.

Result внутренне использует спиннинг, так что я предвижу существенный рост нагрузки на CPU и снижение производительности и в других тестах. Например, Task.

Я посмотрел код .NET Core на предмет того, сколько процессор будет продолжать спиннинг, если блокировка не освобождена, прежде чем вызывать WaitForSingleObject для оплаты «дорогостоящего» переключения контекста. Переключатель контекста занимает где-то микросекунду или гораздо больше, если много потоков ожидают один и тот же объект ядра.

NET-блокировки умножают максимальную продолжительность спиннинга на количество ядер, если брать абсолютный случай, где поток на каждом ядре ожидает одной и той же блокировки, а спиннинг продолжается достаточно долго, чтобы все немного поработали, прежде чем оплатить вызов ядра. . NET использует алгоритм экспоненциальной выдержки, когда он начинается с цикла из 50-ти вызовов pause, где для каждой итерации количество спинов утраивается, пока следующий счётчик спинов не превысит их максимальную продолжительность. Спиннинг в . Я подсчитал общую продолжительность спиннинга в расчёте на процессор для различных процессоров и разного количества ядер:

NET Locks: Ниже упрощённый код спиннинга в .

/// <summary>
/// This is how .NET is spinning during lock contention minus the Lock taking/SwitchToThread/Sleep calls
/// </summary>
/// <param name="nCores"></param>
void Spin(int nCores)
duration *= dwBackOffFactor; } while (duration < dwMaximumDuration); }
}

Раньше время спиннинга находилось в миллисекундном интервале (19 мс для 24 ядер), что уже немало по сравнению с упоминавшимся временем переключения контекста, которое на порядок быстрее. Но в процессорах Skylake общее для процессора время спиннинга просто взрывается до 246 мс на 24-х или 48-ядерной машине просто потому, что инструкция pause замедлилась в 14 раз. Это действительно так? Я написал небольшой тестер для проверки общего спиннинга на CPU — и рассчитанные цифры хорошо соответствуют ожиданиям. Вот 48 потоков на 24-ядерном CPU, ожидающих одну блокировку, которую я назвал Monitor.PulseAll:

Это экспериментальное доказательство того, что у нас действительно есть проблема с нагрузкой на CPU и очень долгий спиннинг реален. Только один поток выиграет гонку, но 47 продолжат спиннинг до потери пульса. Причина спиннинга — попытка быстрее получить блокировку без обращения к ядру. Он подрывает масштабируемость, потому что эти циклы идут вместо полезной работы других потоков, хотя инструкция pause освобождает некоторые общие ресурсы CPU, обеспечивая засыпание на более продолжительное время. Но тесты показали снижение производительности в почти однопоточных операциях, где один поток добавляет что-то в рабочую очередь, в то время как рабочий поток ожидает результат, а затем выполняет некую задачу с рабочим элементом. Если так, то увеличение нагрузки на CPU было бы лишь номинальным, но вообще не влияло на производительность, потому что ядра занимаются другими задачами.

Спин для состязательной блокировки происходит с утроением спиннинга на каждом шаге. Причину проще всего показать на схеме. Хотя спиннинг пытается быть честным и время от времени переключается на другие потоки, чтобы помочь им завершить свою работу. После каждого раунда блокировка снова проверяется на предмет того, может ли текущий поток её получить. Проблема в том, что проверка на взятие возможна только по завершении полного спин-раунда: Это увеличивает шансы на освобождение блокировки при следующей проверке.

Вычислив длительность спина последнего раунда, можно оценить худший случай задержки, для нашего потока: Например, если в момент начала пятого спин-раунда блокировка сигнализирует о доступности, взять её можно только по завершении раунда.

Это реальная проблема? Много миллисекунд ожидания до окончания спиннинга.

Этого достаточно, чтобы увидеть эффект: Я создал простое тестовое приложение, в котором реализована очередь производителей-потребителей, где рабочий поток выполняет каждый элемент работы 10 мс, а у потребителя задержка 1-9 мс перед следующим рабочим элементом.

Это показывает, что чрезмерный спиннинг на CPU — не просто косметическая проблема чрезмерно многопоточных приложений. Мы видим для задержек в 1-2 мс общую продолжительность 2,2-2,3 с, тогда как в других случаях работа выполняется быстрее вплоть до 1,2 с. Для прогона выше данные ETW говорят сами за себя: именно рост спиннинга является причиной наблюдаемой задержки: Она реально вредит простой поточности производителя-потребителя, включающей только два потока.

Если внимательно посмотреть на секцию с «тормозами», мы увидим в красной области 11 мс спиннинга, хотя воркер (светло-синий) завершил свою работу и давно отдал блокировку.

Быстрый недегенативный случай выглядит намного лучше, здесь только 1 мс тратится на спиннинг для блокировки.

В zip-архиве собраны исходный код и двоичные файлы для . Я использовал тестовое приложение SkylakeXPause. NET 4. NET Core и . Для проведения сравнения я установил . 5. 8 Preview с исправлениями и . NET 4. 0, который по-прежнему реализует старое поведение. NET Core 2. NET Standard 2. Приложение предназначено для . NET 4. 0 и . Теперь можно проверить старое и новое поведение спиннинга бок о бок без необходимости что-либо исправлять, так очень удобно. 5, производящих и exe, и dll.

readonly object _LockObject = new object();
int WorkItems;
int CompletedWorkItems;
Barrier SyncPoint; void RunSlowTest()
{ const int processingTimeinMs = 10; const int WorkItemsToSend = 100; Console.WriteLine($"Worker thread works {processingTimeinMs} ms for {WorkItemsToSend} times"); // Test one sender one receiver thread with different timings when the sender wakes up again // to send the next work item // synchronize worker and sender. Ensure that worker starts first double[] sendDelayTimes = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; foreach (var sendDelay in sendDelayTimes) { SyncPoint = new Barrier(2); // one sender one receiver var sw = Stopwatch.StartNew(); Parallel.Invoke(() => Sender(workItems: WorkItemsToSend, delayInMs: sendDelay), () => Worker(maxWorkItemsToWork: WorkItemsToSend, workItemProcessTimeInMs: processingTimeinMs)); sw.Stop(); Console.WriteLine($"Send Delay: {sendDelay:F1} ms Work completed in {sw.Elapsed.TotalSeconds:F3} s"); Thread.Sleep(100); // show some gap in ETW data so we can differentiate the test runs }
} /// <summary>
/// Simulate a worker thread which consumes CPU which is triggered by the Sender thread
/// </summary>
void Worker(int maxWorkItemsToWork, double workItemProcessTimeInMs)
{ SyncPoint.SignalAndWait(); while (CompletedWorkItems != maxWorkItemsToWork) { lock (_LockObject) { if (WorkItems == 0) { Monitor.Wait(_LockObject); // wait for work } for (int i = 0; i < WorkItems; i++) { CompletedWorkItems++; SimulateWork(workItemProcessTimeInMs); // consume CPU under this lock } WorkItems = 0; } }
} /// <summary>
/// Insert work for the Worker thread under a lock and wake up the worker thread n times
/// </summary>
void Sender(int workItems, double delayInMs)
{ CompletedWorkItems = 0; // delete previous work SyncPoint.SignalAndWait(); for (int i = 0; i < workItems; i++) { lock (_LockObject) { WorkItems++; Monitor.PulseAll(_LockObject); } SimulateWork(delayInMs); }
}

Это не проблема .NET. Затронуты все реализации спинлока, использующие инструкцию pause. Я по-быстрому проверил ядро Windows Server 2016, но там на поверхности нет такой проблемы. Похоже, Intel была достаточно любезна — и намекнула, что необходимы некоторые изменения в подходе к спиннингу.

NET Core сообщили в августе 2017 года, а уже в сентябре 2017 года вышел патч и версия . О баге для . 0. NET Core 2. По ссылке видна не только великолепная реакция группы . 3. К сожалению, Desktop . NET Core, но и то, что несколько дней назад проблема устранена в основной ветке, а также дискуссия о дополнительных оптимизациях спиннинга. NET Framework 4. NET Framework двигается не так быстро, но в лице . Теперь я жду бэкпорт для . 8 Preview у нас есть хотя бы концептуальное доказательство, что исправления там тоже реализуемы. 7. NET 4. NET на полной скорости и на последнем железе. 2, чтобы использовать . ETW остаётся основным профайлером в Windows. Это первый найденный мною баг, который непосредственно связан с изменением производительности из-за одной инструкции CPU. Там недавно добавили интересные возможности ядра, но инструментов анализа вроде WPA до сих пор нет. Если бы я мог, то попросил бы Microsoft портировать инфраструктуру ETW на Linux, потому что текущие профайлеры в Linux по-прежнему отстойные.

NET Core 2. Если вы работаете с . NET Framework на последних процессорах, которые выпускались с середины 2017 года, то в случае проблем со снижением производительности следует обязательно проверить свои приложения профайлером — и обновиться на . 0 или десктопным . NET Desktop. NET Core и, надеюсь, вскоре на . Моё тестовое приложение скажет вам о наличии или отсутствии проблемы.

0>dotnet SkylakeXPause.dll -check
Did call pause 1,000,000 in 3. D:\SkylakeXPause\bin\Release\netcoreapp2. 5990 ms, Processors: 8
No SkylakeX problem detected

или

6195 ms, Processors: 8
No SkylakeX problem detected D:\SkylakeXPause\SkylakeXPause\bin\Release\net45>SkylakeXPause.exe -check
Did call pause 1,000,000 in 3.

NET Framework без соответствующего апдейта и на процессоре Skylake. Инструмент сообщит о проблеме, если вы работаете на .

Чтобы по-настоящему понять проблему, нужно создать средство её воспроизведения, позволяющее экспериментировать и искать влияющие факторы. Надеюсь, вам расследование этой проблемы показалось настолько же увлекательным, как и мне. Остальное — просто скучная работа, но теперь я намного лучше разбираюсь в причинах и последствиях циклической попытки получить блокировку в CPU.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Создание пакетов для Kubernetes с Helm: структура чарта и шаблонизация

Теперь подойдём к практике с другой стороны — с точки зрения создателя чартов (т.е. Про Helm и работу с ним «в общем» мы рассказали в прошлой статье. И хотя эта статья пришла из мира эксплуатации, она получилась больше похожей на ...

[Из песочницы] Экономим на RAID-контроллере, или как накормить Варю иопсами

В наш век облачных сервисов, AWS Lambda и прочих шаред хостингов абсолютно неосязаемых вычислительных ресурсов иногда хочется немножко своего. Кроме желания, иногда бывают и потребности вдумчиво покрутить тот или иной программный продукт с минимальными затратами на платформу. Найти какие-то излишки ...