Como implementar "_mm_storeu_epi64" sem problemas de alias?
(Nota: embora esta pergunta seja sobre "armazenamento", o caso "carregar" tem os mesmos problemas e é perfeitamente simétrico.)
Os intrínsecos SSE fornecem uma_mm_storeu_pd
função com a seguinte assinatura:
void _mm_storeu_pd (double *p, __m128d a);
Portanto, se eu tiver um vetor de duas dobras e quiser armazená-las em uma matriz de duas dobras, posso usar isso intrínseco.
No entanto, meu vetor não é dois duplos; são dois inteiros de 64 bits e quero armazená-lo em uma matriz de dois inteiros de 64 bits. Ou seja, eu quero uma função com a seguinte assinatura:
void _mm_storeu_epi64 (int64_t *p, __m128i a);
Mas os intrínsecos não fornecem essa função. O mais próximo que eles têm é_mm_storeu_si128
:
void _mm_storeu_si128 (__m128i *p, __m128i a);
O problema é que essa função leva um ponteiro para__m128i
, enquanto minha matriz é uma matriz deint64_t
. Gravar em um objeto através do tipo incorreto de ponteiro é uma violação doaliasing estrito e é definitivamente um comportamento indefinido. Estou preocupado que meu compilador, agora ou no futuro, reorganize ou otimize a loja, interrompendo assim meu programa de maneiras estranhas.
Para ser claro, o que eu quero é uma função que eu possa invocar assim:
__m128i v = _mm_set_epi64x(2,1);
int64_t ra[2];
_mm_storeu_epi64(&ra[0], v); // does not exist, so I want to implement it
Aqui estão seis tentativas para criar essa função.
Tentativa nº 1void _mm_storeu_epi64(int64_t *p, __m128i a) {
_mm_storeu_si128(reinterpret_cast<__m128i *>(p), a);
}
Este parece ter o estrito problema de aliasing com o qual estou preocupado.
Tentativa # 2void _mm_storeu_epi64(int64_t *p, __m128i a) {
_mm_storeu_si128(static_cast<__m128i *>(static_cast<void *>(p)), a);
}
Possivelmente melhor em geral, mas não acho que isso faça diferença neste caso.
Tentativa nº 3void _mm_storeu_epi64(int64_t *p, __m128i a) {
union TypePun {
int64_t a[2];
__m128i v;
};
TypePun *p_u = reinterpret_cast<TypePun *>(p);
p_u->v = a;
}
Isso gera código incorreto no meu compilador (GCC 4.9.0), que emite um alinhadomovaps
instrução em vez de um desalinhadomovups
. (A união está alinhada, então areinterpret_cast
engana o GCC a assumirp_u
também está alinhado.)
void _mm_storeu_epi64(int64_t *p, __m128i a) {
union TypePun {
int64_t a[2];
__m128i v;
};
TypePun *p_u = reinterpret_cast<TypePun *>(p);
_mm_storeu_si128(&p_u->v, a);
}
Isso parece emitir o código que eu quero. O truque "punção de tipo por união", emboratecnicamente indefinido em C ++, éamplamente suportado. Mas é este exemplo - onde passo um ponteiro para um elemento de uma união, em vez de acessá-la através da própria união - realmente uma maneira válida de usar a união para punção de tipo?
Tentativa # 5void _mm_storeu_epi64(int64_t *p, __m128i a) {
p[0] = _mm_extract_epi64(a, 0);
p[1] = _mm_extract_epi64(a, 1);
}
Isso funciona e é perfeitamente válido, mas emite duas instruções em vez de uma.
Tentativa # 6void _mm_storeu_epi64(int64_t *p, __m128i a) {
std::memcpy(p, &a, sizeof(a));
}
Isso funciona e é perfeitamente válido ... eu acho. Mas emite um código francamente terrível no meu sistema. Derramamentos de GCCa
para um slot de pilha alinhado através de uma loja alinhada e, em seguida, move manualmente as palavras do componente para o destino. (Na verdade, ele é derramado duas vezes, uma vez para cada componente. Muito estranho.)
...
Existe alguma maneira de escrever essa função que (a) gere um código ideal em um compilador moderno típico e (b) tenha um risco mínimo de ocorrer um aliasing estrito?