Порядок памяти потребляет использование в C11 - PullRequest
2 голосов
/ 18 апреля 2019

Я читал, что содержит отношение и упорядочено по зависимости до , которое использует его в своем определении 5.1.2.4(p16):

Оценка A упорядочена по зависимостям перед оценкой B, если:

- A выполняет операцию освобождения атомарного объекта M, а в другом B выполняет операцию потребления для M и читает записанное значение. любым побочным эффектом в последовательности выпуска, возглавляемой A или

- для некоторой оценки X, A упорядочивается по зависимостям до того, как X и X переносит зависимость в B.

Поэтому я попытался создать пример, где он мог бы быть полезным. Вот оно:

static _Atomic int i;

void *produce(void *ptr){
    int int_value = *((int *) ptr);
    atomic_store_explicit(&i, int_value, memory_order_release);
    return NULL;
}

void *consume(void *ignored){
    int int_value = atomic_load_explicit(&i, memory_order_consume);
    int new_int_value = int_value + 42;
    printf("Consumed = %d\n", new_int_value);
}

int main(int args, const char *argv[]){
    int int_value = 123123;
    pthread_t t2;
    pthread_create(&t2, NULL, &produce, &int_value);

    pthread_t t1;
    pthread_create(&t1, NULL, &consume, NULL);

    sleep(1000);
}

В функции void *consume(void*) int_value несет зависимость для new_int_value, поэтому, если atomic_load_explicit(&i, memory_order_consume); читает значение, записанное некоторыми atomic_store_explicit(&i, int_value, memory_order_release);, тогда new_int_value вычисление зависимость-упорядоченный-до atomic_store_explicit(&i, int_value, memory_order_release);.

Но что полезного может дать нам заказанная зависимость?

В настоящее время я думаю, что memory_order_consume вполне можно заменить на memory_order_acquire, не вызывая гонки данных ...

Ответы [ 2 ]

3 голосов
/ 18 апреля 2019

consume дешевле , чем acquire. Все процессоры (кроме известной слабой модели памяти DEC Alpha AXP 1 ) делают это бесплатно, в отличие от acquire. (за исключением x86 и SPARC-TSO, где аппаратное обеспечение имеет порядок памяти acq / rel) без дополнительных барьеров или специальных инструкций.)

На слабо упорядоченных ISA ARM / AArch64 / PowerPC / MIPS / и т. Д. 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 нагрузки по природе причинности, если только процессоры не делают прогнозирование значений или что-то сумасшедшее. Предсказание значений похоже на предсказание ветвления, но догадайтесь, какое значение будет загружено, а не какое направление будет идти.

Альфа должна была сделать что-то безумное, чтобы заставить процессоры реально загружать данные до того, как значение указателя было действительно загружено, когда хранилища были выполнены в порядке с достаточными барьерами.

В отличие от хранилищ, где буфер хранилища может вводить переупорядочение между выполнением хранилища и фиксацией в кэш-памяти L1d, загрузки становятся «видимыми», принимая данные из кеш-памяти L1d, когда они выполняют , не тогда, когда на пенсию + в конце концов совершить. Таким образом, заказ 2 груза по сравнению с на самом деле друг друга просто означает выполнение этих двух загрузок по порядку. При зависимости данных друг от друга причинность требует, чтобы на процессорах без прогнозирования значения и на большинстве архитектур правила ISA действительно требовали этого. Так что вам не нужно использовать барьер между загрузкой + использованием указателя в asm, например для обхода связанного списка. )

См. Также Переупорядочение зависимых нагрузок в CPU


Но современные компиляторы просто сдаются и усиливаются consume до acquire

... вместо того, чтобы пытаться отобразить зависимости C в asm data зависимости (без случайного нарушения, имеющего только управляющую зависимость, которую может обойти предсказание ветвления + спекулятивное выполнение). Очевидно, для компиляторов трудно отследить это и сделать его безопасным.

Нетривиально отобразить C в asm, потому что если зависимость только в форме условной ветви, правила asm не применяются. Поэтому трудно определить правила C для mo_consume распространяющихся зависимостей только таким образом, чтобы это соответствовало тому, что "несет зависимость" в терминах asm ISA-правил.

Так что да, вы правы, что consume можно безопасно заменить на acquire, но вы полностью упускаете суть.


ISA со слабыми правилами упорядочения памяти do имеют правила, относительно которых инструкции несут зависимость. Так что даже такая инструкция, как 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

Интересно, вы видели непричинность на Альфе самостоятельно или просто в руководстве?

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

Даже на процессорах, которые фактически могли переупорядочивать нагрузки, было практически невозможно выполнить на практике.Что на самом деле довольно неприятно.Это привело к «ой, я забыл барьер, но все работало хорошо в течение десятилетия, с тремя странными сообщениями о том, что« этого не может быть »с полей».Выяснить, что происходит, просто чертовски больно.

Какие модели действительно имели это?И как именно они сюда попали?

Я думаю, что это был 21264, и у меня эта тусклая память о нем из-за многораздельного кэша: даже если исходный ЦП делал две записи по порядку (сесли между ними находится wmb), процессор чтения может в конечном итоге задержать первую запись (поскольку раздел кэша, в который он вошел, был занят другими обновлениями), и сначала прочитает вторую запись.Если эта вторая запись была адресом первой, она могла затем следовать за этим указателем и без барьера чтения для синхронизации разделов кэша она могла видеть старое устаревшее значение.

Но обратите внимание на "тусклую память"».Возможно, я перепутал это с чем-то другим.На самом деле я не использовал альфу ближе к двум десятилетиям.Вы можете получить очень похожие эффекты от прогнозирования значений, но я не думаю, что какая-либо альфа-микроархитектура когда-либо делала это.

В любом случае, определенно были версии альфы, которые могли бы сделать это, и это было не простотеоретическое.

(RMB = чтение ассемблерной инструкции Memory Barrier и / или имя функции ядра Linux rmb(), которая переносит любой встроенный asm, необходимый для этого. Например, в x86, простобарьер для переупорядочения во время компиляции, asm("":::"memory"). Я думаю, что современному Linux удается избежать барьера получения, когда требуется только зависимость данных, в отличие от C11 / C ++ 11, но я забываю. Linux переносим только для нескольких компиляторов,и эти компиляторы заботятся о поддержке того, от чего зависит Linux, поэтому им легче, чем стандарту ISO C11, готовить что-то, что работает на реальных ISA.)

См. также https://lkml.org/lkml/2012/2/1/521 re: Linux smp_read_barrier_depends(), который необходим в Linux только из-за Alpha.(Но ответ от Hans Boehm указывает, что « компиляторы могут, а иногда и делают, удалить зависимости », поэтому поддержка 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 или и? )

Но это невозможно для 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. Но только , который загружается, а не все другие более поздние загрузки;это не барьер захвата или нагрузка приобретения.

2 голосов
/ 18 апреля 2019

memory_order_consume в настоящее время не указан, и есть текущая работа , чтобы исправить это.В настоящее время AFAIK все реализации неявно продвигают его до memory_order_acquire.

...