Безопасен ли mov + mfence для NUMA?

Я вижу, что g++ генерирует простой mov за x.load() а также mov+mfence за x.store(y), Рассмотрим этот классический пример:

#include<atomic>
#include<thread>
std::atomic<bool> x,y;
bool r1;
bool r2;
void go1(){
    x.store(true);
}
void go2(){
    y.store(true);
}
bool go3(){
    bool a=x.load();
    bool b=y.load();
    r1 = a && !b;
}
bool go4(){
    bool b=y.load();
    bool a=x.load();
    r2= b && !a;
}





int main() {
    std::thread t1(go1);
    std::thread t2(go2);
    std::thread t3(go3);
    std::thread t4(go4);
    t1.join();
    t2.join();
    t3.join();
    t4.join();
    return r1*2 + r2;
}

в котором согласно https://godbolt.org/z/APS4ZY go1 и go2 переведены на

go1():
        mov     BYTE PTR x[rip], 1
        mfence
        ret
go2():
        mov     BYTE PTR y[rip], 1
        mfence
        ret

В этом примере я спрашиваю, могут ли потоки t3 и t4 расходиться во мнениях относительно порядка, в котором записи, выполняемые посредством t1 и t2, "просачиваются" в их соответствующие представления памяти. В частности, рассмотрим архитектуру NUMA, в которой t3 оказывается "ближе" к t1, а t4 "ближе" к t2. Может ли случиться так, что буфер хранения t1 или t2 "преждевременно сбрасывается" даже до достижения mfence и тогда у t3 или t4 есть шанс наблюдать запись раньше, чем планировалось?

1 ответ

Решение

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

NUMA даже не относится к этому; многоядерная система x86 с одним сокетом уже может переупорядочивать память настолько, насколько позволяет модель памяти x86. (Может быть, реже или с меньшими временными окнами.)


TLDR.1: вы, кажется, неправильно понимаете, что mfence делает. Это локальный барьер для ядра, которое его запускало (включая StoreLoad, единственный переупорядочивающий x86 допускает без барьеров для загрузок / хранилищ не-NT). Это совершенно неважно, даже если x86 был слабо упорядочен: мы рассматриваем 1 магазин с разными ядрами, поэтому упорядочение операций с одним ядром не имеет значения. друг друга не имеет значения.

(mfence просто заставляет это ядро ​​ждать выполнения каких-либо загрузок, пока его хранилище не станет глобально видимым. Ничего особенного не происходит, когда магазин совершает mfence ждет этого. Гарантирует ли барьер памяти, что согласованность кэша была завершена?.)


TL: DR.2: Будут ли две атомарные записи в разные места в разных потоках всегда рассматриваться в одном и том же порядке другими потоками? C++ позволяет различным потокам не соглашаться с порядком хранилищ в расслабленных или освобожденных хранилищах (и, конечно, приобретать нагрузки, чтобы исключить переупорядочение LoadLoad), но не с seq_cst,

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

Все основные x86-системы, которые вы можете купить , на самом деле являются x86 с последовательным кэшем и подчиняются модели памяти x86.


Модель памяти TSO в x86 требует, чтобы все ядра могли согласовать общий порядок хранения

Таким образом, релевантное правило буквально соответствует названию модели памяти.

Свойство TSO следует непосредственно из каждого ядра, сохраняя свои собственные хранилища частными, пока они не перейдут в L1d, и из-за наличия связных кэшей.

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

Единственный способ получить данные между ядрами - использовать кеш L1d, чтобы сделать его глобально видимым; не делиться с некоторыми ядрами раньше других. (Это важно для TSO, независимо от NUMA).

Остальные правила упорядочения памяти в основном касаются внутреннего переупорядочения в ядре: оно гарантирует, что его хранилища фиксируются из буфера хранилища в L1d в программном порядке и после того, как любые более ранние загрузки уже прочитали их значение. (И другие внутренние правила, обеспечивающие упорядочение LoadLoad, включая конвейер ошибочной спекуляции порядка памяти, если спекуляция порядка загрузки считывает значение, для которого мы теряем строку кэша, прежде чем нам "позволили" прочитать значение.)

Данные могут передаваться из буфера хранилища в частный L1d только тогда, когда это ядро ​​имеет соответствующую строку в состоянии Modified, что означает, что любое другое ядро ​​имеет ее в состоянии Invalid. Это (наряду с остальными правилами MESI) обеспечивает согласованность: никогда не может быть конфликтующих копий строки кэша в разных кэшах. Таким образом, как только хранилище зафиксировало кеширование, никакое другое ядро ​​не сможет загрузить устаревшее значение. ( Что будет использоваться для обмена данными между потоками, выполняющимися на одном ядре с HT?)

Распространенным заблуждением является то, что хранилища должны просачиваться через систему, прежде чем другие процессоры прекратят загрузку устаревших значений. Это на 100% неправильно в нормальных системах, которые используют MESI для поддержки связных кэшей. Кажется, вы тоже страдаете от этого заблуждения, когда говорите о том, что t3 "ближе" к t1. Это может быть верно для устройств DMA, если у вас есть некогерентный DMA, именно потому, что эти чтения DMA не будут согласованы с представлением памяти, совместно используемой процессорами, участвующими в протоколе MESI. (Но современный x86 также имеет DMA с когерентным кэшем.)

