Comments 15
Пара операций lock/unlock для критической секции в Windows работает около 23.5 наносекунд (на вышеуказанном процессоре).
Это сферический тайминг в вакууме. В реальных условиях, когда пара ядер дергает одну блокировку, при каждом дергании происходит ещё сброс соответствующих кэш-линий у «соседнего» ядра, что очень больно по времени. Даже по графикам видно, что как только частота обращений к блокировкам превышает пару МГц, всё — сушите вёсла, безотносительно длительности собственно критической секции. Тупо подсистема памяти больше не вытягивает, не смотря на все гигагерцы.
>когда пара ядер дергает одну блокировку, при каждом дергании происходит ещё сброс соответствующих кэш-линий у «соседнего» ядра
что это значит? если под понятием «дергании» имеется ввиду операция попытки захвата, то это несколько не так: сброс кеш-линии будет происходить только при операции записи значения «счетчика», а попытка захвата — лишь чтение изначально.
Да, при успешной записи L1-кеш у каждого ядра обновится и общий L2 — тоже. Однако это справедливо для любой операции записи.
И вот здесь самое интересное: мгновенный сброс кеш-линий (или эмуляция оного для юзера) для каждой записи возможен, но только если существует total-store-order между потоками. эта очень вещь затратная и все стараются такие операции записи обходить. здесь уже появляется lock-free код.
критическая секция в Win32 устроена внутри как CAS-цикл, т.е. захват может не всегда быть удачным при первой и последующих попытках, но цикл будет продолжаться. Таким образом, попытка захвата — лишь чтение изначально, но не запись.
графики показывают очень важную вещь (что есть правда) — чем больше кол-во конкурирующих между собой потоков — тем больше проседает система.
здесь необходимо сделать ремарку: lock-free является вещью опасной, т.к. оно еще плохо сочетается с планировщиком ОС. Если система должна быть высококонкурентной, то нужно пользоваться SwitchToThread и т.п. (использовать что-то наподобие spin-wait), иначе
>накладные расходы на блокировки начинают влиять на общую производительность
что это значит? если под понятием «дергании» имеется ввиду операция попытки захвата, то это несколько не так: сброс кеш-линии будет происходить только при операции записи значения «счетчика», а попытка захвата — лишь чтение изначально.
Да, при успешной записи L1-кеш у каждого ядра обновится и общий L2 — тоже. Однако это справедливо для любой операции записи.
И вот здесь самое интересное: мгновенный сброс кеш-линий (или эмуляция оного для юзера) для каждой записи возможен, но только если существует total-store-order между потоками. эта очень вещь затратная и все стараются такие операции записи обходить. здесь уже появляется lock-free код.
критическая секция в Win32 устроена внутри как CAS-цикл, т.е. захват может не всегда быть удачным при первой и последующих попытках, но цикл будет продолжаться. Таким образом, попытка захвата — лишь чтение изначально, но не запись.
графики показывают очень важную вещь (что есть правда) — чем больше кол-во конкурирующих между собой потоков — тем больше проседает система.
здесь необходимо сделать ремарку: lock-free является вещью опасной, т.к. оно еще плохо сочетается с планировщиком ОС. Если система должна быть высококонкурентной, то нужно пользоваться SwitchToThread и т.п. (использовать что-то наподобие spin-wait), иначе
>накладные расходы на блокировки начинают влиять на общую производительность
Давайте с начала.
Даже если процессор в компе физически один, всё равно вокруг него куча микросхем, которые умеют лазить в память напрямую. Это GPU, это чипсет, это всякие втыкаемые контроллеры с DMA и т.д. и т.п.
Даже если в этих микросхемах нет собственного кэша (что вряд ли), они всё равно обязаны соблюдать протоколы когерентности кэшей CPU.
Ну и сам процессор должен соблюдать свои же протоколы, что логично.
Дальше вот что: при любых примитивах синхронизации ближайшим общим хранилищем, через которую эту синхронизацию можно делать, является оперативная память, она же DRAM.
Соответственно, все «атомарные» инструкции типа CAS/XCHG/wtf это просто «синтаксический сахар» для старых добрых наборов микроопераций с префиксом LOCK, которые были доступны ещё на 8086. Смысл в том, что процессор захватывает доступ к DRAM на время выполнения нескольких операций. И потом начинает своё «читать — проверить — модифицировать», не пуская на шину памяти никого другого.
Если захват не удался — да, действительно, «ничего не происходит». Но если удался — то все кэши всех микросхем, подключенных к данной DRAM, инвалидируются по указанной строке.
Проблема с DRAM в том, что она слишком медленная просто из-за архитектуры. Вы не можете взять и прочитать 1 байт памяти, как во времена Z80. Современные процессоры хапают сразу как минимум пакет из 64 байт. И это не потому, что проектировщики дебилы, просто слишком долго распространяется и обрабатывается сигнал запроса. По сути, процессор делает запрос, потом долго-долго ждёт, пока память его переварит, и только потом память буквально «выплёвывает» ответ.
При росте частот инженеры как могли спасали пропускную способность памяти. Но задержки (латенси) так и остались на уровнях 30-летней давности. А для коротких блокировок критичны как раз задержки.
Даже если процессор в компе физически один, всё равно вокруг него куча микросхем, которые умеют лазить в память напрямую. Это GPU, это чипсет, это всякие втыкаемые контроллеры с DMA и т.д. и т.п.
Даже если в этих микросхемах нет собственного кэша (что вряд ли), они всё равно обязаны соблюдать протоколы когерентности кэшей CPU.
Ну и сам процессор должен соблюдать свои же протоколы, что логично.
Дальше вот что: при любых примитивах синхронизации ближайшим общим хранилищем, через которую эту синхронизацию можно делать, является оперативная память, она же DRAM.
Соответственно, все «атомарные» инструкции типа CAS/XCHG/wtf это просто «синтаксический сахар» для старых добрых наборов микроопераций с префиксом LOCK, которые были доступны ещё на 8086. Смысл в том, что процессор захватывает доступ к DRAM на время выполнения нескольких операций. И потом начинает своё «читать — проверить — модифицировать», не пуская на шину памяти никого другого.
Если захват не удался — да, действительно, «ничего не происходит». Но если удался — то все кэши всех микросхем, подключенных к данной DRAM, инвалидируются по указанной строке.
Проблема с DRAM в том, что она слишком медленная просто из-за архитектуры. Вы не можете взять и прочитать 1 байт памяти, как во времена Z80. Современные процессоры хапают сразу как минимум пакет из 64 байт. И это не потому, что проектировщики дебилы, просто слишком долго распространяется и обрабатывается сигнал запроса. По сути, процессор делает запрос, потом долго-долго ждёт, пока память его переварит, и только потом память буквально «выплёвывает» ответ.
При росте частот инженеры как могли спасали пропускную способность памяти. Но задержки (латенси) так и остались на уровнях 30-летней давности. А для коротких блокировок критичны как раз задержки.
про RMW и блокировку шины я подразумевал
>мгновенный сброс кеш-линий (или эмуляция оного для юзера)
То, что вы написали — правильно, но никак не отвечает на мой вопрос. Кеш-линии и так будут инвалидироваться при операциях записи. Мой поинт в том, что не каждая попытка захвата будет сбрасывать кеш-линию.
False sharing на уровне кешей L1 и L2 будет все равно.
Причем же тут «вина» блокировок? Обычно реализации и так используют выровненный доступ по размеру кеш-линии, чтобы случайно не аффектить приложение.
>мгновенный сброс кеш-линий (или эмуляция оного для юзера)
То, что вы написали — правильно, но никак не отвечает на мой вопрос. Кеш-линии и так будут инвалидироваться при операциях записи. Мой поинт в том, что не каждая попытка захвата будет сбрасывать кеш-линию.
False sharing на уровне кешей L1 и L2 будет все равно.
Причем же тут «вина» блокировок? Обычно реализации и так используют выровненный доступ по размеру кеш-линии, чтобы случайно не аффектить приложение.
Все верно, ну а как вы предлагаете жить без блокировок если они нужны? LockFree не панацея. Более того, на моей практике он всегда оказывался либо такой же, по скорости, как и код с блокировками (например легкими, типа Spin), либо медленнее, так как LockFree банально тяжелее и сложнее. LockFree используется в специфических задачах, в основном в ядре, там, где любой поток может быть снят без уведомления и нельзя использовать блокировку (как абстракцию) вообще.
А причем тут кеш-линии, вы думаете что при атомарной изменении одной ячейки они не будут сбрасываться?
Данный синтетически тест работает с очень маленьким набором данных — сброс L1 кэшей в нем не проявляет себя. Если же у вас нормальное приложение, которое действительно делает что-то (активно обращается к разным данным в памяти), то там картина может быть совсем другая.
PS: Если честно, то для меня название стаитьи звучит зак злая ирония — последние несколько недель работаю с hi-concurrency — там лишняя блокировка это сразу сильные потери скорости. С другой стороны там и условия весьма специфические — требуется «прокачивать» через очередь >10M messages/sec.
PS: Если честно, то для меня название стаитьи звучит зак злая ирония — последние несколько недель работаю с hi-concurrency — там лишняя блокировка это сразу сильные потери скорости. С другой стороны там и условия весьма специфические — требуется «прокачивать» через очередь >10M messages/sec.
то там картина может быть совсем другая.
Может. Если у нас код, работающий при захваченной блокировке, обращается к данным, расположенным в разных участках памяти, он сбрасывает/вымывает кеши и так и так, что с блокировкой, что без нее, верно? Обращение к еще одному адресу, стоящему особняком(блокировке), не должно привести к сильному проседанию. Тем более, что spin-lock блокировки может быть настроен на определенное количество обращений.
Если он обращается к разным участкам памяти. Но ведь наиболее частый сценарий — относительно небольшая область используемой памяти, либо обращение к памяти более-менее последовательно. Именно под эти сценарии в первую очередь оптимизируются современные процессоры. И как раз вот эта оптимизация и страдает, если у вас блокировки происходят часто. А вот на сколько страдает — нужно мерить для конкретного приложения реальный сценарий использования. Это как раз та область, где сами по себе синтетические тесты дают лишь пищу для размышлений, но никак не однозначный ответ что лучше, а что хуже. Тест из статьи — пример плохого, вредного теста. Но плохой он не из-за конструкции теста, а из-за интерпретации результатов и совершаемых выводов.
Но ведь наиболее частый сценарий — относительно небольшая область используемой памяти, либо обращение к памяти более-менее последовательно.
Это, право слово, очень вольное допущение.
Именно под эти сценарии в первую очередь оптимизируются современные процессоры.
Не совсем. Этот сценарий в принципе, в отличие от произвольного доступа, возможно оптимизировать, и он оптимизируется. Что в свою очередь заставляет программистов располагать данные и обращаться к ним определенным образом. Здесь нельзя однозначно сказать, где причина, где следствие.
И как раз вот эта оптимизация и страдает, если у вас блокировки происходят часто.
Простите, а вы блокировкой защищаете единичную переменную или массив, который проходите последовательно?
Я к тому, что надо говорить про каждый конретный случай отдельно. В вашем случае — да, страдает. Но lock-free тоже требуют накладных расходов, в зависимости от реализации может быть копирование с опять же выбивание кеша и т.д.
Типичный случай для меня сейчас в текущей работе, например, защита линейного отсортированного массива(пара строка-объект), поиск в котором производится методом дихотомии. При большом размере коллекции скачки поначалу происходят по очень разным адресам, там не то что кеш ЦП будет играть свою скрипку, а банально подкачка страниц.
Тест из статьи — пример плохого, вредного теста. Но плохой он не из-за конструкции теста, а из-за интерпретации результатов и совершаемых выводов.
Скажем так — это средний вывод для среднего теста. Который — да — не учитывает другие факторы, которые могут влиять в другой ситуации.
Зашёл прочитать про торжество РосКомНадзора. Приятно удивился, поставил плюс.
Чуть не пропустил статью из-за заголовка — можно решить, что снова о блокировках в Интернете.
В статье не хватает зависимости throughput'а от настроек spinlock'а. А разница бывает значительной.
Суть статьи в том, что это не блокировки медленные, а программисты неправильно их используют.
А все примеры говорят о том, что если много потоков требуют блокировок одних и тех же данных, то стоит проверить вариант пре- и постпроцессингом данных, для уменьшения количества и времени захвата блокировок.
PS. Почему на 60% подает производительность 4 потоков?
А все примеры говорят о том, что если много потоков требуют блокировок одних и тех же данных, то стоит проверить вариант пре- и постпроцессингом данных, для уменьшения количества и времени захвата блокировок.
PS. Почему на 60% подает производительность 4 потоков?
Sign up to leave a comment.
Блокировки работают не так уж медленно