consume
дешевле, чем acquire
. Все процессоры (кроме известной слабой модели памяти DEC Alpha AXP 1) делают это бесплатно, в отличие от acquire
. (за исключением x86 и SPARC-TSO, где оборудование имеет упорядочение памяти acq / rel без дополнительных барьеров и специальных инструкций.)
На ARM / AArch64 / PowerPC / MIPS / и т. Д. ISA со слабым упорядочением consume
и relaxed
- единственные упорядочения, которые не требуют каких-либо дополнительных барьеров, только обычные дешевые инструкции по загрузке. т.е. все инструкции загрузки asm являются (как минимум) consume
загрузками, кроме Alpha. acquire
требует упорядочивания LoadStore и LoadLoad, что является более дешевой барьерной инструкцией, чем полная барьерная инструкция для seq_cst
, но все же дороже, чем ничего.
mo_consume
похож на acquire
только для нагрузок с зависимостью данных от потребляемой нагрузки. например float *array = atomic_ld(&shared, mo_consume);
, то доступ к любому array[i]
безопасен, если производитель сохранил буфер и затем использовал хранилище mo_release
для записи указателя на общую переменную. Но независимые загрузки / сохранения не должны ждать завершения consume
загрузки и могут произойти раньше, даже если они появятся позже в программном порядке. Поэтому consume
заказывает только самый минимум, не влияя на другие грузы или магазины.
(В принципе, реализовать поддержку семантики consume
в оборудовании для большинства конструкций ЦП можно бесплатно, потому что OoO exec не может нарушить истинные зависимости, а загрузка имеет зависимость данных от указателя, поэтому загрузка указателя и затем разыменование его по своей сути упорядочивает эти 2 загрузки только по природе причинно-следственной связи. Если только ЦП не выполняют прогнозирование значений или что-то безумное. Предсказание значений похоже на прогнозирование ветвления, но угадайте, какое значение будет загружено, а не в какую сторону будет идти.
Alpha пришлось сделать несколько сумасшедших вещей, чтобы сделать процессоры, которые действительно могли загружать данные до того, как значение указателя было действительно загружено, когда хранение было выполнено по порядку с достаточными барьерами.
В отличие от магазинов, где в буфере хранилища может происходить переупорядочение между выполнением хранилища и фиксацией в кеш-памяти L1d, загрузки становятся "видимыми" "путем извлечения данных из кэша L1d при их выполнении, а не при окончательном подтверждении вывода на пенсию +. Таким образом, заказывая 2 загрузки по весу. друг друга на самом деле просто означает выполнение этих двух загрузок по порядку. При зависимости данных друг от друга причинно-следственная связь требует этого на ЦП без прогнозирования значений, и на большинстве архитектур правила ISA этого требуют. Таким образом, вам не нужно использовать барьер между загрузкой + с помощью указателя в asm, например для просмотра связанного списка.)
См. также Изменение порядка зависимых нагрузок в ЦП
Но нынешние компиляторы просто сдаются и улучшают consume
до acquire
... вместо того, чтобы пытаться сопоставить зависимости C с зависимостями asm data (без случайного нарушения, имея только контрольную зависимость, которую можно было бы обойти с помощью предсказания ветвления + спекулятивного выполнения). Очевидно, компиляторам сложно отследить это и сделать его безопасным.
Сопоставить C с asm нетривиально, потому что, если зависимость находится только в форме условной ветки, правила asm не применяются. Так что трудно определить правила C для mo_consume
распространения зависимостей только способами, которые совпадают с тем, что «несет зависимость» в терминах правил asm ISA.
Так что да, вы правы, что consume
можно безопасно заменить на acquire
, но вы полностью упускаете суть.
У ISA со слабыми правилами упорядочивания памяти действительно есть правила о том, какие инструкции имеют зависимость. Таким образом, даже такая инструкция, как ARM eor r0,r0
, которая безоговорочно обнуляет r0
, архитектурно требуется, чтобы по-прежнему нести зависимость данных от старого значения, в отличие от x86, где идиома xor eax,eax
специально распознается как разрушающая зависимость 2.
См. Также http://preshing.com/20140709/the-purpose-of-memory_order_consume-in-cpp11/
Я также упомянул mo_consume
в ответе на Атомарные операции, std :: atomic ‹› и заказ писем.
Сноска 1. Несколько альфа-моделей, которые теоретически могли «нарушить причинно-следственную связь», не выполняли прогнозирования значений, был другой механизм с их банковским кешем. Думаю, я видел более подробное объяснение того, как это было возможно, но комментарии Линуса о том, насколько редко это было на самом деле, интересны.
Линус Торвальдс (ведущий разработчик Linux) в ветке форума RealWorldTech
Интересно, вы сами видели отсутствие причинности на Alpha или просто в руководстве?
Я никогда не видел этого сам и не думаю, что какая-либо из моделей, к которым у меня когда-либо был доступ, действительно это делала. Что на самом деле делало (медленную) инструкцию ПКМ еще более раздражающей, потому что это был просто недостаток.
Даже на процессорах, которые действительно могли изменять порядок нагрузок, на практике это было практически невозможно. Что на самом деле довольно неприятно. Это привело к появлению «ой, я забыл барьер, но все работало нормально в течение десяти лет, с тремя странными сообщениями об ошибках с мест». Разобраться в том, что происходит, чертовски больно.
На каких моделях это действительно было? И как именно они сюда попали?
Я думаю, что это был 21264, и у меня эта тусклая память связана с разделенным кешем: даже если исходный ЦП сделал две записи по порядку (с wmb между ними), ЦП чтения может в конечном итоге получить первую запись отложено (потому что раздел кеша, в который он вошел, был занят другими обновлениями), и сначала будет читать вторую запись. Если эта вторая запись была адресом первой, она могла бы следовать за этим указателем и без барьера чтения для синхронизации разделов кеша могла бы увидеть старое устаревшее значение.
Но обратите внимание на «тусклую память». Возможно, я перепутал это с чем-то другим. Я практически не использовал альфу почти два десятилетия назад. Вы можете получить очень похожие эффекты от прогнозирования стоимости, но я не думаю, что какая-либо альфа-микроархитектура когда-либо делала это.
В любом случае, определенно были версии альфы, которые могли это сделать, и это не было чисто теоретическим.
(ПКМ = чтение asm-инструкции барьера памяти и / или имя функции ядра Linux rmb()
, которая обертывает все встроенные asm-файлы, необходимые для этого. Например, на x86, просто барьер для переупорядочения во время компиляции, asm("":::"memory")
. Я думаю, что современный Linux удается избежать барьера приобретения, когда требуется только зависимость данных, в отличие от C11 / C ++ 11, но я забываю. Linux переносится только на несколько компиляторов, и эти компиляторы действительно заботятся о поддержке того, от чего зависит Linux, поэтому они легче, чем стандарт ISO C11, приготовить что-то, что работает на практике с настоящими ISA.)
См. Также https://lkml.org/lkml/2012/2/1/521 re: smp_read_barrier_depends()
Linux, который необходим в Linux только из-за Alpha. (Но в ответе от Ганса Боэма указывается, что " компиляторы могут, а иногда и удаляют зависимости ", поэтому поддержка C11 memory_order_consume
должна быть настолько сложной, чтобы избежать риска поломки. Таким образом, smp_read_barrier_depends
потенциально хрупкий.)
Сноска 2: x86 упорядочивает все загрузки независимо от того, несут ли они зависимость данных от указателя или нет, поэтому ему не нужно сохранять «ложные» зависимости, а с набором инструкций переменной длины он фактически сохраняет размер кода xor eax,eax
(2 байта) вместо mov eax,0
(5 байтов).
Итак, xor reg,reg
стал стандартной идиомой с начала 8086-х годов, и теперь он распознается и фактически обрабатывается как mov
, без зависимости от старого значения или RAX. (И на самом деле более эффективно, чем mov reg,0
, помимо размера кода: Как лучше всего обнулить регистр в сборке x86: xor, mov или and? )
Но это невозможно для ARM или большинства других слабо упорядоченных ISA, как я уже сказал, им буквально не разрешено это делать.
ldr r3, [something] ; load r3 = mem
eor r0, r3,r3 ; r0 = r3^r3 = 0
ldr r4, [r1, r0] ; load r4 = mem[r1+r0]. Ordered after the other load
требуется для внедрения зависимости от r0
и упорядочивания загрузки r4
после загрузки r3
, даже если адрес загрузки r1+r0
всегда равен r1
, потому что r3^r3 = 0
. Но только эту загрузку, а не все другие последующие загрузки; это не препятствие для приобретения или нагрузка для приобретения.
person
Peter Cordes
schedule
18.04.2019