Какова цель инструкции "PAUSE" в x86?

Я пытаюсь создать тупую версию спин-блокировки. Просматривая веб-страницы, я наткнулся на инструкцию по сборке под названием "PAUSE" в x86, которая используется, чтобы дать подсказку процессору, который в данный момент выполняет спин-блокировку на этом процессоре. Руководство Intel и другая доступная информация гласят, что

Процессор использует эту подсказку, чтобы избежать нарушения порядка памяти в большинстве ситуаций, что значительно повышает производительность процессора. По этой причине рекомендуется поместить инструкцию PAUSE во все циклы ожидания вращения. В документации также упоминается, что "wait(некоторая задержка)" является псевдо-реализацией инструкции.

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

Однако что мы подразумеваем под нарушением порядка памяти в случае спиновой блокировки? Означает ли "нарушение порядка памяти" неправильную спекулятивную загрузку / сохранение инструкций после спин-блокировки?

Вопрос о спин-блокировке задавался ранее по поводу переполнения стека, но вопрос о нарушении порядка памяти остался без ответа (по крайней мере, для моего понимания).

2 ответа

Решение

Только представьте, как процессор будет выполнять типичный цикл ожидания ожидания:

1 Spin_Lock:
2    CMP lockvar, 0   ; Check if lock is free
3    JE Get_Lock
4    JMP Spin_Lock
5 Get_Lock:

После нескольких итераций предиктор ветвления будет предсказывать, что условная ветвь (3) никогда не будет принята, и конвейер заполнится инструкциями CMP (2). Это продолжается до тех пор, пока, наконец, другой процессор не запишет ноль в lockvar. На данный момент у нас есть конвейер, полный спекулятивных (то есть еще не зафиксированных) инструкций CMP, некоторые из которых уже прочитали lockvar и сообщили (неверный) ненулевой результат в следующую условную ветвь (3) (также спекулятивную). Это когда происходит нарушение порядка памяти. Всякий раз, когда процессор "видит" внешнюю запись (запись от другого процессора), он ищет в своем конвейере инструкции, которые спекулятивно получили доступ к той же области памяти и еще не зафиксировали. Если какие-либо такие инструкции найдены, то спекулятивное состояние процессора является недействительным и стирается при очистке конвейера.

К сожалению, этот сценарий будет (очень вероятно) повторяться каждый раз, когда процессор ожидает спин-блокировки, и делает эти блокировки намного медленнее, чем они должны быть.

Введите инструкцию PAUSE:

1 Spin_Lock:
2    CMP lockvar, 0   ; Check if lock is free
3    JE Get_Lock
4    PAUSE            ; Wait for memory pipeline to become empty
5    JMP Spin_Lock
6 Get_Lock:

Инструкция PAUSE "депонирует" чтение памяти, поэтому конвейер не заполняется спекулятивными инструкциями CMP (2), как в первом примере. (То есть он может блокировать конвейер до тех пор, пока не будут зафиксированы все более старые инструкции памяти.) Поскольку инструкции CMP (2) выполняются последовательно, маловероятно (т. Е. Временное окно намного короче), что внешняя запись происходит после чтения инструкции CMP (2) Lockvar, но до того, как CMP будет совершено.

Конечно, "удаление конвейера" также будет тратить меньше энергии на спин-блокировку, а в случае гиперпоточности не будет тратить ресурсы, которые другой поток мог бы использовать лучше. С другой стороны, все еще существует неправильное предсказание ветвления, ожидающее перед каждым выходом из цикла. Документация Intel не предполагает, что PAUSE устраняет этот конвейер, но кто знает...

Как говорит @Mackie, конвейер заполнится cmps. Intel придется очистить эти cmps, когда пишет другое ядро, что является дорогой операцией. Если процессор не очищает его, значит у вас нарушение порядка памяти. Пример такого нарушения будет ниже:

(Это начинается с lock1 = lock2 = lock3 = var = 1)

Тема 1:

spin:
cmp lock1, 0
jne spin
cmp lock3, 0 # lock3 should be zero, Thread 2 already ran.
je end # Thus I take this path
mov var, 0 # And this is never run
end:

Тема 2:

mov lock3, 0
mov lock1, 0
mov ebx, var # I should know that var is 1 here.

Сначала рассмотрим тему 1:

если cmp lock1, 0; jne spin ветвь предсказывает, что lock1 не равен нулю, добавляет cmp lock3, 0 к трубопроводу.

В трубопроводе, cmp lock3, 0 читает lock3 и обнаруживает, что он равен 1.

Теперь предположим, что поток 1 занимает приятное время, а поток 2 начинает работать быстро:

lock3 = 0
lock1 = 0

Теперь вернемся к теме 1:

Скажем cmp lock1, 0 наконец, читает lock1, обнаруживает, что lock1 равен 0, и рад своей способности предсказывать переходы.

Эта команда фиксируется, и ничего не сбрасывается. Правильное предсказание ветвления означает, что ничего не сбрасывается, даже при чтении не по порядку, поскольку процессор пришел к выводу об отсутствии внутренней зависимости. lock3 не зависит от lock1 в глазах процессора, так что все в порядке.

Теперь cmp lock3, 0, который правильно прочитал, что lock3 был равен 1, фиксирует.

je end не берется, и mov var, 0 выполняет.

В теме 3 ebx равно 0. Это должно было быть невозможно. Это нарушение порядка памяти, которое Intel должна компенсировать.


Теперь решение, которое Intel принимает, чтобы избежать этого недопустимого поведения, заключается в сбрасывании. когда lock3 = 0 запускается в потоке 2, заставляет поток 1 сбрасывать инструкции, использующие lock3. Очистка в этом случае означает, что Поток 1 не будет добавлять инструкции в конвейер, пока все инструкции, использующие lock3, не будут зафиксированы. Перед темой 1 cmp lock3 может совершить, cmp lock1 должен совершить. Когда cmp lock1 пытается зафиксировать, он читает, что lock1 фактически равен 1, и что предсказание ветвления было неудачным. Это вызывает cmp быть выброшенным Теперь, когда поток 1 очищен, lock3расположение в кеше темы 1 установлено на 0, а затем поток 1 продолжает выполнение (Ожидание lock1). Поток 2 теперь получает уведомление о том, что все остальные ядра сбросили использование lock3 и обновил свои кэши, поэтому поток 2 затем продолжает выполнение (тем временем он будет выполнять независимые операторы, но следующей инструкцией была другая запись, поэтому он, вероятно, должен зависнуть, если у других ядер нет очереди для ожидания lock1 = 0 записывать).

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

Важным пониманием является то, что, хотя в моем примере сброс необходим, в примере Маки это не так. Тем не менее, ЦП не имеет возможности узнать (он вообще не анализирует код, кроме проверки последовательных зависимостей операторов и кэша прогнозирования ветвлений), поэтому ЦП сбрасывает инструкции, обращающиеся к ним. lockvar в примере Маки, как и в моем, чтобы гарантировать правильность.

Другие вопросы по тегам