Re[8]: Memory barrier не могу понять что это
От: okman Беларусь https://searchinform.ru/
Дата: 09.04.23 09:35
Оценка: 9 (1) +1
Здравствуйте, Философ, Вы писали:

Ф>Видел что? Во что превращается c++ intrinsic _mm_mfence?


Подчеркну: я описываю лишь то, что многократно видел сам за последние несколько лет. Без претензий на что-либо, просто наблюдения и выводы,
сделанные в контексте использования MS C++ под 32- и 64-битные версии Windows.

Если мы пишем 'MemoryBarrier' — компилятор впихнет туда xchg или какой-нибудь lock or.
Если используем Interlocked-функции или работаем через <atomic> из C++ (memory_order_seq_cst) — снова получаем на выходе xchg, cmpxchg, xadd и т.д.
Если заглянем в реализацию каких-нибудь примитивов синхронизации — там тоже много где используется "xchg и компания".
И т.д.

Почему там отсутствует (или исчезающе мало) mfence — я не берусь утверждать со 100% определенностью. Но подозреваю, что все очень прозаично и на
современных процессорах locked instructions вроде xchg выполняются быстрее, чем mfence.

Ну а sfence/lfence, если не рассматривать кейсы с "non-temporal hint", на x86 и x64 вообще бессмысленны, потому что там для "load-load" и
"store-store" порядок гарантируется архитектурой. И вставлять, например, sfence между двумя записывающими mov — только терять время.


Ф>Это потому, что указанные инструкции используются для атомарных операций, при этом суть их — блокировки. Все приведённые инструкции неявно выставляют LOCK# на шину. Барьеры же используются для lock-free алгоритмов.


Давайте рассмотрим xchg как пример. В контексте обсуждения эта инструкция обладает как минимум тремя полезными свойствами:

1. она атомарна
2. она работает как барьер, запрещая "перепрыгивать" через нее операциям записи или чтения
3. она сбрасывает буферы записи (store buffer), действуя аналогично инструкциям сериализации вроде cpuid

Пруф — документ "Intel® 64 and IA-32 Architectures Software Developer’s Manual (Combined Volumes)", редакция "May 2019":

8.2.2 Memory Ordering in P6 and More Recent Processor Families

...
Reads or writes cannot be reordered with I/O instructions, locked instructions, or serializing instructions.
...
Locked instructions have a total order.


11.10 STORE BUFFER

Intel 64 and IA-32 processors temporarily store each write (store) to memory in a store buffer. The store buffer
improves processor performance by allowing the processor to continue executing instructions without having to
wait until a write to memory and/or to a cache is complete. It also allows writes to be delayed for more efficient use
of memory-access bus cycles.

In general, the existence of the store buffer is transparent to software, even in systems that use multiple processors.
The processor ensures that write operations are always carried out in program order. It also insures that the
contents of the store buffer are always drained to memory in the following situations:

...

* When a LOCK operation is performed.

...


22.34 STORE BUFFERS AND MEMORY ORDERING

The Pentium 4, Intel Xeon, and P6 family processors provide a store buffer for temporary storage of writes (stores)
to memory (see Section 11.10, “Store Buffer”). Writes stored in the store buffer(s) are always written to memory
in program order, with the exception of “fast string” store operations (see Section 8.2.4, “Fast-String Operation and
Out-of-Order Stores”).

The Pentium processor has two store buffers, one corresponding to each of the pipelines. Writes in these buffers
are always written to memory in the order they were generated by the processor core.

It should be noted that only memory writes are buffered and I/O writes are not. The Pentium 4, Intel Xeon, P6
family, Pentium, and Intel486 processors do not synchronize the completion of memory writes on the bus and
instruction execution after a write. An I/O, locked, or serializing instruction needs to be executed to synchronize
writes with the next instruction (see Section 8.3, “Serializing Instructions”).


И теперь вопрос от меня: чем xchg хуже mfence в плане использования в качестве барьера?


O>>В обычном прикладном коде такие вещи практически не встречаются. Ну и сам по себе mfence, если верить дискуссиям в Гугле, занимает больше тактов CPU по сравнению с

O>>locked-инструкциями типа xchg.

Ф>Весьма примерно представляю, как это проверить. Буду рад примеру кода, который подтвердит или опровергнет эту гипотезу.


У меня есть только ссылка, ведущая на обсуждение в группы linux kernel, где, насколько я понял, там в итоге отказались от mfence в пользу lock:

x86 memory barrier: why does Linux prefer MFENCE to Locked ADD?
https://groups.google.com/g/fa.linux.kernel/c/hNOoIZc6I9E/m/WlyXcgwoCwAJ


O>>Например, очень часто компилятор не вставляет никаких специальных

O>>барьеров туда, где ты вроде бы их ожидаешь. Например потому, что определенные гарантии предоставляет сама архитектура: "stores are not reordered with other stores" и т.д.

Ф>Не понимаю о чём идёт речь. Что и где должно теоретически быть, но не вставляет? Я думал, что синхронизация целиком и полностью ответственность программиста.


Ну например, примерно такой код обычно приводится в качестве примера использования acquire/release семантики:
#include <atomic>
#include <cstdio>
#include <thread>
 
int data;
std::atomic<bool> ready;
 
void producer()
{
    data = 123;
    ready.store(true, std::memory_order_release);
}
 
void consumer()
{
    while (false == ready.load(std::memory_order_acquire)) {}
    printf("data = %d", data);
}
 
int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join(); t2.join();
    return 0;
}


Здесь, мол, у нас в producer между записью в data и ready будет барьер, гарантирующий, что первое будет завершено строго до второго. И то же самое в consumer —
поток сначала увидит ready, и только потом начнет читать data. Но если посмотреть сгенерированный ассемблерный код, там нет никаких явно выставленных барьеров
при работе с переменной 'ready', просто mov на чтение и mov на запись:
void producer(void) PROC
    mov     DWORD PTR int data, 123
    npad    1
    mov     BYTE PTR std::atomic<bool> ready, 1
    ret     0
void producer(void) ENDP 

void consumer(void) PROC
    npad    2
$LL2@consumer:
    movzx   eax, BYTE PTR std::atomic<bool> ready
    npad    1
    test    al, al
    je      SHORT $LL2@consumer
    ...
void consumer(void) ENDP
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.