Я уже довольно давно использую 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
).
Я абсолютно не уверен, для чего нужна такая довольно сложная функция и как она должна сделать что-то атомарное.