Pull to refresh

Comments 33

Наконец, после долгих «обзоров новых девайсов», «дайджестов интересных новостей» в Хабре появилась реально крутая статья.
Согласен на все 100%. Каким образом изменить тип поста после опубликования?
Если бы еще кто-нибудь составил подборку, какие барьеры в каких случаях автоматически расставляются трансляторами…
Так, в C++ ключевое слово volatile по стандарту не дает даже барьера компилятора (оно выключает оптимизацию для конкретной переменной, но не влияет на остальные переменные). Но в Visual C++ на него повешена дополнительная функция: чтение volatile переменной дает (в терминах этой статьи) LOCK-барьер компилятора, запись — UNLOCK-барьер компилятора.
А вот в Java тонкостей еще больше…
Трансляторами автоматически барьеры никогда не расставлются по одной простой причине -транслятор просто-напросто не знает где одну операцию чтения/записи надо «отбарьеривать» от другой, а где не надо.
Поэтому выставление «барьера» это:
1) всегда исключительно забота программиста
2) всегда происходит в явном виде
3) машинно зависимо.

ВАЖНОЕ ЗАМЕЧАНИЕ: Кстати, насчет использования барьеров из языков высокого уровня, расстановка барьера делиться на две части «барьер для процессора» и «доходчивого обьяснения что здесь стоит барьер для компилятора».

Поясню на примере. В x86/x86_64 есть такая ассемблерная инструкция «mfence», реализующая барьер чтения и записи. Однако, если ее вставить «в лоб» через asm(«mfence»); результат может оказаться совсем далеким от ожидания:
while(bla_bla){
 do_something_1();
 asm("mfence");    // так ставить барьер неправильно!
 do_something_2();
 }


Компилятор видя, во-первых может вставить asm(«mfence») куда ему заблагорассудится, да и вообще нахрен вынести за цикл (как инструкцию не зависящую от условия цикла). И барьер пойдет насмарку.

Поэтому надо во-первых обьяснить с помощью asm volatile() что ассемблерная вставка «имеет побочные эффекты», ее оптимизировать не надо, а во-вторых с помощью asm volatile("":::«memory»); обьяснить, что сама ассемблерная вставка является «мемори-барьером» для компилятора, и что все что стояло ДО этой вставки по коду должно размещатся до нее в результирующем машинном коде, а все что ПОСЛЕ и в машинном коде будет после:

while(bla_bla){
 do_something_1();
 asm volatime("mfence":::"memory");    // А вот так правильно!
 do_something_2();
 }


В Java всё довольно просто, есть Java Memory Model, описанная в Java Language Specification. Есть строго прописанное соотношение «happened before», есть понятные и простые примитивы синхронизации. Ничего особо страшного и сложного. Опять же хорошо прописаны условия видимости изменений.
Из плюсов: все грабли хорошо расписаны в классической литературе и почти не меняются от версии к версии.
Для интересующихся рекомендую JCIP (http://www.javaconcurrencyinpractice.com/) и JLS (http://docs.oracle.com/javase/specs/).
К сожалению, в дереве Линуса текущей (3.7-rc2) версии нет ни одной архитектуры с нетривиальной реализацией этого примитива. Для подавляющего большинства архитектур это do { } while (0), для alpha и blackfin — это rmb(). Так что либо вся магия зашита в процессор, либо нет никакой магии.
Верно ли, что data dependency барьер это оптимизированный вариант барьера на чтение? Действует не на все операции чтения, а только на те из них, которые вместе с какой-нибудь другой инструкцией после барьера формирует зависимость по данным?

Резонный вопрос — как «глубоко» железо способно отследить зависимость (грубо говоря пусть N — «расстояние» в инструкциях между начальной и конечной инструкциями, связанных зависимостью; как велико N)?

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

Не троллинга ради, но исключительно лучшего понимания для :)
My bad! Прочитал этот док и все понял.
Т.е. это не оптимизированный барьер нифига; это тот же самый барьер на чтение с более четкой семантикой использования, что позволяет его соптимизировать «в ноль» во время компиляции на тех платформах, где все работает правильно и без барьера.
Я там прочел другое…
The rmb() forces all outstanding invalidates to be processed before any subsequent reads are allowed to proceed.
Т.е. эта инструкция заставляет кэш обработать все сообщения, о том что какие то ячейки были инвалидированы.

