1

Saiu o SIMD portátil do Go 1.27, medi em dois sistemas de produção: um deu 2,7× o outro zero e um grande risco encontrado

O Go 1.27 saiu com um pacote SIMD portátil (GOEXPERIMENT=simd): você escreve o laço uma vez e o compilador gera uma versão para cada largura de vetor (4 faixas no NEON do Mac, 8 no AVX2, 16 no AVX-512), escolhida quando o programa sobe. O 1.26 só tinha SIMD para amd64; agora tem arm64 e Wasm também.

Peguei três trechos quentes de dois sistemas meus e medi. Não foi benchmark sintético: usei 182.976 medições reais de vibração de um rolamento (dataset CWRU, 12 kHz). Resumo do que achei, com os números, e depois o que me preocupou de verdade.

O que acelerou (e o que não)

TrechoEscalarCom SIMDResultado
Conferência do codec (maior desvio entre original e reconstruído)3,87 ns/medição1,74 ns2,2× mais rápido, bit a bit idêntico
Histograma das variações (128 caixas)6,6 ns7,2 nsnada (dentro do ruído)
Distância entre vetores (128 dim, 1.024 centroides)335 µs126 µs2,7× mais rápido

O histograma não ganhou porque o trabalho pesado ali é bins[i]++, uma posição de memória diferente por valor, e o pacote ainda não tem scatter. Também não tem redução (ReduceSum vem no 1.28) nem alargamento float32→float64. É experimental e a API pode mudar.

Já o coração do codec, onde de fato está o custo (~53 ns/amostra), não vetoriza nem vai vetorizar: cada previsão depende da reconstrução anterior, e o range coder é serial. Lei de Amdahl: o teto de ganho no encode inteiro é de poucos %.

A surpresa: o modo de emulação

Quando o chip não tem SIMD, ou alguém deixa um GODEBUG=simd=0 esquecido no ambiente, o Go emula as instruções em software. A documentação diz que a emulação é competente. Medi:

  • conferência: 37 ns/medição, ~10× mais lenta que o escalar (não que o SIMD: que o código comum, um número por vez);
  • histograma: 26 ns, ~4× mais lento.

Ou seja: uma variável de ambiente errada num servidor vira uma regressão grande e silenciosa. Emulação não é plano B. Se você adotar, mantenha o caminho escalar escrito e escolhido de propósito.

O que me preocupou: os bits mudam com a máquina

Meu codec de telemetria promete uma coisa acima de tudo: o mesmo arquivo, byte a byte, em qualquer máquina, porque o arquivo é selado com SHA-256 e conferido em outro lugar.

Passei as somas de cada bloco para SIMD e comparei com a versão escalar: o resultado mudou em 98,7% dos blocos. Em float, (a+b)+c nem sempre é a+(b+c), e somar em 4 subtotais paralelos arredonda em outros lugares. Os números que vão para o arquivo saíram iguais nos 304 blocos porque o arredondamento final absorveu, mas isso foi sorte com esse sinal, não garantia.

Pior: usei MulAdd (multiplica e soma num passo só). O mesmo binário, com o mesmo dado, deu um hash no ARM (que funde a operação) e outro no amd64 sob Rosetta 2 (que não funde). Como a largura do vetor também muda com o chip (4, 8 ou 16 faixas), toda redução em ponto flutuante passa a ter ordem de soma dependente do hardware. Uma API "agnóstica de largura" é, por construção, agnóstica de bits.

As regras que adotei

  1. SIMD só em operação que não depende de ordem: máximo, mínimo, comparação, subtração e divisão elemento a elemento. Soma de float que vira byte, hash ou selo fica de fora.
  2. Emulação não é fallback. Caminho escalar explícito.
  3. Onde a soma é inevitável, conferir perto da fronteira. Meu índice de busca promete recall 1,0 (se está dentro do raio, aparece). Calculei o erro máximo da distância em float32 (~8 milionésimos da distância, para 128 dimensões) e testei contra a conta exata em 3.000 pares: nunca passou. Aí plantei 20 mil pontos rente ao raio: o float32 sozinho errou de 9 a 38 deles. Com uma faixa de recheck em precisão total perto do raio: zero erros, ao custo de 16 recálculos em 200 mil (0,008%). Os 2,7× sobrevivem quase inteiros.
  4. Medir na máquina de produção. O tamanho do bloco muda com o chip, e o resultado também.

Onde SIMD rende, em geral

Rende muito quando a mesma conta se repete sobre dados independentes, já enfileirados na memória, com poucas decisões no meio e números pequenos (com bytes cabem 16 a 64 por bloco; com float64, 2 a 8). Por isso parsing de JSON/CSV, validação de UTF-8, base64, CRC, AES e distância entre vetores inteiros ganham tanto, e cálculo científico em float64 ganha pouco.

Rende pouco ou atrapalha: laços em que cada passo depende do anterior (compressores de entropia, predição adaptativa), acesso espalhado (grafos, tabelas), trechos curtos e, de novo, somas de float que precisam dar o mesmo resultado em qualquer máquina.

Ressalvas honestas

  • Medi num Mac ARM, mediana de 6 repetições, sem isolar o núcleo nem fixar frequência. Tem ruído; a diferença do histograma cabe nele.
  • Não medi AVX2 nem AVX-512 em servidor real (o Rosetta não emula AVX-512). É a próxima rodada, num Linux com taskset.
  • Os 2,7× são de um trecho com dados na cache. No índice inteiro, com milhões de vetores, a memória pode comer boa parte.
  • Nada disso vai para produção enquanto o pacote for experimental. O que já vale é a regra 1.

O texto completo, com os gráficos e os hashes lado a lado, está no meu caderno de pesquisa: https://stickybit.com.br/caderno/simd-outros-bits/

Pergunta para quem já usa: alguém mediu o pacote portátil em AVX-512 de verdade? E como vocês tratam determinismo em redução vetorizada, quando o resultado precisa ser reprodutível entre máquinas: recheck como fiz, soma em ordem fixa (pairwise/Kahan), ou simplesmente não vetorizam?

Carregando publicação patrocinada...