He estado utilizando builtins compatibles con Intel de gcc (como __sync_fetch_and_add
) durante bastante tiempo, utilizando mi propia plantilla atomic
. Las funciones "__sync
" ahora se consideran oficialmente "heredadas".¿Por qué GCC std :: atomic increase genera un ensamblaje ineficiente no atómico?
C++ 11 admite std::atomic<>
y sus descendientes, por lo que parece razonable usar eso en su lugar, ya que hace que mi código sea compatible y el compilador generará el mejor código de cualquier manera, independientemente de la plataforma, es decir casi demasiado bueno para ser verdad.
Por cierto, solo tendría que reemplazar texto atomic
con std::atomic
, también. Hay mucho en std::atomic
(re: modelos de memoria) que realmente no necesito, pero los parámetros predeterminados se encargan de eso.
Ahora las malas noticias. Como resultado, el código generado es, por lo que puedo decir, ... una mierda total, y ni siquiera atómico en absoluto. Incluso un ejemplo mínimo que incrementa una única variable atómica y lo emite tiene no menos de 5 llamadas a funciones no en línea a ___atomic_flag_for_address
, ___atomic_flag_wait_explicit
y __atomic_flag_clear_explicit
(totalmente optimizadas), y por otro lado, no hay una sola instrucción atómica en el ejecutable generado
¿Qué ofrece? Por supuesto, siempre existe la posibilidad de un error en el compilador, pero con la gran cantidad de revisores y usuarios, tales cosas bastante drásticas generalmente no pasan desapercibidas. Lo que significa que probablemente esto no es un error, sino un comportamiento intencionado.
¿Cuál es el "fundamento" detrás de tantas llamadas a funciones, y cómo se implementa la atomicidad sin atomicidad?
Como simple-como-se-puede-get ejemplo:
#include <atomic>
int main()
{
std::atomic_int a(5);
++a;
__builtin_printf("%d", (int)a);
return 0;
}
produce el siguiente .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
... y las funciones mencionadas se ven, por ejemplo, como este en 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
Los otros son algo más simple, pero no encuentro una sola instrucción que realmente sería atómica (a excepción de algunos espuria xchg
cuales son atómica sobre X86, pero estos parecen ser más bien NOP/relleno, ya que es xchg %ax,%ax
después de ret
).
No estoy seguro de para qué se necesita una función tan complicada, y cómo se debe hacer algo atómico.
¿Qué versión de GCC estás utilizando? ¿Puedes mostrar un pequeño programa que tenga como resultado un código tan pobre? Estoy ejecutando una instantánea de 4.7 del mes pasado y parece producir un código decente, con instrucciones de 'bloqueo'. –
El modelo de memoria que "no necesitas" viene a la mente como un posible culpable. ¿Cómo se ve tu código? Además, ¿qué quiere decir con la última frase: "¿Cómo se aplica la atomicidad sin atomicidad"? – jalf
Por "modelos de memoria", ¿quiere decir "pedidos de memoria"? –