Atomic 64-битная запись с GCC

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

Проделав довольно много чтения, я пришел к пониманию, что могу установить значение 64-битного int атомарно в 64-битной системе 1.

Я нашел много этого чтения трудным, хотя, поэтому подумал, что я попытаюсь сделать тест, чтобы проверить это. Поэтому я написал простую программу с одним потоком, который установил бы переменную в одно из двух значений:

bool switcher = false;

while(true)
{
    if (switcher)
        foo = a;
    else
        foo = b;
    switcher = !switcher;
}

И еще один поток, который будет проверять значение foo:

while (true)
{
    __uint64_t blah = foo;
    if ((blah != a) && (blah != b))
    {
        cout << "Not atomic! " << blah << endl;
    }
}

Я установил a = 1844674407370955161; и b = 1144644202170355111;. Я запускаю эту программу и не получаю никаких предупреждений о том, что blah не a или b.

Отлично, похоже, что это, вероятно, атомная запись... но затем я изменил первый поток, чтобы установить a и b напрямую, например:

bool switcher = false;

while(true)
{
    if (switcher)
        foo = 1844674407370955161;
    else
        foo = 1144644202170355111;
    switcher = !switcher;
}

Я снова запустил и вдруг:

Not atomic! 1144644203261303193
Not atomic! 1844674406280007079
Not atomic! 1144644203261303193
Not atomic! 1844674406280007079

Что изменилось? В любом случае я назначаю большое число foo - компилятор обрабатывает постоянное число по-другому или я все неправильно понял?

Спасибо!

<ч/" > 1: Документация Intel CPU, раздел 8.1, Гарантированные Atomic Operations

2: Список разработчиков GCC, в котором обсуждается, что GCC не гарантирует его в документации, но ядро ​​и другие программы полагаются на него

Ответ 1

Разборка цикла, я получаю следующий код с gcc:

.globl _switcher
_switcher:
LFB2:
    pushq   %rbp
LCFI0:
    movq    %rsp, %rbp
LCFI1:
    movl    $0, -4(%rbp)
L2:
    cmpl    $0, -4(%rbp)
    je  L3
    movq    [email protected](%rip), %rax
    movl    $-1717986919, (%rax)
    movl    $429496729, 4(%rax)
    jmp L5
L3:
    movq    [email protected](%rip), %rax
    movl    $1486032295, (%rax)
    movl    $266508246, 4(%rax)
L5:
    cmpl    $0, -4(%rbp)
    sete    %al
    movzbl  %al, %eax
    movl    %eax, -4(%rbp)
    jmp L2
LFE2:

Итак, похоже, что gcc использует 32-разрядную инструкцию movl с 32-битными непосредственными значениями. Существует инструкция movq, которая может перемещать 64-разрядный регистр в память (или память в 64-разрядный регистр), но, похоже, не может установить немедленное перемещение адреса памяти, поэтому компилятор вынужден либо использовать временный регистр, а затем переместить значение в память или использовать для movl. Вы можете попытаться заставить его использовать регистр с помощью временной переменной, но это может не сработать.

Литература:

Ответ 2

http://www.x86-64.org/documentation/assembly.html

непосредственные значения внутри инструкций остаются 32 бит.

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

Ответ 3

Документация Intel CPU правильная, выровненная 8 байтов. Чтение/запись всегда является атомарным для последнего оборудования (даже в 32-разрядных операционных системах).

Что вы нам не говорите, используете ли вы 64-битное оборудование на 32-битной системе? Если это так, 8-байтная запись, скорее всего, будет разделена на две записи по 4 байта компилятором.

Просто взгляните на соответствующий раздел в объектном коде.