Considere o seguinte exemplo compilado com -O3 -march=native
:
struct str{
volatile uint64_t a1;
volatile uint64_t a2;
volatile uint64_t a3;
volatile uint64_t a4;
};
int main(void){
struct str str1;
struct str str2;
str1.a1 = str2.a2;
str1.a2 = str2.a2;
str1.a3 = str2.a3;
str1.a4 = str2.a4;
}
Ele produz o seguinte código assembly:
main:
push rbp
vpxor xmm0, xmm0, xmm0
vmovdqu8 YMMWORD PTR [rsp-32], ymm0
mov rbp, rsp
mov rax, QWORD PTR [rsp-24]
mov QWORD PTR [rsp-64], rax
mov rax, QWORD PTR [rsp-24]
mov QWORD PTR [rsp-56], rax
mov rax, QWORD PTR [rsp-16]
mov QWORD PTR [rsp-48], rax
mov rax, QWORD PTR [rsp-8]
mov QWORD PTR [rsp-40], rax
xor eax, eax
vzeroupper
pop rbp
ret
Na minha máquina, KbL i7-8550U
ele produz praticamente o mesmo código de máquina:
(gdb) disas main
Dump of assembler code for function main:
pxor xmm0,xmm0
movaps XMMWORD PTR [rsp-0x28],xmm0
mov rax,QWORD PTR [rsp-0x20]
movaps XMMWORD PTR [rsp-0x18],xmm0
mov QWORD PTR [rsp-0x48],rax
mov rax,QWORD PTR [rsp-0x20]
mov QWORD PTR [rsp-0x40],rax
mov rax,QWORD PTR [rsp-0x18]
mov QWORD PTR [rsp-0x38],rax
mov rax,QWORD PTR [rsp-0x10]
mov QWORD PTR [rsp-0x30],rax
xor eax,eax
ret
Na minha máquina há suporte para avx2
, mas não SIMD
é usado para copiar.
Como sugerir gcc
o uso 256 bit
baseado SIMD
(já que a estrutura tem 256 bytes de tamanho)?
O GCC fará cada
volatile
acesso com um acesso separado em ASM, não como um elemento de um vetor SIMD. Se esses fossem endereços MMIO, isso seria necessário para a correção. Ao usarvolatile
, você proíbe o GCC de fazer a otimização desejada!Para testar como algo é compilado, escreva uma função que receba ponteiros ou atue em globais, conforme discutido em Como remover "ruído" da saída do assembly GCC/clang? . Consulte https://godbolt.org/z/5h9Gc9o9c : sem
volatile
, GCC e clang-march=skylake
usam AVX2 para seu embaralhamento conforme escrito ou para cópia direta com carregamento/armazenamento de 256 bits se você não duplicara2
.Se você for usar
volatile
cada umuint64_t
separadamente, você poderia ter usado apenas_Atomic
(commemory_order_relaxed
ourelease
.)Você não pode contar com a ordem disso porque
volatile
não faz nada para impedir a reordenação em tempo de compilação , exceto acessos voláteis em relação a outrosvolatile
s, portanto você não terá sincronização garantida de aquisição/liberação. É muito parecidomemory_order_relaxed
._Atomic uint64_t
commemory_order_acquire
/release
lhe daria a mesma geração de código,volatile
mas com comportamento garantido. Quando usar volátil com multithreading? - praticamente nunca, a menos que você não consiga_Atomic
fazerstd::atomic<>
um bom trabalho, como neste caso em que nem ele nemvolatile uint64_t
fará exatamente o que você deseja.Raio Deus
Mas cuidado, isso não funcionará em C++; A atribuição de struct se transforma em uma cópia por elemento e
std::atomic<>
o construtor de cópia de é excluído. Ou comvolatile
, o construtor de cópia implícito não está marcadovolatile
, portanto não copiará uma estrutura comvolatile
membros.Infelizmente, GCC e Clang não fazem nenhuma suposição sobre a atomicidade por elemento de carga/armazenamento vetorial e coleta/dispersão? então
_Atomic uint64_t
os membros resultam na cópia por elemento, e não conheço uma ótima maneira de contornar isso sem algum código hackeado. https://godbolt.org/z/8zGE4soMe . (E eles não otimizam os atômicos de qualquer maneira; os componentes internos do compilador provavelmente tratam os atômicos de maneira muito semelhantevolatile
, já que essa é uma maneira de garantir que eles não sejam otimizados.)Se a estrutura estivesse alinhada por 16, seria 100% seguro e garantido no papel copiá-la como duas metades de 128 bits com
movaps
ouvmovdqa
, em CPUs com AVX (pelo menos Intel), já que a Intel finalmente conseguiu documentar que AVX implica Atomicidade de carga/armazenamento de 128 bits para acessos alinhados . Instruções SSE: quais CPUs podem realizar operações atômicas de memória de 16B?Um hack do mundo real para obter o que você deseja, bastante seguro/à prova de futuro
Considere alinhar sua estrutura em 32 bytes (
_Alignas(32)
no primeiro membro) e usá-lavolatile __m256i*
para copiá-la. (Desreferenciar o ponteiro diretamente, não use_mm256_load_si256
.) É exatamente como usar,volatile uint64_t*
mas você está forçando o compilador a fazer um acesso de 32 bytes em vez de quatro acessos de 8 bytes.Toda a cópia da estrutura será atômica na prática em CPUs modernas, exceto para Alder Lake E-cores, onde cada metade de 128 bits será atômica. https://rigtorp.se/isatomic/.
Não conheço nenhum que rasgue em pedaços de 8 bytes, que é tudo o que você obtém da interpretação do GCC de
volatile
fornecer acessos livres de rasgos até a largura do registro inteiro, adequado para como o kernel do Linux o usa para atômicos. (Veja os comentários sobre esta resposta para o GCC evitando um armazenamento não atômico para voláteis no AArch64.)Mais importante ainda, não existe um mecanismo plausível para uma CPU rasgar os elementos de um amplo armazenamento SIMD, mesmo que ela seja dividida em pedaços de 8 ou 16 bytes para armazenar separadamente. Como está alinhado, os pedaços parciais de 8 bytes também estão alinhados, especialmente quando todo o vetor está alinhado naturalmente. No papel, acessos maiores que 16 bytes (ou maiores que 8 sem AVX) não têm garantias, portanto um Deathstation 9000 x86 poderia quebrar esse código, mas acessar o cache mais de uma vez para o mesmo pedaço de 8 bytes não faz sentido prático.
Usar
_mm256_loadu_si256
sem alinhamento não funcionaria porque não évolatile
. (No GNU C,volatile
é mais ou menos bem definido e suportado para rolar seus própriosrelaxed
atômicos. E não fique tentado a usarasm("" ::: "memory")
para forçar uma carga ou armazenamento em torno de acessos não voláteis: veja Quem tem medo de uma grande otimização ruim compilador? para algumas das otimizações mais obscuras que podem incomodar você, como inventar cargas extras para algo não volátil que a fonte lê uma vez.)Usar seu próprio
__attribute__((aligned(1),vector_size(32),may_alias))
vetor não alinhado para permitirvolatile*
seria uma cópia de 32 bytes com carregamento/armazenamento de instrução única ( ou metades de 16 bytes, dependendo das configurações de ajuste ). Mas se foi alinhado apenas por 8, no papel você não tem garantias sobre a atomicidade, mesmo dentro dos elementos de 8 bytes. E na prática você pode romper entre os elementos. Provavelmente é melhor alinhá-lo naturalmente com ele, definitivamente dentro de uma linha de cache, e não dividido entre bancos de cache ou qualquer outra coisa em algumas CPUs AMD.