Атомные операции и генерация кода для gcc

Я смотрю на сборку, сгенерированную для атомных операций gcc. Я попробовал следующую короткую последовательность:

int x1;
int x2;

int foo;

void test()
{
  __atomic_store_n( &x1, 1, __ATOMIC_SEQ_CST );
  if( __atomic_load_n( &x2  ,__ATOMIC_SEQ_CST ))
    return;

  foo = 4;
}

Глядя на атомное оружие Herb Sutter по генерации кода, он упоминает, что руководство X86 обязуется использовать xchg для атомных хранилищ и простой mov для атомных чтений. Поэтому я ожидал чего-то вроде:

test():
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    $1, %eax
    xchg    %eax, x1(%rip)
    movl    x2(%rip), %eax
    testl   %eax, %eax
    setne   %al
    testb   %al, %al
    je      .L2
    jmp     .L1
.L2:
    movl    $4, foo(%rip)
.L1:
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc

Если забор памяти является неявным из-за заблокированной инструкции xchg.

Однако, если я скомпилирую это с помощью gcc -march=core2 -S test.cc, я получаю следующее:

test():
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    $1, %eax
    movl    %eax, x1(%rip)
    mfence
    movl    x2(%rip), %eax
    testl   %eax, %eax
    setne   %al
    testb   %al, %al
    je      .L2
    jmp     .L1
.L2:
    movl    $4, foo(%rip)
.L1:
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc

Поэтому вместо использования операции xchg gcc здесь использует комбинацию mov + mfence. В чем причина этого генерации кода, которая отличается от той, которая предусмотрена архитектурой x86 в соответствии с Herb Sutter?

Ответ 1

В инструкции xchg подразумевается семантика блокировки, когда местом назначения является ячейка памяти. Это означает, что вы можете поменять содержимое регистра на содержимое ячейки памяти атомарно.

Примером в вопросе является атомный магазин, а не своп. Модель памяти архитектуры x86 гарантирует, что в многопроцессорных/многоядерных системах хранения, выполненных одним потоком, в этом порядке будут отображаться другие потоки... поэтому достаточно движения памяти. Сказав это, есть более старые процессоры Intel и некоторые клоны, где есть ошибки в этой области, а xchg требуется в качестве обходного пути для этих процессоров. См. Раздел "Значительная оптимизация" этой статьи в википедии по спинлокам:

http://en.wikipedia.org/wiki/Spinlock#Example_implementation

Какие состояния

Простая реализация выше работает на всех процессорах, использующих архитектуру x86. Тем не менее, возможен ряд оптимизаций производительности:

В более поздних версиях архитектуры x86 spin_unlock может безопасно использовать разблокированный MOV вместо медленного заблокированного XCHG. Это происходит из-за тонких правил упорядочения памяти, которые поддерживают это, хотя MOV не является полным барьером памяти. Однако некоторые процессоры (некоторые процессоры Cyrix, некоторые версии Intel Pentium Pro (из-за ошибок) и более ранние системы Pentium и i486 SMP) будут делать не то, и данные, защищенные блокировкой, могут быть повреждены. На большинстве архитектур, отличных от x86, должен использоваться явный барьер памяти или атомарные инструкции (как в примере). В некоторых системах, таких как IA-64, существуют специальные инструкции "разблокировки", которые обеспечивают необходимый порядок памяти.

Защитный барьер памяти mfence гарантирует, что все магазины завершены (сохранение буферов в ядре процессора пуст, а значения хранятся в кеше или памяти), это также гарантирует, что никакие будущие загрузки не будут выполнены.

Тот факт, что MOV достаточен для разблокировки мьютекса (без необходимости сериализации или памяти), был "официально" разъяснен в ответ Линусу Торвальдсу архитектором Intel еще в 1999 году.

http://lkml.org/lkml/1999/11/24/90.

Я предполагаю, что позже было обнаружено, что это не сработало для некоторых старых процессоров x86.