Sto usando i builtin di gcc Intel-compatibili (come __sync_fetch_and_add
) per un po 'di tempo, usando il mio modello atomic
. Le funzioni "__sync
" sono ora ufficialmente considerate "legacy".Perché GCC std :: incremento atomico genera un assemblaggio non atomico inefficiente?
C++ 11 supporta std::atomic<>
ei relativi discendenti, quindi sembra ragionevole utilizzarlo, poiché rende il mio codice conforme allo standard e il compilatore produrrà il codice migliore in entrambi i modi, in modo indipendente dalla piattaforma, ovvero quasi troppo bello per essere vero
Per inciso, dovrei solo sostituire il testo atomic
con std::atomic
. C'è molto in std::atomic
(re: modelli di memoria) che non ho davvero bisogno, ma i parametri di default si occupano di questo.
Ora per le cattive notizie. A quanto pare, il codice generato è, da quello che posso dire, ... una schifezza totale, e nemmeno atomica. Anche un esempio minimo che incrementa una singola variabile atomica e gli output ha non meno di 5 chiamate di funzione non inline a ___atomic_flag_for_address
, e __atomic_flag_clear_explicit
(completamente ottimizzato), e d'altra parte, non c'è una singola istruzione atomica nel generato eseguibile.
Cosa dà? Ovviamente c'è sempre la possibilità di un bug del compilatore, ma con l'enorme numero di revisori e utenti, è improbabile che in genere cose così drastiche passino inosservate. Il che significa che probabilmente non è un bug, ma un comportamento intenzionale.
Qual è la "motivazione" alla base di così tante chiamate di funzione e come viene implementata l'atomicità senza atomicità?
As-semplice-as-it-può-get esempio:
#include <atomic>
int main()
{
std::atomic_int a(5);
++a;
__builtin_printf("%d", (int)a);
return 0;
}
produce il seguente .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
... e le funzioni citate aspetto esempio come questo in 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
Gli altri sono un po 'più semplice, ma non trovo una singola istruzione che sarebbe davvero essere atomica (diverso da alcune spurie xchg
che sono atomica su X86, ma questi sembrano essere piuttosto NOP/padding, dal momento che è xchg %ax,%ax
seguito da ret
).
Non sono assolutamente sicuro di quale sia la funzione così complicata per cui è necessario e come si intende rendere tutto atomico.
Quale versione di GCC stai usando? Puoi mostrare un piccolo programma che produce un codice così povero? Sto eseguendo un'istantanea 4.7 del mese scorso e sembra che produca codice decente, con le istruzioni di 'lock' in esso. –
Il modello di memoria che "non hai bisogno" mi viene in mente come possibile colpevole. Com'è il tuo codice? Inoltre cosa intendi con l'ultima frase: "Come viene realizzata l'atomicità senza atomicità"? – jalf
Con "modelli di memoria" intendi "ordini di memoria"? –