Pergunta

Existe alguma diferença entre a intrínseca lógica de SSE para diferentes tipos? Por exemplo, se aceitarmos ou operarmos, existem três intrínsecos: _mm_or_ps, _mm_or_pd e _mm_or_si128, tudo o que fazem a mesma coisa: calcular bit a bit Ou de seus operando. Minhas perguntas:

  1. Existe alguma diferença entre usar um ou outro intrínseco (com fundição de tipo apropriado). Não haverá custos ocultos como execução mais longa em alguma situação específica?

  2. Esses intrinsics mapeiam três instruções diferentes X86 (POR, ORPS, ORPD). Alguém tem alguma idéia de por que a Intel está desperdiçando espaço precioso de código de opções para várias instruções que fazem a mesma coisa?

Foi útil?

Solução

Eu acho que todos os três são efetivamente os mesmos, ou seja, operações de 128 bits bit newwise. A razão pela qual existem formas diferentes é provavelmente histórica, mas não tenho certeza. Eu acho que é possível que pode haver algum comportamento adicional nas versões de ponto flutuante, por exemplo, quando há Nans, mas isso é pura adivinhação. Para entradas normais, as instruções parecem intercambiáveis, por exemplo,

#include <stdio.h>
#include <emmintrin.h>
#include <pmmintrin.h>
#include <xmmintrin.h>

int main(void)
{
    __m128i a = _mm_set1_epi32(1);
    __m128i b = _mm_set1_epi32(2);
    __m128i c = _mm_or_si128(a, b);

    __m128 x = _mm_set1_ps(1.25f);
    __m128 y = _mm_set1_ps(1.5f);
    __m128 z = _mm_or_ps(x, y);

    printf("a = %vld, b = %vld, c = %vld\n", a, b, c);
    printf("x = %vf, y = %vf, z = %vf\n", x, y, z);

    c = (__m128i)_mm_or_ps((__m128)a, (__m128)b);
    z = (__m128)_mm_or_si128((__m128i)x, (__m128i)y);

    printf("a = %vld, b = %vld, c = %vld\n", a, b, c);
    printf("x = %vf, y = %vf, z = %vf\n", x, y, z);

    return 0;
}

$ gcc -Wall -msse3 por.c -o por

$ ./por

a = 1 1 1 1, b = 2 2 2 2, c = 3 3 3 3
x = 1.250000 1.250000 1.250000 1.250000, y = 1.500000 1.500000 1.500000 1.500000, z = 1.750000 1.750000 1.750000 1.750000
a = 1 1 1 1, b = 2 2 2 2, c = 3 3 3 3
x = 1.250000 1.250000 1.250000 1.250000, y = 1.500000 1.500000 1.500000 1.500000, z = 1.750000 1.750000 1.750000 1.750000

Outras dicas

  1. Existe alguma diferença entre usar um ou outro intrínseco (com fundição de tipo apropriado). Não haverá custos ocultos como execução mais longa em alguma situação específica?

Sim, pode haver razões de desempenho para escolher uma versus a outra.

1: Às vezes, há um ciclo extra ou dois de latência (atraso de encaminhamento) se a saída de uma unidade de execução inteira precisar ser roteada para a entrada de uma unidade de execução de FP ou vice -versa. É preciso muitos fios para mover 128b de dados para qualquer um dos muitos destinos possíveis; portanto, os designers da CPU precisam fazer trocas, como apenas um caminho direto de cada saída FP para cada entrada de FP, não para todas as entradas possíveis.

Ver esta resposta, ou Docp de microarquitetura de Agner Fog para delírios de desvio. Pesquise "atrasos de ignição de dados em Nehalem" no Doc da Agner; Tem alguns bons exemplos práticos e discussões. Ele tem uma seção para cada microarch que analisou.

No entanto, os atrasos para passar dados entre os diferentes domínios ou diferentes tipos de registros são menores na ponte Sandy e Ivy Bridge do que no Nehalem, e geralmente zero. - Micro Arch Doc da Agner Fog

Lembre -se de que a latência não importa se não estiver no caminho crítico do seu código. Usando pshufd ao invés de movaps + shufps Pode ser uma vitória se a taxa de transferência da UOP for o seu gargalo, em vez de latência do seu caminho crítico.

2: o ...ps A versão leva menos 1 byte de código do que os outros dois. Isso alinhará as seguintes instruções de maneira diferente, o que pode ser importante para os decodificadores e/ou linhas de cache UOP.