На самом деле, нарушение TSO требует довольно забавного поведения, когда магазин становится видимым для некоторых других ядер, прежде чем становится видимым для всех. PowerPC делает это в реальной жизни для логических потоков на одном физическом ядре, отслеживающих удаленные хранилища друг друга, которые еще не зафиксировали кэш-память первого уровня. См. Мой ответ на вопрос: будут ли две атомные записи в разные места в разных потоках всегда рассматриваться в одном и том же порядке другими потоками? Это редко, даже на слабо упорядоченных ISA, которые позволяют это на бумаге.


Системы с процессорами x86, но с некогерентной общей памятью (или будут) очень разные звери

(Я не уверен, существуют ли такие звери.)

Это больше похоже на тесно связанные кластеры суперкомпьютеров, чем на отдельные машины. Если это то, о чем вы думаете, это не просто NUMA, это принципиально иное, и вы не можете запускать обычное многопоточное программное обеспечение в разных областях когерентности.

Как говорит Википедия, по существу все системы NUMA являются NUMA, связанными с кэшем, то есть ccNUMA.

Хотя системы NUMA, не связанные с кэшем, проще проектировать и создавать, они становятся слишком сложными для программирования в стандартной модели программирования архитектуры фон Неймана

Любая некогерентная система с общей памятью, использующая процессоры x86, не будет работать с одним экземпляром ядра в разных доменах когерентности. Вероятно, он будет иметь пользовательскую библиотеку MPI и / или другие пользовательские библиотеки для использования совместно используемой памяти с явными сбросами / когерентностью для обмена данными между доменами когерентности (системами).

Любые потоки, которые вы можете запустить из одного процесса, обязательно будут использовать согласованное с кэшем представление памяти и подчиняться модели памяти x86, в противном случае ваша система сломана / имеет аппаратные ошибки. (Я не знаю о каких-либо таких ошибках HW, существующих и нуждающихся в работе на реальном оборудовании.)

Система с одной или несколькими картами Xeon Phi PCIe рассматривает каждый ускоритель Xeon Phi как отдельную "систему", поскольку они не связаны с основной памятью или друг с другом, а только внутренне согласованы. См. Нижний раздел ответа @Hadi о том, как кэши данных маршрутизируют объект в этом примере?, Вы можете перенести некоторую работу на ускоритель Xeon Phi, подобно тому, как вы бы разгрузили работу на GPU, но это делается с помощью чего-то вроде передачи сообщений. У вас не будет некоторых потоков, работающих на основном процессоре Skylake (например), и других обычных потоков того же процесса, работающих на ядрах KNL на Xeon Phi. Если бы на плате Xeon Phi работала ОС, это был бы отдельный экземпляр Linux или чего-либо другого из того, что работает на хост-системе.


Системы x86 NUMA реализуют MESI, отслеживая другие сокеты перед загрузкой из локальной памяти DRAM, чтобы поддерживать согласованность кэша.

И, конечно, запросы RFO (чтение для владения) передаются на другие сокеты.

Новые поколения Xeon вводят все больше и больше настроек Snoop для компромисса между различными аспектами производительности. (Например, более активное отслеживание требует большей пропускной способности канала между сокетами, но может уменьшить межъядерную задержку между сокетами.)

Микросхемы, которые могут работать в четырехъядерных сокетах и ​​более крупных системах (E7 v1..4), имеют фильтры snoop; E5 v1.4 с двумя сокетами просто транслирует отслеживание на другой сокет, используя приличную долю пропускной способности QPI по сравнению с тем, что я прочитал. (Это относится к Xeons до Skylake-X, Broadwell и более ранним. SKX использует ячеистую сеть на чипе и может всегда иметь какую-то snoop-фильтрацию между сокетами. Я не уверен, что это делает. BDW и ранее использовали инклюзивный Кэш-память L3 в качестве snoop-фильтра для локальных ядер, но SKX имеет не включающий L3 и, следовательно, нуждается в чем-то еще для snoop-фильтрации даже в пределах одного сокета.

Многоканальные чипы AMD, используемые для гипертранспорта. Zen использует Infinity Fabric между кластерами из 4 ядер в одном сокете; Я предполагаю, что он использует это и между сокетами.

(Забавный факт: мультипроцессорный гипертранспорт AMD K10 Opteron мог создать разрыв на 8-байтовых границах, в то время как 16-байтовые SIMD загрузки / хранения в одном сокете были на практике атомарными. Инструкции SSE: какие процессоры могут выполнять атомные операции памяти 16B? И Атомность в x86. Если вы считаете это переупорядочением, это один случай, когда мульти-сокет может сделать больше странностей с памятью, чем один сокет. Но это не зависит от NUMA per se: у вас будет то же самое со всей памятью, подключенной к одному. разъем для настройки UMA.)


Связанные с:

Смотрите также дубликаты ссылок в разделе В чем разница в логике и производительности между LOCK XCHG и MOV+MFENCE? для xchg против mov+mfence. На современных процессорах, особенно Skylake, mov + mfence определенно медленнее для некоторых способов тестирования, чем xchg и оба являются эквивалентными способами сделать seq_cst хранить.

release или же relaxed магазин просто нуждается в равнине mov и до сих пор имеет те же гарантии заказа TSO.

Я думаю, что даже слабо упорядоченные магазины NT по-прежнему видны всем ядрам в порядке, с которым они могут согласиться. "Слабость" - это порядок, который становится глобально видимым по отношению к. другие нагрузки + магазины из ядра делают их.

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