2011-11-14 12 views
10

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.

+0

¿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'. –

+1

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

+0

Por "modelos de memoria", ¿quiere decir "pedidos de memoria"? –

Respuesta

14

Es una compilación de compilador inadecuada.

Compruebe su c++config.h, que shoukld este aspecto, pero no es así:

/* 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 

Estas macros se definen o no dependiendo de configure pruebas, que comprueban el apoyo máquina host para __sync_XXX funciones. Estas pruebas están en libstdc++v3/acinclude.m4, AC_DEFUN([GLIBCXX_ENABLE_ATOMIC_BUILTINS] ....

En su instalación, es evidente a partir de la MEM[(__i_type *)&a] puesto en el archivo de ensamblaje por -fverbose-asm que el compilador utiliza macros de atomic_0.h, por ejemplo:

#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; }) 

Con un compilador adecuadamente construida, con su programa de ejemplo, c++ -m32 -std=c++0x -S -O2 -march=core2 -fverbose-asm debería producir algo como esto:

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 # 
+1

Y adivine qué, la edición 'C++ config.h' para contener esos define corrige el problema, dándome exactamente la secuencia' lock addl, mfence' que publicó anteriormente, que es lo que yo quería, también. (Enviaré el problema a mi compilador). Muchas gracias. – Damon

3

Hay dos implementaciones. Uno que usa las primitivas __sync y una que no. Además de una mezcla de los dos que solo usa algunos de esos primitivos. El que se selecciona depende de las macros _GLIBCXX_ATOMIC_BUILTINS_1, _GLIBCXX_ATOMIC_BUILTINS_2, _GLIBCXX_ATOMIC_BUILTINS_4 y _GLIBCXX_ATOMIC_BUILTINS_8.

Al menos se necesita la primera para la implementación mixta, todas son necesarias para la totalmente atómica. Es seems que si se definen depende de la máquina de destino (no se pueden definir para -mi386 y se deben definir para -mi686).

+0

Ninguno de estos se define aquí aunque insns atómicos están ciertamente disponibles (estoy compilando para '-march = core2') y trabajo sin problemas usando las funciones' __sync'. He intentado definir estas macros antes de incluir '' solo para ver si eso hace la diferencia, aunque no. Entonces, básicamente, ¿estás diciendo que esto es probablemente una especie de "implementación fallida de los pobres"? En ese caso, ¿cómo habilitaría el real (sin compilar mi propio gcc)? – Damon

Cuestiones relacionadas