- A primeira aula de assembly do FFmpeg organiza, em nível introdutório, por que SIMD em assembly escrito à mão é necessário no processamento de multimídia e quais são as convenções do FFmpeg para escrever funções
- O público-alvo deve conhecer ponteiros em C e os conceitos de escalar·vetor, adição e multiplicação em nível de ensino médio; a aula usa como base x86 64-bit e sintaxe Intel
- No FFmpeg, assembly function, SIMD e vectorise são usados quase com o mesmo sentido, indicando o processamento simultâneo de vários elementos de dados, algo que combina bem com imagem, vídeo e áudio
- Em termos de desempenho, há preferência por assembly manual; intrinsics costumam ser 10~15% mais lentos, e em comparações do dav1d a vetorização automática chegou a cerca de 2x, enquanto a versão escrita manualmente alcançou até 8x
- A primeira função de exemplo usa
x86inc.asm, INIT_XMM sse2, cglobal, movu, paddb e RET para somar via SIMD 16 bytes de dois buffers uint8_t e salvar o resultado de volta no primeiro buffer
Objetivo da aula e conhecimentos prévios
- FFmpeg Assembly Language Lesson One é uma aula introdutória que aborda os fundamentos de como a linguagem assembly é escrita no FFmpeg e ajuda a entender o que realmente acontece dentro do computador
- Os conhecimentos necessários são os seguintes
- Linguagem C, especialmente ponteiros
- Se você não conhece C, recomenda-se estudar The C Programming Language
- Conceitos de escalares e vetores, adição e multiplicação em nível de matemática do ensino médio
Linguagem assembly e SIMD
- Linguagem assembly é uma linguagem de programação em que se escreve, de forma legível para humanos, código que corresponde diretamente às instruções processadas pela CPU
- O código assembly legível por humanos é convertido por um assembler em dados binários de código de máquina (machine code) que a CPU entende
- O código assembly do FFmpeg é, em sua maior parte, do tipo SIMD (Single Instruction Multiple Data)
- Uma única instrução opera simultaneamente sobre vários elementos de dados
- Também é chamado de programação vetorial
- A programação escalar comum processa um elemento de dados por vez
- SIMD combina bem com processamento de imagem, vídeo e áudio, que lida com muitos dados organizados sequencialmente na memória
- No FFmpeg, as expressões abaixo são usadas quase com o mesmo significado
assembly function
SIMD
vectorise
- Ou seja, escrever manualmente uma função em assembly para processar vários elementos de dados ao mesmo tempo
Por que o FFmpeg escreve assembly diretamente
- O objetivo principal é aumentar a velocidade do processamento multimídia
- Ganhos de mais de 10x com código em assembly são comuns
- Isso é importante para reduzir travamentos na reprodução de vídeo em tempo real
- Também pode reduzir o consumo de energia e aumentar a duração da bateria
- Funções de codificação e decodificação de vídeo são usadas em enorme volume por usuários finais e datacenters, então até pequenas melhorias se acumulam rapidamente
- O FFmpeg usa assembly escrito à mão em vez de intrinsics
- Intrinsics são funções parecidas com C que correspondem a instruções de assembly
- Normalmente são 10~15% mais lentos que assembly escrito manualmente
- Esse número varia conforme o compilador, e defensores de intrinsics podem discordar
- Também há a opinião de que, por usar Hungarian Notation, o código fica difícil de ler
- Inline assembly ainda pode aparecer em alguns trechos antigos do FFmpeg ou em projetos como o Linux Kernel
- É a abordagem de escrever assembly diretamente dentro do código C, em vez de usar um arquivo separado
- Em projetos como o FFmpeg, prevalece a opinião de que isso dificulta a leitura, não tem suporte amplo dos compiladores e é difícil de manter
- A recomendação é ignorar, para fins de aprendizado, a ideia de que a vetorização automática do compilador já seria suficiente
- Em testes recentes do projeto dav1d, a vetorização automática mostrou ganho de cerca de 2x
- Versões escritas manualmente puderam chegar a 8x
Escopo da sintaxe e materiais de referência
- A aula foca em assembly x86 64-bit
- Também chamado de amd64, e funciona em CPUs Intel
- Há possibilidade de expansão futura para assembly de outras CPUs, como ARM e RISC-V
- A sintaxe de assembly x86 tem variantes AT&T e Intel
- A sintaxe AT&T é mais antiga e considerada mais difícil de ler do que a sintaxe Intel
- A aula usa sintaxe Intel
- Livros gerais ou materiais online como Stack Overflow podem não ser especialmente úteis como referência para assembly do FFmpeg
- Isso porque ele usa assembly em sintaxe Intel escrito à mão
- Muitos materiais online focam em programação de sistema operacional, programação de hardware ou código sem SIMD
- O assembly do FFmpeg segue uma abordagem própria, especializada em processamento de imagem de alto desempenho
- Os diagramas que visualizam instruções SIMD e seu funcionamento na parte final de The Art of 64-bit assembly podem ser úteis
- É disponibilizado um servidor no Discord para perguntas
Conceito básico de registradores
- Registradores são áreas dentro da CPU onde os dados são processados
- A CPU não opera diretamente sobre a memória; ela primeiro carrega os dados em registradores, processa e depois grava de volta na memória
- Em assembly, normalmente não é possível copiar dados diretamente de uma posição de memória para outra; é preciso passar primeiro por um registrador
Registradores de uso geral
- GPR (General Purpose Register) é um registrador de uso geral que pode armazenar dados ou endereços de memória
- Aqui ele pode armazenar valores de até 64-bit
- Também pode armazenar ponteiros
- Permite operações como adição, multiplicação e shift
- Muitos livros de assembly tratam longamente dos detalhes dos GPRs e de seu contexto histórico
- No código assembly do FFmpeg, os GPRs são usados principalmente como andaime (scaffolding), e grande parte dessa complexidade não é necessária ou fica abstraída
Registradores vetoriais e tamanho dos dados
- Registradores vetoriais armazenam vários elementos de dados
- Os principais registradores vetoriais do x86 são os seguintes
mm: registradores MMX, com tamanho de 64-bit, históricos e hoje pouco usados
xmm: registradores XMM, com tamanho de 128-bit, amplamente disponíveis
ymm: registradores YMM, com tamanho de 256-bit, cujo uso traz alguma complexidade
zmm: registradores ZMM, com tamanho de 512-bit, com uso mais limitado
- A maior parte dos cálculos em compressão e descompressão de vídeo é baseada em inteiros, então a aula também se concentra em inteiros
- Um registrador
xmm de 128-bit pode ser interpretado da seguinte forma
- 16 bytes, cada um com 8-bit
- 8 words, cada um com 16-bit
- 4 doublewords, cada um com 32-bit
- 2 quadwords, cada um com 64-bit
- As abreviações passam a ser importantes depois
- byte: dado de 8-bit
- word: dado de 16-bit
- doubleword: dado de 32-bit
- quadword: dado de 64-bit
- double quadword: dado de 128-bit
O papel de x86inc.asm
x86inc.asm é uma camada leve de abstração usada por FFmpeg, x264 e dav1d
- Ele fornece vários recursos para ajudar programadores de assembly a escrever código com mais facilidade
- Um dos recursos importantes no começo é atribuir rótulos como
r0, r1, r2 aos GPRs
- Assim, não é preciso decorar os nomes reais dos registradores
- Como os GPRs no FFmpeg servem principalmente como andaime, isso reduz o esforço de escrita
Exemplo simples de asm escalar
mov r0q, 3
inc r0q
dec r0q
imul r0q, 5
- A primeira linha armazena o valor imediato (immediate value)
3 no registrador r0 como quadword
- Valor imediato é um valor armazenado no próprio código assembly, e não carregado da memória
- Na sintaxe Intel, o operando-fonte à direita é transferido para o operando-destino à esquerda
- Dá para ler como
r0q = 3
- A ordem é parecida com o comportamento de
memcpy
- O sufixo
q em r0q indica que o registrador está sendo usado como quadword
- Depois disso, o comportamento é o seguinte
- Com
inc, o valor vira 4
- Com
dec, o valor volta para 3
- Com
imul, ele é multiplicado por 5, então r0q termina com 15
- Instruções legíveis por humanos como
mov e inc são chamadas de mnemonics
- O assembler as converte em código de máquina
MOV, INC em maiúsculas e mov, inc em minúsculas são equivalentes
- No FFmpeg, os mnemonics são escritos em minúsculas e as maiúsculas ficam reservadas para macros
Primeiro exemplo de função SIMD
%include "x86inc.asm"
SECTION .text
;static void add_values(uint8_t *src, const uint8_t * src2)
INIT_XMM sse2
cglobal add_values, 2, 2, 2, src, src2
movu m0, [srcq]
movu m1, [src2q]
paddb m0, m1
movu [srcq], m0
RET
- Essa função soma via SIMD os dados de
src e src2 e grava novamente o resultado no endereço de src
%include "x86inc.asm" inclui helpers, nomes predefinidos e macros desenvolvidos pelas comunidades de x264, FFmpeg e dav1d
SECTION .text indica a seção onde fica o código executável
- Dados constantes podem ficar na seção
.data
;static void add_values(uint8_t *src, const uint8_t * src2) é um comentário que mostra o formato dos argumentos da função em C
- Em assembly, o ponto e vírgula
; funciona como comentário, como // em C
INIT_XMM sse2 define o uso de registradores XMM e do conjunto de instruções sse2
- Isso porque
paddb é uma instrução sse2
cglobal add_values, 2, 2, 2, src, src2 define a função C add_values
- A função recebe 2 argumentos
- O número de GPRs usados pela função, incluindo os argumentos, é 2
- O número de registradores XMM usados é 2
- Os dois itens finais são os rótulos dos argumentos da função,
src e src2
- Em código mais antigo, pode-se usar diretamente GPRs como
r0 e r1 sem rótulos de argumento
load, packed add, store
movu m0, [srcq]
movu m1, [src2q]
movu é uma forma abreviada de movdqu, que significa move double quad unaligned
- O alinhamento (alignment) será tratado em aulas posteriores; aqui basta entender como uma instrução que move 128-bit a partir de
[srcq]
- Em
mov, os colchetes indicam que o endereço deve ser dereferenciado
- É um conceito parecido com
*src em C
- Essa operação é um load
- O sufixo
q indica o tamanho do ponteiro
- Em sistemas 64-bit, representa 8 bytes, que é o tamanho do ponteiro em C
- Em sistemas 32-bit, x86asm usa 32-bit
- O load real continua sendo de 128-bit
- Registradores vetoriais são referenciados com o nome abstraído
m0, em vez do nome completo como xmm0
- Isso se conecta a aulas futuras, em que o mesmo código pode se adaptar a vários tamanhos de registradores SIMD
paddb m0, m1
paddb soma entre si os elementos byte de cada registrador
- O prefixo
p significa packed e é usado para distinguir instruções vetoriais de instruções escalares
- O sufixo
b indica soma em unidades de byte
- Ao somar dois registradores com 16 bytes cada, os elementos correspondentes são somados posição por posição, como
a+q, b+r, c+s
movu [srcq], m0
RET
movu [srcq], m0 grava o dado resultante de volta no endereço apontado pelo ponteiro srcq
RET é uma macro que indica o retorno da função
- Quase todas as funções em assembly do FFmpeg modificam os dados recebidos por argumento em vez de retornar valores
- Nos exercícios, a continuação envolve criar function pointers para as funções assembly disponíveis e usá-los
1 comentários
Opiniões no Hacker News
Outro material sobre o mesmo tema: https://blogs.gnome.org/rbultje/2017/07/14/writing-x86-simd-...
Olhando os comentários aqui, a utilidade de SIMD escrito à mão varia de “totalmente incerta” a “essencial para a missão”, mas parece haver mais gente no lado do “totalmente incerta”, então vou explicar um pouco o lado essencial para a missão
O FFmpeg é um caso evidente por causa da frequência de uso, mas acho que dav1d, um decodificador AV1 de produção e uso geral, é um exemplo mais fácil para quantificar o impacto de SIMD escrito à mão
O dav1d é usado praticamente everywhere, dos principais navegadores ao sistema operacional Android, substituiu o libgav1, e uma grande razão de seu sucesso é sua velocidade enorme, possível porque uma parte considerável da base de código é SIMD escrito à mão
É bom que linguagens como Zig tenham suporte embutido a SIMD, mas, em alguns usos em que também vale investigar a possível diferença de desempenho, acaba sendo necessário escrever diretamente. Certas linhas de código do dav1d rodam trilhões de vezes por dia, então precisam ser tão rápidas quanto possível, e a diferença entre SIMD escrito à mão e SIMD gerado pelo compilador pode chegar a 50% em alguns casos, o que importa
Eu também estou envolvido, até certo ponto, em escrever código que será executado uma quantidade enorme de vezes, então acho que materiais como a escola de linguagem assembly do FFmpeg são bem importantes para que esse tipo de técnica não desapareça
A convenção de chamada pode até variar de método para método, o que resulta em muito menos gravações e leituras na pilha do que o código gerado por um compilador que segue a convenção de chamada normal da plataforma
Por exemplo, nossa nova multiplicação de matrizes é mais rápida que bibliotecas famosas para inferência de LLM e, em alguns casos, isso acontece mesmo quando o outro lado usa AMX e nós usamos AVX512BF16. O motivo talvez seja um gargalo de threading, ou talvez outra causa, mas quando há JIT envolvido fica difícil entender
Se tivéssemos precisado escrever kernels específicos por plataforma manualmente, esse resultado não teria acontecido. O tempo no dia é limitado e, graças a escrever uma única implementação com Highway, conseguimos explorar um espaço de projeto mais amplo, incluindo um autotuner que escolhe não só novos tipos de kernel e tamanhos de bloco, mas também estratégias de paralelização e seus parâmetros
Em uma segunda etapa, até dá para ajustar algumas partes à mão, mas espero que a exploração mais ampla venha antes de micro-otimizar alocação de registradores e convenções de chamada
Gosto de Zig, mas entendo que, na maioria dos casos, ainda é preciso usar intrinsics específicos de CPU, como em C/C++
GCC e Clang dão suporte ao atributo
vector_sizee à sobrecarga de operadores aritméticos desses tipos “vetorizados”, além de muito mais. Na prática, intrinsics como_mm256_mul_pstambém são implementados como#define _mm256_mul_ps(a,b) (__m256)((v8sf)(a) * (v8sf)(b))A utilidade desses recursos é muito maior do que o que é possível em Zig
No caso de encoders, muitas vezes é preciso “estruturar” o problema de modo a permitir eliminar loops e loads cedo, e compiladores não conseguem gerar esse tipo de código vetorizado automaticamente
Antigamente eu criava muitas versões SIMD de funções centrais, mas hoje quase não faço isso. Uma abordagem que vale tentar é isolar esse código e rodá-lo no excelente Compiler Explorer [0]
Depois é só examinar o código gerado
Hoje em dia, a vetorização automática muitas vezes gera versões SIMD muito boas das funções, e frequentemente basta dar “dicas” ao compilador. Por exemplo, explicitar o alinhamento ou fornecer diretamente os tipos de origem/destino dos vetores
Dá para fazer muita coisa ao “estilizar” o código C pensando no que o compilador pode fazer. Usar mais variáveis intermediárias e dividir a operação desejada em partes bem pequenas, por exemplo
Mesmo no pior caso, se o compilador não for inteligente o suficiente, dá para modificar com base no assembly gerado sem escrever manualmente todo o boilerplate
Na maioria dos casos, a função C resultante é vetorizada tão bem quanto, ou melhor que, a que eu teria escrito à mão; e, em muitos outros casos, fica “perto o suficiente” para não fazer grande diferença. Além disso, esse código tem grande chance de também ser bem vetorizado para WASM, NEON etc., sem versões explícitas
[0] https://godbolt.org/
Tanto GCC quanto Clang dão suporte a algumas extensões vetoriais, então é possível implementar de forma razoavelmente portável coisas como loads/stores dispersos, shuffles e mascaramento de elementos em um único registrador — coisas que são difíceis de expressar em “C puro” de modo legível para humanos e que sempre produzam claramente o código esperado entre versões do compilador
Mas, como também precisávamos dar suporte a outros compiladores e plataformas, nos builds reais acabávamos usando o assembly gerado a partir desse arquivo-fonte
Isso é adição saturada de bytes sem sinal. Tanto em x86-64 quanto em ARM64 há suporte direto com uma única instrução, PADDUSB e UQADD.16B
Mas todos os compiladores se atrapalham com a descrição intuitiva, falhando em vetorizar ou gerando código vetorizado maior e mais lento do que o necessário
E isso apesar de ser uma primitiva de vetorização básica e simples. Fazer o compilador usar instruções mais complexas, como deslocamento à direita saturado com estreitamento e arredondamento (UQRSHRN), é difícil ou quase impossível
Normalmente é mais rápido simplesmente escrever direto
pshufbmais largo que 16 bytes no caso geralMesmo no caso de largura de 16 bytes, se você usar a definição real de
pshufb, não obtémpshufb; se usar uma versão com comportamento indefinido, aí saipshufbSou o autor desta aula
Podem perguntar qualquer coisa
Com tanto assembly, parece um milagre que o FFmpeg funcione no meu Mac. Foi portado manualmente?
Tenho curiosidade sobre em que momento você percebe “isso poderia ficar mais rápido em assembly”. Se encontrou uma função que realmente ficaria rápida em assembly, como a escreve? Pega a saída que o compilador transformou em assembly e começa daí, ou escreve do zero? Isso sequer importa?
Como a usabilidade para desenvolvedores e o desempenho do código se comparam ao SIMD tradicional? Estamos indo para um mundo com menos tipos de conjuntos de instruções SIMD para programar?
x86asm.inccuida disso ou se simplesmente não se preocupam com issoNão daria para simplesmente escrever os testes e deixar um LLM tentar 10.000 algoritmos e depois perfilar os resultados?
Ou, mesmo dando 10.000 seeds aleatórios, ainda seria difícil para um LLM encontrar o ótimo?
Pergunto só por curiosidade. Otimizar x86 à mão não é fácil. Para pensar nisso, é preciso encaixar todos os registradores na cabeça e avaliar as combinações, além de saber quanto tempo cada combinação de instruções vai levar. E algumas instruções têm casos especiais estranhos, difíceis para uma pessoa considerar, em que podem levar muito mais tempo ou ficar muito mais rápidas
Tenho curiosidade sobre quem já fez isso na prática. Ao aprender ou implementar assembly, há algum prazer como em LISP ou RISC-V, ou é algo que se aprende para fazer outra coisa, como aprender COBOL por causa de algum trabalho específico em sistemas?
Sempre tive interesse, mas no trabalho do dia a dia não tenho exatamente um motivo para me aprofundar. Fico pensando se vale investir tempo por diversão
Agora eu até gosto bastante de programar em assembly. Depois do tutorial, não fiz muita coisa além de criar uma biblioteca de arrays que pode ser chamada a partir de C
Acho divertido porque, nesse nível, sobra muito pouca mágica. Você diz exatamente o que precisa acontecer, e o que se vê geralmente é o que sai como resultado
Também passei a entender linking muito melhor, e isso ajuda a compreender coisas que eu conhecia em alto nível, mas cujos detalhes eram nebulosos
Agora também quero conferir este tutorial de FFmpeg. Este é x86, não ARM
Existem modelos teóricos mais simples, como máquinas de Turing ou cálculo lambda, mas as arquiteturas com que programadores lidam de fato têm um certo grau de generosidade
Não é algo para temer. Assembly é mais verboso do que complexo. Para cada coisa que você faz, há cargas e armazenamentos, cargas e armazenamentos, repetidos inúmeras vezes
Se você acrescentar alguns macros e verificações em tempo de build, ou colocar isso no contexto de um sistema Forth com um interpretador que “executa blocos de assembly”, permitindo desenvolvimento interativo e scripting, ele não fica tão distante de C e remove a mágica do compilador
Também recomendo uma abordagem retrô. Uma máquina de 8 bits em um emulador mantém o modelo de trabalho dentro de um espaço pequeno e bem documentado, e as restrições tornam valioso pensar em fazer mais coisas em assembly. Em arquiteturas pós-32 bits, quando há recursos em abundância, isso não é tão comum
Quem desenvolve em assembly profissionalmente provavelmente terá preferências mais específicas, mas, para iniciantes, o mais importante é um ambiente com boa documentação e bons exemplos. O Rosetta Code tem bons exemplos de assembly que servem para estudo
Lembro de uma disciplina na faculdade em que competíamos para ver quem criava o programa em assembly mais performático para uma determinada tarefa. Todo mundo tentava várias variações de loop unrolling para extrair o máximo desempenho e evitar predições de desvio ruins
Não sei se atingi o Ballmer Peak na noite anterior à entrega, mas tentei uma configuração que quase todos os outros deixaram passar e venci por uma margem mínima
Também há o enorme prazer de olhar para https://github.com/chrislgarry/Apollo-11 e poder brincar: “isso é um sistema Unix, eu conheço isso!”. A maravilha de conseguir ler a linguagem que levou a humanidade à Lua não desaparece
Resposta curta: sim
O momento em que você entende como transistores, portas lógicas, arquitetura de CPU e programação de alto nível se encaixam vale o esforço, mesmo que você nunca use assembly profissionalmente
Às vezes é útil, mas há um grande prazer em colocar até o último byte no lugar certo, explorar binários que ninguém examinou por décadas ou criar emuladores que antes seriam impossíveis
É uma das poucas áreas em que ainda sinto a mágica como quando comecei
Pessoalmente, não vejo grande valor em escrever assembly diretamente em vez de usar intrinsics, mas ler assembly foi realmente útil
Uso bastante o Compiler Explorer(https://godbolt.org/) para ver o assembly gerado e entender as otimizações que o compilador faz ao otimizar desempenho
“É para acelerar o processamento multimídia. Ao escrever código assembly, é muito comum obter melhorias de velocidade de mais de 10 vezes, o que é especialmente importante ao tentar reproduzir vídeo em tempo real sem engasgos”
Gostei de ver a referência ao K&R. Foi o livro que comprei para aprender C e programação em geral
No começo tentei C++ como primeira linguagem, mas eu ficava querendo saber o que acontecia por dentro, então ela parecia abstrata demais e difícil de aprender
Este material é perfeito. Eu conhecia assembly x86 da época do 386, mas em processadores mais avançados ficou complexo demais
Eu queria aprender mais sobre SIMD em CPUs modernas, e isto parece um excelente material
A frase “observe que o sufixo
qse refere ao tamanho de ponteiro *(*ou seja, em C, em um sistema de 64 bits, significa *sizeof(*src) == 8, e x86asm é esperto o suficiente para usar 32 bits em um sistema de 32 bits), mas o carregamento real é de 128 bits” é confusaAcho que
i.edeveria seri.e.,, e o que significa*(*? Não deveria ser apenas um parêntese de abertura?Também não vejo em que contexto
*sizeof(*src)poderia ser válido. Até onde sei,sizeofnão produz um ponteiroParece que a frase recebeu asteriscos aleatórios, ou que tentaram misturar asteriscos de itálico com sintaxe de C e deu errado
Não me importo com a forma como foi dividido; só queria dizer que este guia é muito bom
Eu gostaria que existisse algo assim quando eu me interessava por coisas de nível muito baixo
Assembly é 10 vezes mais rápida que C? Em algum momento isso certamente deve ter sido verdade, mas ainda é assim hoje? Os compiladores realmente ficaram tão estagnados a ponto de nem chegarem perto de assembly escrito à mão?
Os desenvolvedores do FFmpeg são um tanto notoriamente contrários a intrínsecos e, se me lembro bem, não os aceitam na base de código mesmo que tenham desempenho tão bom quanto o assembly equivalente. Ainda assim, pela própria estimativa do texto, a diferença entre intrínsecos e assembly fica em torno de 10% a 15%
Se você comparar assembly cuidadosamente otimizado com C ingênuo, é possível ver uma diferença de 10 vezes nos casos em que a vetorização é possível, mas o compilador não consegue aproveitá-la. Isso é comum: a autovetorização ainda costuma ser bem ruim além dos casos triviais
Dito isso, não é surpresa que código escrito por especialistas supere muito código ingênuo
Dito de forma mais simples, um compilador C não consegue inferir, olhando apenas para uma implementação comum em C, que a intenção é expressar determinada matemática com intrínsecos SIMD mais eficientes. Isso porque ele não tem acesso à intenção matemática do autor
Também há considerações específicas de cada alvo. Um compilador é, inevitavelmente, um compilador de propósito geral. Problemas de recursos, como alocação de registradores, são NP-completos e equivalentes ao problema da mochila. Quase ninguém quer que o compilador passe horas tentando encontrar a geração de assembly absolutamente ótima. Se é possível saber estaticamente essa otimalidade é outra questão
Compiladores são inteligentes em código comum, mas codecs não são código comum. Não sou programador do FFmpeg, mas tenho experiência trabalhando com áudio
Em problemas aos quais SIMD se aplica, é razoável esperar algo de 2 a 16 vezes mais rápido do que uma implementação escalar ingênua
Eu não conseguiria escrever assembly 10 vezes melhor que C, mas não presumiria que ninguém consiga