3: As CPUs Intel recentes podem executar apenas as versões FP no PORT5.

  • Merom (Core2) e Penryn: orps pode ser executado em P0/P1/P5, mas apenas no domínio inteiro. Presumivelmente, todas as 3 versões decodificadas exatamente na mesma UOP. Portanto, o atraso de encaminhamento cruzado acontece. (As CPUs da AMD também fazem isso: as instruções bit -bitwluche são executadas no domínio IVEC.)

  • Nehalem / Sandybridge / IVB / Haswell / Broadwell: por pode ser executado em p0/p1/p5, mas orps pode ser executado apenas na porta5. P5 também é necessário para as shuffles, mas as unidades FMA, FP Add e FP MUL estão nas portas 0/1.

  • Skylake: por e orps Ambos têm taxa de transferência de 3 por ciclo. Informações sobre atrasos no encaminhamento ainda não estão disponíveis.

Observe que no SNB/IVB (AVX, mas não Avx2), apenas o P5 precisa lidar com operações lógicas de 256b, como vpor ymm, ymm requer avx2. Provavelmente essa não foi a razão da mudança, já que Nehalem fez isso.

Como escolher com sabedoria:

Se a taxa de transferência lógica do Port5 puder ser um gargalo, use as versões inteiras, mesmo nos dados de FP. Isso é especialmente verdadeiro se você deseja usar shuffles inteiros ou outras instruções de movimentação de dados.

A AMD CPUS sempre usa o domínio inteiro para lógicos; portanto, se você tiver várias coisas no domínio inteiro, faça-as de uma só vez para minimizar as viagens de ida e volta entre os domínios. As latências mais curtas liberarão as coisas do buffer de reordenação mais rapidamente, mesmo que uma cadeia DEP não seja o gargalo do seu código.

Se você deseja apenas definir/limpar/flip um pouco nos vetores FP entre as instruções de add e mul, use o ...ps lógicos, mesmo em dados de precisão dupla, porque FP único e duplo são o mesmo domínio em cada CPU existente, e o ...ps As versões são um byte mais curto.

Existem razões práticas / fator humano para usar o ...pd As versões, porém, que muitas vezes superam a economia de 1 byte de código. A legibilidade do seu código por outros seres humanos é um fator: eles se perguntam por que você está tratando seus dados como solteiros quando realmente dobrar. Esp. com C/C ++ Intrinsics, espalhando seu código com elenco entre __mm256 e __mm256d não vale a pena. Se o ajuste no nível do alinhamento INSn é importante, escreva diretamente no ASM, não intrínsecos! (Ter a instrução um byte mais tempo pode alinhar as coisas melhor para a densidade e/ou decodificadores da linha de cache UOP.)

Para dados inteiros, use as versões inteiras. Salvar um byte de instrução não vale o atraso de desvio, e o código inteiro geralmente mantém o Port5 totalmente ocupado com shuffles. Para Haswell, muitas instruções de embaralhamento / inserção / extração / embalagem / descompactar se tornaram apenas P5, em vez de P1 / P5 para SNB / IVB.

  1. Esses intrinsics mapeiam para três instruções diferentes X86 (por, orps, orpd). Alguém tem alguma idéia de por que a Intel está desperdiçando espaço precioso de código de opções para várias instruções que fazem a mesma coisa?

Se você olhar para a história desses conjuntos de instruções, poderá ver como chegamos aqui.

por  (MMX):     0F EB /r
orps (SSE):     0F 56 /r
orpd (SSE2): 66 0F 56 /r
por  (SSE2): 66 0F EB /r

O MMX existia antes da SSE, por isso parece opcodes para SSE (...ps) As instruções foram escolhidas do mesmo 0F xx espaço. Então para SSE2, o ...pd Versão adicionada a 66 prefixo do tamanho de operando para o ...ps opcode e a versão inteira adicionada um 66 Prefixo à versão MMX.

Elas poderia deixou de fora orpd e/ou por, mas eles não o fizeram. Talvez eles pensassem que os futuros designs da CPU poderiam ter caminhos de encaminhamento mais longos entre diferentes domínios e, portanto, usar as instruções correspondentes para seus dados seria um negócio maior. Embora existam códigos opciais separados, a AMD e a Intel primitiva os trataram da mesma forma, como o Vector Int.

De acordo com as diretrizes de otimização da Intel e da AMD, a mistura de tipos de opções com tipos de dados produz um desempenho atingido, pois a CPU tags internamente as metades de 64 bits do registro para um tipo de dados específico. Isso parece afetar principalmente a linha de tubos à medida que a instrução é decodificada e os UOPs são agendados. Funcionalmente eles produzem o mesmo resultado. As versões mais recentes para os tipos de dados inteiros têm codificação maior e ocupam mais espaço no segmento de código. Portanto, se o tamanho do código for um problema, use as operações antigas, pois elas têm codificação menor.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top