Является ли загрузка и хранение единственными инструкциями, которые переупорядочиваются?
Я прочитал много статей по упорядочению памяти, и все они говорят только о том, что процессоры переупорядочиваются, загружаются и сохраняются.
ЦП (меня особенно интересует процессор x86) только переупорядочивает загрузки и сохранения, а не переупорядочивает остальные инструкции, которые у него есть?
2 ответа
Внеочередное выполнение сохраняет иллюзию запуска в программном порядке для одного потока / ядра. Это похоже на правило оптимизации "как будто" на C/C++: делайте все, что хотите, внутренне, пока видимые эффекты одинаковы.
Отдельные потоки могут обмениваться данными друг с другом только через память, поэтому глобальный порядок операций с памятью (загрузка / сохранение) является единственным внешне видимым побочным эффектом выполнения 1.
Даже для процессоров, работающих по порядку, операции с памятью могут стать невидимыми глобально. (Например, даже простой конвейер RISC с буфером хранилища будет переупорядочивать StoreLoad, например, x86). Процессор, который запускает загрузку / хранение по порядку, но позволяет им завершать работу не по порядку (чтобы скрыть задержку с отсутствием кэша), также может переупорядочивать нагрузки, если он специально не избегает этого (или, как современный x86, выполнять агрессивно вне очереди). заказать, но делать вид, что это не так, внимательно отслеживая порядок в памяти).
Простой пример: две цепочки зависимостей ALU могут перекрываться
(связанный: http://blog.stuffedcow.net/2013/05/measuring-rob-capacity/ для получения дополнительной информации о том, насколько велико окно для нахождения параллелизма на уровне инструкций, например, если вы увеличили его до times 200
вы бы увидели только ограниченное перекрытие. Также связано: этот ответ для начинающих и промежуточного уровня, который я написал о том, как ЦП OoO, такой как Haswell или Skylake, находит и использует ILP.)
global _start
_start:
mov ecx, 10000000
.loop:
times 25 imul eax,eax ; expands to imul eax,eax / imul eax,eax / ...
; lfence
times 25 imul edx,edx
; lfence
dec ecx
jnz .loop
xor edi,edi
mov eax,231
syscall ; sys_exit_group(0)
построен (с nasm
+ ld
) в статический исполняемый файл на Linux x86-64, он запускается (на Skylake) с ожидаемыми 750 М тактовыми циклами для каждой цепочки 25 * 10M
IMUL инструкции раз 3 задержки цикла.
Комментируя один из imul
Цепи не изменяют время, необходимое для работы: по-прежнему 750 миллионов циклов.
Это явное доказательство того, что выполнение не по порядку выполняет чередование двух цепочек зависимостей, в противном случае. (imul
пропускная способность - 1 за такт, задержка - 3 такта. http://agner.org/optimize/. Таким образом, третья цепочка зависимостей может быть добавлена без особого замедления).
Фактические цифры от taskset -c 3 ocperf.py stat --no-big-num -etask-clock,context-switches,cpu-migrations,page-faults,cycles:u,branches:u,instructions:u,uops_issued.any:u,uops_executed.thread:u,uops_retired.retire_slots:u -r3 ./imul
:
- с обеими цепями:
750566384 +- 0.1%
- только с цепочкой EAX:
750704275 +- 0.0%
- с одним
times 50 imul eax,eax
цепь:1501010762 +- 0.0%
(почти в два раза медленнее, чем ожидалось). - с
lfence
предотвращение перекрытия между каждым блоком 25imul
:1688869394 +- 0.0%
Хуже, чем в два раза медленнее.uops_issued_any
а такжеuops_retired_retire_slots
оба 63M, по сравнению с 51M, в то время какuops_executed_thread
все еще 51M (lfence
не использует порты выполнения, но, по-видимому, дваlfence
Инструкция стоит 6 мопов в слитном домене. Агнер Туман только измеряется 2.)
(lfence
сериализует выполнение инструкций, но не хранит память). Если вы не используете загрузку NT из памяти WC (что не произойдет случайно), это не будет, кроме как остановить более поздние инструкции от выполнения до тех пор, пока предыдущие инструкции не будут "выполнены локально". то есть до тех пор, пока они не уйдут из ядра из строя. Вероятно, поэтому он более чем удваивает общее время: он должен ждать последнего imul
в блоке, чтобы пройти больше этапов конвейера.)
lfence
на Intel это всегда так, но на AMD это только частично сериализация с включенным смягчением Spectre.
Сноска 1: Существуют также побочные каналы синхронизации, когда два логических потока совместно используют один физический поток (гиперпоточность или другой SMT). например, выполнение последовательности независимых imul
инструкции будут выполняться по 1 разу в такт на последнем процессоре Intel, если другой гиперпотоке не нужен порт 1 для чего-либо. Таким образом, вы можете измерить, какое давление порта 0 существует, синхронизируя цикл ALU на одном логическом ядре.
Другие микроархитектурные побочные каналы, такие как доступ к кешу, более надежны. Например, Spectre / Meltdown проще всего использовать с побочным каналом чтения кэша, а не ALU.
Но все эти побочные каналы являются привередливыми и ненадежными по сравнению с архитектурно-поддерживаемыми операциями чтения / записи в общую память, поэтому они важны только для безопасности. Они не используются намеренно в одной программе для связи между потоками.
MFENCE на Skylake - это барьер OoO exec, такой как LFENCE
mfence
на Skylake неожиданно блокирует неупорядоченное исполнение imul
, лайк lfence
даже если это не задокументировано, чтобы иметь такой эффект. (См. Обсуждение в чате).
xchg [rdi], ebx
(неявный lock
префикс) вообще не блокирует неупорядоченное выполнение инструкций ALU. Общее время составляет 750M циклов при замене lfence
с xchg
или lock
Инструкция в приведенном выше тесте.
Но с mfence
, стоимость уходит до 1500М циклов + время на 2 mfence
инструкции. Чтобы провести контролируемый эксперимент, я оставил счетчик команд таким же, но переместил mfence
инструкции рядом друг с другом, поэтому imul
цепи могли переупорядочиваться друг с другом, и время сократилось до 750M + время для 2 mfence
инструкции.
Такое поведение Skylake, скорее всего, является результатом обновления микрокода для исправления ошибки SKL079, MOVNTDQA из памяти WC может пройти ранее инструкции MFENCE. Наличие опечатки показывает, что раньше было возможно выполнить более поздние инструкции до mfence
завершено, так что, вероятно, они сделали грубое исправление добавления lfence
упс в микрокод для mfence
,
Это еще один фактор в пользу использования xchg
для последовательных магазинов, или даже lock add
к некоторой стековой памяти как отдельный барьер. Linux уже делает обе эти вещи, но компиляторы все еще используют mfence
для барьеров. См. Почему хранилище std::atomic с последовательной последовательностью использует XCHG?
(См. Также обсуждение выбора барьеров для Linux в этой ветке групп Google со ссылками на 3 отдельные рекомендации по использованию. lock addl $0, -4(%esp/rsp)
вместо mfence
как отдельный барьер.
Процессоры с неупорядоченным порядком обычно могут переупорядочивать все инструкции там, где это возможно, возможно, выгодно для производительности. Из-за переименования регистров это прозрачно для машинного кода, за исключением случая загрузки и хранения. Именно поэтому люди обычно говорят только о переупорядочении нагрузки и хранения, поскольку это единственный наблюдаемый вид переупорядочения.
† Как правило, исключения FPU также можно наблюдать за переупорядочением. По этой причине большинство неработающих процессоров имеют неточные исключения, но не x86. На платформе x86 процессор обеспечивает выдачу исключений, как если бы операции с плавающей запятой не переупорядочивались.