Если Data dependency barrier устроен именно так, то получается что он решает поблему только в частном случае, когда она вызвана кэшем процессора.
Другая прична роблемы может быть в том, что некоторые процессоры пытаются угадывают значения. Те. это вид load speculation. Такая штука есть у IA64, он может попытаться угадать значение указателя, и прочесть по нему значение, до того как другое ядро собсвенно записало адрес в указатель. Здесь выходит защитится можно только обычными барьерами.
Кеш в этой картине не участвует, в последней главе об этом явно написано.
Из статьи:
Замечание: такая противоестественная ситуация легче всего воспроизводима на машинах с разделённым кешем

А почему это работает хорошо объяснено в сылке от mejedi,
Ок, похоже кеш, а вернее система поддержки его когерентности, действительно участвует в случае с Альфой, и только по причине его разделённости и асинхронности частей.

Для IA64 read_barrier_depends — пустой, что означает, что зависимости по данным отрабатываются процессором (или компилятором в их идеологии?) самостоятельно.
Вот тут интересный момент. На других процах проблемы такой не возникает или возникает, но нужно пользоваться обычным барьером?
Верно ли, что data dependency барьер это оптимизированный вариант барьера на чтение? Действует не на все операции чтения, а только на те из них, которые вместе с какой-нибудь другой инструкцией после барьера формирует зависимость по данным?

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

Резонный вопрос — как «глубоко» железо способно отследить зависимость (грубо говоря пусть N — «расстояние» в инструкциях между начальной и конечной инструкциями, связанных зависимостью; как велико N)?

Опять же, насколько я понимаю, у процессора есть буфера для операций чтения и записи. Я, к сожалению, не знаю деталей ни одной архитектуры имеющей буфер чтения, но думаю, что работает аналогично буферу записи. Так, например у ARM926ejs два буфера записи, один на четыре адреса и 16 слов данных, второй — на один адрес и 8 слов данных. Операции покинувшие буфера и отправленные подсистеме памяти уже упорядочены, соответственно их отслеживать не надо. Остаётся следить только за операциями в буферах, и если это делать, то получается что N ничем не ограничено.
Как говорил один, наверняка, умный человек: «Операционная система делает три вещи — управляет оборудованием, распределяет память и мешает работе программиста. Причем с первыми двумя обычно справляется плохо»
Оффтопик. В обоих смыслах, если вы понимаете, о чём я.
Не написали, что на многих платформах wb() и rb() превращаются в совершенно пустое место, если барьеры не нужны (например, архитектура много обещает) — т.е. добавление барьеров не снижает производительности кода на системах, где барьеры излишни.
Просится мысль вставить в spin_lock и spin_unlock барьеры. В unlock — записи перед собственно отпиранием (который запись, и, значит, все операции записи внутри запертого спинлока будут видны до того, как другие процессоры увидят отпирание спинлока), в lock — чтения после запирания, чтобы гарантировать, что чтения памяти внутри спинлока не заспекулированы до собственно запирания лока и видят то, что другой процессор записал внутри спинлока.
Это же просто acquire / release семантика, нет?
Просится мысль вставить в spin_lock и spin_unlock барьеры

Если речь про https://github.com/dzavalishin/phantomuserland/blob/master/oldtree/kernel/phantom/ia32/spinlock.c#L14 и дальше, то xchg в память — это барьер (согласно тому 3A Intel 64 and IA-32 Architectures Software Developer's Manual, глава Software Controlled Bus Locking). Единственное, чего на мой взгляд не хватает — это "memory" после третьего двоеточия, чтобы компилятор не наоптимизировал.
Причем это, видимо, полный барьер (записи и чтения, если в той терминолонии, что используется в данной статье).
на интеле — ок, но на других архитектурах?
На ARM говорят вот так:
«In order to provide for synchronization functions, IA-32 provides for bus-locking (via a LOCK prefix to certain memory access instructions) which guarantees atomicity for affected accesses. The most common use of this is to lock the memory transactions associated with the XCHG instruction to form an atomic exchange of a register with a value in memory. This can be used to implement higher-level semaphore and lock constructs as required by operating systems.
Earlier ARM architectures supported similar behavior via the SWP instruction. This instruction is now deprecated and has been replaced by a non-blocking mechanism using “exclusive” memory access instructions, LDREX/STREX. These work with internal and, optionally, external exclusive access monitor logic within the memory system to provide for atomic software constructs.»

Взято отсюда:
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dai0274b/index.html
Насколько я понимаю, пара ldrex/strex — аналог ll/sc в mips?
Ага. Поэтому предлагается цикл + барьер по данным:
get_lock
LDREX r1, [r0]
CMP r1, #0
BNE get_lock

MOV r1, #1
STREX r2, r1, [r0]
CMP r2, #0x0
BNE get_lock
DMB
Sign up to leave a comment.

Articles