Почему GCC std :: atomic генерирует неэффективную неатомарную сборку? - PullRequest
11 голосов
/ 14 ноября 2011

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

C ++ 11 поддерживает std::atomic<> и его потомков, поэтому разумно использовать его вместо этого, поскольку это делает мой код совместимым со стандартом, и компилятор будет генерировать лучший код в любом случае, независимо от платформы, что почти слишком хорош, чтобы быть правдой.
Кстати, мне нужно было бы просто сменить текст atomic на std::atomic тоже. В std::atomic есть много вещей, которые мне не нужны, но параметры по умолчанию позаботятся об этом.

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

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

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

Пример "как просто, как может":

#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).

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

Ответы [ 2 ]

14 голосов
/ 15 ноября 2011

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

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

/* 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  #
3 голосов
/ 14 ноября 2011

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

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

...