Порядок памяти потребляет использование в C11

Я читал о переносе отношений зависимости и упорядоченности зависимостей, прежде чем использовать их в своем определении 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 ответа

Решение

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

На ARM/AArch64/PowerPC/MIPS/etc слабо упорядоченные ISA, consume а также relaxed это единственные заказы, которые не требуют каких-либо дополнительных барьеров, просто обычные дешевые инструкции по загрузке. т.е. все инструкции по загрузке asm (как минимум) consume загружает, кроме как на альфе. 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 выполняются при их выполнении, а не когда фиксация retire + в конечном итоге фиксируется. Таким образом, заказ 2 груза по сравнению с на самом деле друг друга просто означает выполнение этих двух загрузок по порядку. При зависимости данных друг от друга причинность требует, чтобы на процессорах без прогнозирования значения и на большинстве архитектур правила ISA действительно требовали этого. Таким образом, вам не нужно использовать барьер между загрузкой + использованием указателя в asm, например, для обхода связанного списка.)

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


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

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

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

Так что да, вы правы, что 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: Те немногие модели Alpha, которые теоретически могли "нарушать причинность", не делали предсказания стоимости, существовал другой механизм с их кэш-памятью. Я думаю, что видел более подробное объяснение того, как это было возможно, но комментарии Линуса о том, как редко это было на самом деле, интересны.

Линус Торвальдс (ведущий разработчик Linux) в теме форума RealWorldTech

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

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

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

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

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

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

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

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

Смотрите также https://lkml.org/lkml/2012/2/1/521 относительно Linux smp_read_barrier_depends() что нужно в линуксе только из-за альфы. (Но ответ Ханса Бема указывает на то, что " компиляторы могут, а иногда и делают, удалить зависимости ", поэтому 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, Но только эта нагрузка, а не все другие поздние загрузки; это не барьер приобретения или нагрузка приобретения.

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

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