Почему GCC std:: атомный приращение генерирует неэффективную неатомную сборку?

Я использую gcc Intel-совместимые встроенные функции (например, __sync_fetch_and_add) в течение некоторого времени, используя собственный atomic шаблон. Функции "__sync" теперь официально считаются "наследием".

С++ 11 поддерживает std::atomic<> и его потомков, поэтому представляется разумным использовать это вместо этого, поскольку он делает мой код стандартным, а компилятор будет создавать лучший код в любом случае, независимо от платформы, что почти слишком хорошо, чтобы быть правдой.
Кстати, мне нужно будет только text-replace atomic с помощью std::atomic. Там много в std::atomic (модели re: memory), которые мне действительно не нужны, но параметры по умолчанию позаботятся об этом.

Теперь о плохих новостях. Как оказалось, сгенерированный код - это то, что я могу сказать,... полное дерьмо, и даже не атомное вообще. Даже минимальный пример, который увеличивает единую атомную переменную и выводит ее, имеет не менее 5 неинтенсивных вызовов функций ___atomic_flag_for_address, ___atomic_flag_wait_explicit и __atomic_flag_clear_explicit (полностью оптимизирован), а с другой стороны, нет единую инструкцию атома в сгенерированном исполняемом файле.

Что дает? Конечно, всегда есть вероятность ошибки компилятора, но с огромным количеством рецензентов и пользователей такие довольно резкие вещи, как правило, вряд ли останутся незамеченными. Это означает, что это, вероятно, не ошибка, а намеренное поведение.

Что такое "обоснование" стольких вызовов функций и как атомарность реализована без атомарности?

Пример As-simple-as-it-can-get:

#include <atomic>

int main()
{
    std::atomic_int a(5);
    ++a;
    __builtin_printf("%d", (int)a);
    return 0;
}

выдает следующее .s:

movl    $5, 28(%esp)     #, a._M_i
movl    %eax, (%esp)     # tmp64,
call    ___atomic_flag_for_address   #
movl    $5, 4(%esp)  #,
movl    %eax, %ebx   #, __g
movl    %eax, (%esp)     # __g,
call    ___atomic_flag_wait_explicit     #
movl    %ebx, (%esp)     # __g,
addl    $1, 28(%esp)     #, MEM[(__i_type *)&a]
movl    $5, 4(%esp)  #,
call    _atomic_flag_clear_explicit  #
movl    %ebx, (%esp)     # __g,
movl    $5, 4(%esp)  #,
call    ___atomic_flag_wait_explicit     #
movl    28(%esp), %esi   # MEM[(const __i_type *)&a], __r
movl    %ebx, (%esp)     # __g,
movl    $5, 4(%esp)  #,
call    _atomic_flag_clear_explicit  #
movl    $LC0, (%esp)     #,
movl    %esi, 4(%esp)    # __r,
call    _printf  #
(...)
.def    ___atomic_flag_for_address; .scl    2;  .type   32; .endef
.def    ___atomic_flag_wait_explicit;   .scl    2;  .type   32; .endef
.def    _atomic_flag_clear_explicit;    .scl    2;  .type   32; .endef

... и упомянутые функции выглядят, например. как это в objdump:

004013c4 <__atomic_flag_for_address>:
mov    0x4(%esp),%edx
mov    %edx,%ecx
shr    $0x2,%ecx
mov    %edx,%eax
shl    $0x4,%eax
add    %ecx,%eax
add    %edx,%eax
mov    %eax,%ecx
shr    $0x7,%ecx
mov    %eax,%edx
shl    $0x5,%edx
add    %ecx,%edx
add    %edx,%eax
mov    %eax,%edx
shr    $0x11,%edx
add    %edx,%eax
and    $0xf,%eax
add    $0x405020,%eax
ret    

Другие несколько проще, но я не нахожу ни одной инструкции, которая действительно была бы атомарной (кроме некоторых ложных xchg, которые являются атомарными на X86, но они кажутся скорее NOP/padding, так как это xchg %ax,%ax после ret).

Я абсолютно не уверен, для чего нужна такая довольно сложная функция, и как это означало сделать что-то атомное.

Ответ 1

Это неадекватная сборка компилятора.

Проверьте c++config.h, это shoukld выглядит так, но это не так:

/* Define if builtin atomic operations for bool are supported on this host. */
#define _GLIBCXX_ATOMIC_BUILTINS_1 1

/* Define if builtin atomic operations for short are supported on this host.
   */
#define _GLIBCXX_ATOMIC_BUILTINS_2 1

/* Define if builtin atomic operations for int are supported on this host. */
#define _GLIBCXX_ATOMIC_BUILTINS_4 1

/* Define if builtin atomic operations for long long are supported on this
   host. */
#define _GLIBCXX_ATOMIC_BUILTINS_8 1

Эти макросы определяются или не зависят от тестов configure, которые проверяют поддержку хост-компьютера для функций __sync_XXX. Эти тесты находятся в libstdc++v3/acinclude.m4, AC_DEFUN([GLIBCXX_ENABLE_ATOMIC_BUILTINS] ....

В вашей установке видно, что MEM[(__i_type *)&a] помещает в файл сборки -fverbose-asm, что компилятор использует макросы из atomic_0.h, например:

#define _ATOMIC_LOAD_(__a, __x)                        \
  ({typedef __typeof__(_ATOMIC_MEMBER_) __i_type;                          \
    __i_type* __p = &_ATOMIC_MEMBER_;                      \
    __atomic_flag_base* __g = __atomic_flag_for_address(__p);          \
    __atomic_flag_wait_explicit(__g, __x);                 \
    __i_type __r = *__p;                           \
    atomic_flag_clear_explicit(__g, __x);                      \
    __r; })

С правильно построенным компилятором, с вашей примерной программой, c++ -m32 -std=c++0x -S -O2 -march=core2 -fverbose-asm должен произвести что-то вроде этого:

movl    $5, 28(%esp)    #, a.D.5442._M_i
lock addl   $1, 28(%esp)    #,
mfence
movl    28(%esp), %eax  # MEM[(const struct __atomic_base *)&a].D.5442._M_i, __ret
mfence
movl    $.LC0, (%esp)   #,
movl    %eax, 4(%esp)   # __ret,
call    printf  #

Ответ 2

Существует две реализации. Тот, который использует примитивы __sync, и тот, который этого не делает. Плюс смесь из двух, которая использует только некоторые из этих примитивов. Выбор выбирается в зависимости от макросов _GLIBCXX_ATOMIC_BUILTINS_1, _GLIBCXX_ATOMIC_BUILTINS_2, _GLIBCXX_ATOMIC_BUILTINS_4 и _GLIBCXX_ATOMIC_BUILTINS_8.

По крайней мере, первый необходим для смешанной реализации, все они необходимы для полностью атомного. Он кажется, что независимо от того, определены ли они, зависит от целевой машины (они не могут быть определены для -mi386 и должны быть определены для -mi686).