Clang не встроенный std :: atomi c :: загрузка для загрузки 64-битных структур - PullRequest
4 голосов
/ 28 февраля 2020

Рассмотрим следующий код, который использует std::atomic для атомарной загрузки 64-битного объекта.

#include <atomic>

struct A {
    int32_t x, y;
};

A f(std::atomic<A>& a) {
    return a.load(std::memory_order_relaxed);
}

С G CC происходят хорошие вещи, и генерируется следующий код. (https://godbolt.org/z/zS53ZF)

f(std::atomic<A>&):
        mov     rax, QWORD PTR [rdi]
        ret

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

С Clang, однако, история другая. Clang генерирует следующее. (https://godbolt.org/z/d6uqrP)

f(std::atomic<A>&):                     # @f(std::atomic<A>&)
        push    rax
        mov     rsi, rdi
        mov     rdx, rsp
        mov     edi, 8
        xor     ecx, ecx
        call    __atomic_load
        mov     rax, qword ptr [rsp]
        pop     rcx
        ret
        mov     rdi, rax
        call    __clang_call_terminate
__clang_call_terminate:                 # @__clang_call_terminate
        push    rax
        call    __cxa_begin_catch
        call    std::terminate()

Это проблематично c для меня по нескольким причинам:

  1. Более очевидно, есть гораздо больше инструкций, поэтому я ожидаю, что код будет менее эффективным
  2. Менее очевидно, обратите внимание, что сгенерированный код также включает в себя вызов библиотечной функции __atomic_load, что означает, что мой двоичный файл должен быть связан с libatomi c. Это означает, что мне нужны разные списки библиотек для связи в зависимости от того, использует ли пользователь моего кода G CC или Clang.
  3. Функция библиотеки может использовать блокировку, что приведет к снижению производительности

Важный вопрос, который у меня сейчас возникает, заключается в том, есть ли способ заставить Clang также преобразовать нагрузку в одну инструкцию. Мы используем это как часть библиотеки, которую планируем распространять среди других, поэтому мы не можем полагаться на конкретный используемый компилятор. Решение, предложенное мне до сих пор, состоит в том, чтобы использовать тип punning и хранить структуру внутри объединения вместе с 64-битным int, поскольку Clang правильно загружает 64-битные целые числа атомарно в одной инструкции. Однако я скептически отношусь к этому решению, поскольку, хотя оно, похоже, работает на всех основных компиляторах, я прочитал, что на самом деле это неопределенное поведение. Такой код также не особенно удобен для чтения и понимания другими, если они не знакомы с уловкой.

Подводя итог, можно ли атомарно загрузить 64-битную структуру, которая:

  1. Работает как в Clang, так и в G CC, и, предпочтительно, в большинстве других популярных компиляторов,
  2. Генерирует одну команду при компиляции,
  3. Не неопределенное поведение,
  4. Читатель дружелюбен?

1 Ответ

6 голосов
/ 28 февраля 2020

Эта пропущенная оптимизация происходит только с libstdc ++; лягните на линии Годболта, как мы ожидаем для -stdlib=libc++. https://godbolt.org/z/Tt8XTX.

Кажется, что для выравнивания руки достаточно дать 64-битное выравнивание struct.

libstdc++ ' Шаблон s std::atomic делает это для типов, которые достаточно малы, чтобы быть атомарными c при естественном выравнивании, но, возможно, clang ++ видит только выравнивание базового типа, а не члена класса atomic<T>, в реализации libstdc ++. Я не исследовал; кто-то должен сообщить об этом в bugzilla clang / LLVM.

#include <atomic>
#include <stdint.h>  // you forgot this header.

struct A {
    alignas(2 * sizeof(int32_t)) int32_t x;
    int32_t y;  // this one must be separate, otherwise y would also be aligned -> 16-byte object
};

A f(std::atomic<A>& a) {
    return a.load(std::memory_order_relaxed);
}

Выравнивание по размеру структуры делает его c из alignof(int64_t), что на 32-битном ABI может быть только 4. (И Я не использовал alignas(8), чтобы избежать чрезмерного выравнивания в системах, где char 32-битный и sizeof (int64_t) = 2.) Это может быть излишне сложно, и alignas(int64_t) легче читать, даже если это не всегда то же самое, что и придание этой структуре естественного выравнивания.)

Godbolt

# clang++ 9.0  -std=gnu++17 -O3;  g++ is the same
f(std::atomic<A>&):
        mov     rax, qword ptr [rdi]
        ret

Кстати, нет, libatomic функция библиотеки не будет использовать блокировку; он знает, что 8-байтовые выровненные нагрузки - это, естественно, атомы c, и что другие используемые потоки будут использовать обычные загрузки / хранилища, а не блокировки.

В старшем кланге по крайней мере используется call __atomic_load_8 вместо универсального c переменной величины, но это все еще большая пропущенная оптимизация.

Интересный факт: clang -m32 будет использовать lock cmpxchg8b для реализации 8-байтовой загрузки атома c вместо использования SSE или fild, как G CC. : /

...