diff --git a/README.fr.md b/README.fr.md index b9b1fa8..4a02254 100644 --- a/README.fr.md +++ b/README.fr.md @@ -15,4 +15,5 @@ https://discord.com/invite/Ks5MhUhqfB **Traductions** * [English](./README.md) -* [Spanish](./README.es.md) \ No newline at end of file +* [Português](./README.pt-br.md) +* [Spanish](./README.es.md) diff --git a/README.md b/README.md index 67c4007..b26dbf4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Welcome to the FFmpeg School of Assembly Language. You have taken the first step **Required Knowledge** -* Knowledge of C, in particular pointers. If you don't know C, work through [The C Programming Language](https://en.wikipedia.org/wiki/The_C_Programming_Language) book +* Knowledge of C, in particular pointers. If you don't know C, work through [The C Programming Language](https://en.wikipedia.org/wiki/The_C_Programming_Language) book * High School Mathematics (scalar vs vector, addition, multiplication etc) **Lessons** @@ -15,4 +15,5 @@ https://discord.com/invite/Ks5MhUhqfB **Translations** * [Français](./README.fr.md) +* [Português](./README.pt-br.md) * [Spanish](./README.es.md) \ No newline at end of file diff --git a/README.pt-br.md b/README.pt-br.md new file mode 100644 index 0000000..e383509 --- /dev/null +++ b/README.pt-br.md @@ -0,0 +1,19 @@ +Bem-vindo à Escola de Linguagem Assembly do FFmpeg. Você deu o primeiro passo na jornada mais interessante, desafiadora e gratificante da programação. Estas lições fornecerão os fundamentos sobre como assembly é usado no FFmpeg e abrirão seus olhos para o que realmente acontece dentro do seu computador... + +**Conhecimentos Necessários** + +* Conhecimento em C, especialmente ponteiros. Se você não está familiarizado com C, você encontrará todos os fundamentos no livro [The C Programming Language](https://en.wikipedia.org/wiki/The_C_Programming_Language). +* Matemática do ensino médio (escalar vs vetor, adição, multiplicação, etc...) + +**Lições** + +Neste repositório Git estão as lições e exercícios (ainda não incluídos) relacionados a cada lição. Ao final de todas as lições, você será capaz de contribuir para o FFmpeg. + +Um servidor Discord está disponível para responder suas perguntas: +https://discord.com/invite/Ks5MhUhqfB + +**Traduções** + +* [English](./README.md) +* [Français](./README.fr.md) +* [Spanish](./README.es.md) diff --git a/lesson_01/index.pt-br.md b/lesson_01/index.pt-br.md new file mode 100644 index 0000000..218c359 --- /dev/null +++ b/lesson_01/index.pt-br.md @@ -0,0 +1,218 @@ +**Lição Um de Linguagem Assembly do FFmpeg** + +**Introdução** + +Bem-vindo à Escola de Linguagem Assembly do FFmpeg. Você deu o primeiro passo na jornada mais interessante, desafiadora e gratificante da programação. Estas lições fornecerão os fundamentos sobre como assembly é usado no FFmpeg e abrirão seus olhos para o que realmente acontece dentro do seu computador... + +**Conhecimentos Necessários** + +* Conhecimento em C, especialmente ponteiros. Se você não está familiarizado com C, você encontrará todos os fundamentos no livro [The C Programming Language](https://en.wikipedia.org/wiki/The_C_Programming_Language). +* Matemática do ensino médio (escalar vs vetor, adição, multiplicação, etc...) + +**O que é Assembly?** + +Assembly é uma linguagem de programação onde o código que você escreve corresponde diretamente a instruções compreensíveis por uma CPU. O assembly legível por humanos é, como o nome indica, *montado* em dados binários, conhecido como *linguagem de máquina*, que a CPU pode entender. O código assembly é frequentemente chamado de "assembly" ou "asm" abreviadamente. + +A grande maioria do assembly no FFmpeg é o que chamamos de *SIMD, Single Instruction Multiple Data (instrução única, múltiplos dados)*. SIMD às vezes é referido como programação vetorial. Isso significa que uma instrução particular opera em múltiplos elementos de dados simultaneamente. A maioria das linguagens de programação processa um único elemento de dados por vez, o que é chamado de programação escalar. + +Como você pode ter adivinhado, SIMD se presta bem ao processamento de imagens, vídeos e áudio, que contêm uma grande quantidade de dados organizados sequencialmente na memória. Instruções especializadas no processador nos ajudarão a processar esses dados sequenciais. + +No FFmpeg, você verá que os termos `função assembly`, `SIMD`, `vetorização` são usados de forma intercambiável. Todos eles se referem à mesma coisa: escrever uma função em assembly manualmente para processar múltiplos elementos de dados de uma só vez. Alguns projetos também podem se referir a `kernels assembly`. + +Tudo isso pode parecer complicado, mas é importante lembrar que estudantes do ensino médio escreveram assembly no FFmpeg. Como em qualquer lugar, aprender é 50% jargão e 50% aprendizado real. + +**Por que escrevemos em Assembly?** + +Para tornar o processamento multimídia rápido. É muito comum ter uma velocidade de processamento pelo menos 10 vezes mais rápida ao escrever em assembly, o que é ainda mais importante quando queremos reproduzir vídeos em tempo real sem travamentos. Isso também economiza energia e estende a vida útil das baterias. É importante destacar que as funções de codificação e decodificação estão entre as funções mais usadas no mundo, tanto por usuários finais quanto por multinacionais em seus data centers. Portanto, mesmo uma pequena melhoria traz muito benefício. + +Você verá frequentemente, online, pessoas usando *funções intrínsecas*, funções que se parecem com C mas que são na verdade instruções assembly usadas para permitir desenvolvimento mais rápido. No FFmpeg, não usamos esse tipo de função, escrevemos todo o código assembly manualmente. Este é um ponto de discórdia, mas as funções intrínsecas são cerca de 10 a 15% mais lentas que o equivalente em assembly escrito manualmente (os defensores dessas funções discordariam), tudo depende do compilador. Para o FFmpeg, cada melhoria conta, por isso escrevemos todo o código diretamente em assembly. Um argumento a nosso favor é o uso da `[notação húngara](https://pt.wikipedia.org/wiki/Notação_húngara)` nas funções intrínsecas que complicam sua leitura. + +Além disso, você verá referências a *assembly inline*, ou seja, não usando funções intrínsecas, em alguns lugares no FFmpeg por razões históricas, ou em projetos como o kernel Linux em cenários de uso muito específicos. Aqui, o código assembly não está em um arquivo separado, mas escrito diretamente em arquivos com código C. A visão majoritária em projetos como o FFmpeg é que esse código é difícil de ler, não amplamente suportado por compiladores e difícil de manter. + +Finalmente, você verá muitos especialistas autoproclamados na internet dizendo que nada disso é necessário e que o compilador pode fazer essa `vetorização` para você. Para fins de aprendizado, ignore-os: testes recentes como os presentes no [projeto dav1d](https://www.videolan.org/projects/dav1d.html) mostraram um ganho de velocidade de cerca de 2x graças a essa vetorização automática, enquanto para versões escritas manualmente, esse ganho subiu para 8x. + +**As Variedades de Assembly** + +Estas lições se concentrarão no assembly x86 de 64 bits. Também conhecido como amd64, embora continue funcionando em CPUs Intel. Existem tantos tipos de assembly quanto CPUs, como aqueles para ARM ou RISC-V, com possíveis atualizações destes cursos para incluí-los. + +Existem dois tipos de sintaxes para assembly x86 que você encontrará online: AT&T e Intel. A primeira é mais antiga e mais difícil de ler comparada à segunda. Nós nos concentraremos nesta última. + +**Materiais de Suporte** + +Você pode ficar surpreso ao saber que livros ou recursos online como Stack Overflow não são de grande ajuda como referências. Particularmente devido à nossa escolha de usar assembly escrito manualmente com sintaxe Intel. Mas também porque muitos desses recursos online se concentram em programação de sistemas operacionais ou programação para hardware, não usando código SIMD. O assembly do FFmpeg é principalmente focado no processamento de imagens de alto desempenho, e como você verá, com uma abordagem particular à programação assembly. Dito isso, é fácil entender outros casos de uso do assembly uma vez que essas lições sejam concluídas. + +Muitos livros detalham muitas arquiteturas de computador diferentes antes de detalhar o assembly. Do nosso ponto de vista, isso é ótimo se é isso que você quer aprender, mas é como querer estudar motores antes de aprender a dirigir um carro. + +Dito isso, nas partes seguintes, os diagramas do livro `The Art of 64-bit assembly` mostrando instruções SIMD e seu comportamento em forma visual serão muito úteis: [https://artofasm.randallhyde.com/](https://artofasm.randallhyde.com/) + +Um servidor Discord está disponível para responder suas perguntas: +[https://discord.com/invite/Ks5MhUhqfB](https://discord.com/invite/Ks5MhUhqfB) + +**Os Registradores** + +Registradores são áreas da CPU onde os dados podem ser processados. As CPUs não intervêm diretamente na memória, os dados são carregados em registradores, processados e depois escritos novamente na memória. Em assembly, de maneira geral, você não pode copiar diretamente os dados de um local de memória para outro sem primeiro passar esses dados por um registrador. + +**Registradores de Uso Geral** + +O primeiro tipo de registrador que encontraremos é conhecido como Registrador de Uso Geral (GPR). Os GPRs são chamados assim porque podem conter tanto dados, um valor de até 64 bits, quanto um endereço de memória (um ponteiro). Um valor em um GPR pode ser processado por operações como adição, multiplicação, deslocamento, etc... + +Na maioria dos livros sobre assembly, muitos capítulos inteiros são dedicados às sutilezas dos GPRs, sua história, etc... Pois os GPRs desempenharam um papel importante na programação de sistemas operacionais, engenharia reversa, etc... No assembly escrito para o FFmpeg, os GPRs são considerados como andaimes e na maioria das vezes, suas complexidades não são necessárias e são abstraídas. + +**Registradores Vetoriais** +Os registradores vetoriais (SIMD), como o nome sugere, contêm múltiplos elementos de dados. Existem diferentes tipos de registradores vetoriais: + +* registradores `mm`: registradores `MMX`, de tamanho 64 bits, históricos e pouco usados hoje em dia +* registradores `xmm`: registradores `XMM`, de tamanho 128 bits, amplamente disponíveis +* registradores `ymm`: registradores `YMM`, de tamanho 256 bits, com algumas complicações ao usá-los +* registradores `zmm`: registradores `ZMM`, de tamanho 512 bits, muito pouco disponíveis + +A maioria dos cálculos de compressão e descompressão de vídeo são baseados em inteiros, nos limitaremos a eles. Aqui está um exemplo de um registrador `XMM` de 16 bytes: + +| a | b | c | d | e | f | g | h | i | j | k | l | m | n | o | p | +| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | + +Isso também poderia ser oito palavras (inteiros de 16 bits): +| a | b | c | d | e | f | g | h | +| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | + +Ou quatro palavras duplas (inteiros de 32 bits): +| a | b | c | d | +| :---- | :---- | :---- | :---- | + + +Ou duas palavras quádruplas (inteiros de 64 bits): + +| a | b | +| :---- | :---- | + + +Para recapitular: +* **b**ytes - dados de 8 bits +* **w**ords - dados de 16 bits +* **d**oublewords - dados de 32 bits +* **q**uadwords - dados de 64 bits +* **d**ouble **q**uadwords - dados de 128 bits. + +Os caracteres em negrito serão importantes na sequência. + +**Header x86inc.asm** + +Em muitos exemplos, incluímos o arquivo x86inc.asm. x86inc.asm é uma camada de abstração leve usada no FFmpeg, x264 e dav1d para facilitar a vida dos desenvolvedores. Ela ajuda de muitas maneiras, mas uma das coisas úteis que permite é, entre outras, rotular os GPRs `r0`, `r1` e `r2`. Você não precisará se lembrar dos nomes dos registradores. Como mencionado anteriormente, os GPRs são geralmente apenas andaimes, então isso torna as coisas muito mais simples. + +**Um Trecho Simples de Assembly Escalar** + +Vamos olhar um trecho simples (e totalmente artificial) de código assembly escalar (código assembly que opera em elementos de dados individuais, um por vez, em cada instrução) para ver o que acontece: + +```assembly +mov r0q, 3 +inc r0q +dec r0q +imul r0q, 5 +``` + +Na primeira linha, o *valor imediato* 3 (um valor armazenado diretamente no código assembly em si, ao contrário de um valor recuperado da memória) é armazenado no registrador `r0` como quadword. Note que na sintaxe Intel, o operando fonte (o valor ou localização fornecendo os dados, localizado à direita) é transferido para o operando de destino (a localização recebendo os dados, localizado à esquerda), um pouco como o comportamento de *memcpy*. Você também pode ler como `r0q = 3`, já que a ordem é a mesma. O sufixo `q` de `r0` designa o registrador como sendo usado como quadword. `inc` incrementa o valor para que `r0q` contenha 4, `dec` decrementa o valor para voltar a 3. `imul` multiplica o valor por 5. No final, `r0q` contém 15. + +Note que as instruções legíveis por humanos, como mov e inc, que são montadas em código de máquina pelo assembler, são chamadas *mnemônicos*. Você pode ver online e em livros mnemônicos representados com letras maiúsculas como `MOV` e `INC`, mas eles são os mesmos que as versões em minúsculas. No FFmpeg, usamos mnemônicos em minúsculas e reservamos maiúsculas para macros. + +**Entendendo uma Função Vetorial Básica** + +Aqui está nossa primeira função SIMD: + +```assembly +%include "x86inc.asm" + +SECTION .text + +;static void add_values(const 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 +``` + +Vamos revisar o código linha por linha: + +```assembly +%include "x86inc.asm" +``` + +Este `header` desenvolvido nas comunidades x264, FFmpeg e dav1d para fornecer auxiliares, nomes predefinidos e macros (como `cglobal` abaixo) para simplificar a escrita do código assembly. + +```assembly +SECTION .text +``` +Isso designa a seção onde o código que você quer executar é colocado. Isso contrasta com a seção `.data`, onde você pode colocar dados constantes. + +```assembly +;static void add_values(const uint8_t *src, const uint8_t *src2); +INIT_XMM sse2 +``` + +A primeira linha é um comentário (o ponto e vírgula `;` em asm é o equivalente ao `//` em C) mostrando como é o protótipo da função em C. A segunda linha mostra como inicializamos a função para que ela use um registrador XMM, usando o conjunto de instruções sse2. Isso é necessário porque a instrução paddb faz parte do sse2. Abordaremos o sse2 com mais detalhes na próxima lição. + +```assembly +cglobal add_values, 2, 2, 2, src, src2 +``` + +O código anterior mostra uma linha importante: a definição de uma função em C chamada `add_values`. + +Vamos revisar cada elemento da linha um por um: + +* O parâmetro seguinte indica que a função recebe dois argumentos (o primeiro 2). +* Em seguida, o segundo 2 indica que vamos usar dois GPRs para os argumentos. Em alguns casos, poderíamos querer usar mais GPRs, então precisaríamos indicar ao x86util que precisaremos de mais. +* O parâmetro seguinte indica ao x86util quantos registradores `XMM` vamos usar. +* Os dois últimos parâmetros são os rótulos usados para os argumentos da função `add_values`. + +É importante notar que o código mais antigo pode não ter os rótulos como argumentos de funções, mas sim os endereços dos registradores GPR no lugar, usando `r0`, `r1`, etc... + +```assembly + movu m0, [srcq] + movu m1, [src2q] +``` + +`movu` é uma abreviação de `movdqu` (*move double quad unaligned*). O alinhamento será abordado em outra lição, mas por enquanto, você pode considerar movu como uma transferência de 128 bits de [srcq]. No caso de mov, os colchetes `[]` significam que o endereço contido em [srcq] é desreferenciado, o que equivale a **src* em C. Isso é chamado de carregamento (load). + +O sufixo `q` se refere ao tamanho do ponteiro. Em C, isso corresponde a sizeof(*src) == 8 em sistemas de 64 bits. O assembler x86 é inteligente o suficiente para usar 32 bits em sistemas de 32 bits, mas a operação de carregamento subjacente permanece de 128 bits. + +Note que não fazemos referência aos registradores vetoriais por seu nome completo, neste caso `xmm0`, mas sim em uma forma abstrata como `m0`. Nas próximas lições, você verá como essa abordagem permite escrever código uma única vez e fazê-lo funcionar com múltiplos tamanhos de registradores SIMD. + +```assembly +paddb m0, m1 +``` + +`paddb` (leia isso *p-add-b*) adiciona cada byte em cada registrador, como ilustrado abaixo. O prefixo `p` significa `packed` e serve para distinguir instruções vetoriais de instruções escalares. O sufixo `b` indica que a operação é realizada no nível de bytes (adição de bytes). + +| a | b | c | d | e | f | g | h | i | j | k | l | m | n | o | p | +| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | + +\+ + +| q | r | s | t | u | v | w | x | y | z | aa | ab | ac | ad | ae | af | +| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | + +\= + +| a+q | b+r | c+s | d+t | e+u | f+v | g+w | h+x | i+y | j+z | k+aa | l+ab | m+ac | n+ad | o+ae | p+af | +| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | + +```assembly +movu [srcq], m0 +``` + +Isso é chamado de armazenamento (*store*). Os dados são escritos na memória no endereço apontado por `srcq`. + +```assembly +RET +``` + +`RET` é uma macro indicando que a função termina. Quase todas as funções assembly no FFmpeg modificam os dados passados como argumento em vez de retornar um valor. + +Como você verá no exercício, criaremos ponteiros de função para funções assembly e os usaremos quando possível. + +[Próxima lição](../lesson_02/index.pt-br.md) \ No newline at end of file diff --git a/lesson_02/index.pt-br.md b/lesson_02/index.pt-br.md new file mode 100644 index 0000000..1866d7b --- /dev/null +++ b/lesson_02/index.pt-br.md @@ -0,0 +1,173 @@ +**Lição Dois de Linguagem Assembly do FFmpeg** + +Agora que você escreveu sua primeira função em assembly, vamos apresentar desvios (*branch*) e laços (*loop*). + +Primeiro, precisamos introduzir as noções de **labels** e **jumps** (saltos). Vejamos o seguinte exemplo: + +```assembly +mov r0q, 3 +.loop: + dec r0q + jmp .loop +``` + +A instrução `jmp` move a execução do código após `.loop:`. `.loop:` é o que chamamos de **label**. O **ponto** (.) prefixando o label indica que este label é um **label local**. Isso significa que você pode reutilizar o mesmo nome de label em várias funções sem provocar conflitos. + +Esta porção de código é um exemplo de laço infinito, vamos melhorá-lo nas próximas lições para ter um exemplo muito mais realista. + +Antes de fazer um laço realista, precisamos introduzir o registrador **FLAGS**. Não vamos entrar nos detalhes complexos dos **FLAGS** (pois as operações nos **GPRs** são principalmente andaimes), mas saiba que existem várias *flags* como a **Zero Flag** (**Z** ou **ZF**, para *indicador de zero*), a **Sign Flag** (**SF**, para *indicador de sinal*), e a **Overflow Flag** (**OF**, para *indicador de estouro*) que são um conjunto de operações baseado na saída da maioria das instruções (exceto instruções do tipo `mov`) em dados escalares como operações aritméticas e deslocamentos (*shifts*). + +Aqui está um exemplo onde o contador `r0q` do laço decrementa até zero e onde `jg` (*jump if greater than zero*) é usado como condição de parada. A instrução `dec r0q` atualiza os **FLAGS** com base nos valores de `r0q` após cada decremento e você *volta ao laço* até que `r0q` atinja zero, momento em que o laço para. + +```assembly +mov r0q, 3 +.loop: + ; fazer algo + dec r0q + jg .loop ; saltar se maior que zero +``` + +O equivalente em **C** é o seguinte: + +```c +int i = 3; +do +{ + // fazer algo + i--; +} while(i > 0) +``` + +Este código em **C** é um pouco contraintuitivo. Normalmente, um laço em **C** é escrito desta maneira: + +```c +int i; +for(i = 0; i < 3; i++) { + // fazer algo +} +``` + +Os dois exemplos são quase equivalentes à porção em assembly seguinte (não há maneira simples de encontrar o equivalente a este laço ```for```): + +```assembly +xor r0q, r0q +.loop: + inc r0q + ; fazer algo + cmp r0q, 3 + jl .loop ; saltar se (r0q - 3) < 0, i.e (r0q < 3) +``` + +Várias coisas devem ser examinadas neste trecho de código. Primeiro, ```xor r0q, r0q``` é uma maneira simples de atribuir zero a um registrador, o que é mais rápido em alguns sistemas que ```mov r0q, 0```, porque não há nenhum carregamento de registradores. +Isso também pode ser usado com registradores **SIMD** com a linha ```pxor m0, m0``` para zerar um registrador inteiro. A próxima coisa a notar é o uso de ```cmp```. ```cmp``` pode efetivamente subtrair o segundo registrador do primeiro (sem ter que salvar o valor em algum lugar) e atualiza as *FLAGS*, mas como indica o comentário, isso pode ser lido com a instrução de salto ```jl``` (salto se inferior a zero) para efetuar um salto se ```r0q < 3```. + +Note a presença de uma instrução adicional (````cmp```) neste curto trecho. Geralmente, poucas instruções equivalem a código mais rápido, por isso o exemplo anterior é preferido. Como você verá mais tarde, existem várias técnicas para evitar instruções adicionais e fazer com que as *FLAGS* sejam definidas por uma operação aritmética ou outra operação. Note que não escrevemos assembly para corresponder exatamente aos laços em C, escrevemos laços em assembly para torná-los os mais rápidos em assembly. + +Aqui estão alguns mnemônicos de salto que você acabará usando (os registradores *FLAGS* são apresentados aqui para cobrir tudo, mas você não precisará conhecê-los para escrever laços): + +| Mnemônico | Significado | Descrição | FLAGS | +| :---- | :---- | :---- | :---- | +| JE/JZ | Jump if Equal/Zero | Salto se Igual / Zero | ZF = 1 | +| JNE/JNZ | Jump if Not Equal/Not Zero | Salto se Não Igual / Não Zero | ZF = 0 | +| JG/JNLE | Jump if Greater/Not Less or Equal (signed) | Salto se Maior / Não Menor ou Igual (com sinal) | ZF = 0 e SF = OF | +| JGE/JNL | Jump if Greater or Equal/Not Less (signed) | Salto se Maior ou Igual / Não Menor (com sinal) | SF = OF | +| JL/JNGE | Jump if Less/Not Greater or Equal (signed) | Salto se Menor / Não Maior ou Igual (com sinal) | SF ≠ OF | +| JLE/JNG | Jump if Less or Equal/Not Greater (signed) | Salto se Menor ou Igual / Não Maior (com sinal) | ZF = 1 ou SF ≠ OF | + +**Constantes** + +Vamos mergulhar em alguns exemplos sobre o uso de constantes: + +```assembly +SECTION_RODATA + +constants_1: db 1,2,3,4 +constants_2: times 2 dw 4,3,2,1 +``` + +* SECTION_RODATA indica que esta é uma seção somente leitura. (É uma macro porque os diferentes formatos de arquivo de saída usados pelos sistemas operacionais os declaram de forma diferente.) +* o label *constants_1* é definido como sendo um ```db``` (*declared bytes*), é equivalente a uint8_t constants_1[4] = {1, 2, 3, 4}; +* constants_2 usa a macro ```times 2``` para repetir a definição de palavra (```dw``` para *declared word*), é equivalente a uint16_t constants_2[8] = {4, 3, 2, 1, 4, 3, 2, 1}; + +Esses labels, que o assembler converte em endereços de memória, podem ser usados para carregamentos (mas não para armazenamentos, pois são somente leitura). Algumas instruções aceitam um endereço de memória como operando, o que permite usá-los sem carregamento explícito em um registrador (há vantagens e desvantagens nisso). + +**Offsets (deslocamentos)** + +Os offsets (*deslocamentos*) são as distâncias (em bytes) entre dois elementos consecutivos na memória. O offset é determinado pelo **tamanho de cada elemento** na estrutura de dados. + +Agora que somos capazes de escrever laços, é hora de recuperar dados. Mas existem diferenças em relação ao C. Vejamos o seguinte laço em C: + +```c +uint32_t data[3]; +int i; +for(i = 0; i < 3; i++) { + data[i]; +} +``` + +O offset de 4 bytes entre cada elemento de dados é pré-calculado pelo compilador. Mas ao escrever manualmente em assembly, você precisará calcular esse offset você mesmo. + +Vejamos a sintaxe do cálculo de endereços de memória. Isso se aplica a todos os tipos de endereços de memória: + +```assembly +[base + scale*index + disp] +``` + +* base - Este é um GPR (geralmente um ponteiro vindo de um argumento de uma função em C). +* scale - Inteiro valendo 1, 2, 4, 8. O valor padrão é 1. +* index - um registrador GPR (o contador de um laço). +* disp - Inteiro (armazenado até no máximo 32 bits). disp é um deslocamento nos dados. + +x86asm fornece a constante **mmsize**, que indica o tamanho do registrador SIMD com o qual você está trabalhando. + +Aqui está um exemplo simples (e não muito lógico) de ilustração do carregamento com diferentes deslocamentos: + +```assembly +;static void simple_loop(const uint8_t *src) +INIT_XMM sse2 +cglobal simple_loop, 1, 2, 2, src + movq r1q, 3 +.loop: + movu m0, [srcq] + movu m1, [srcq+2*r1q+3+mmsize] + + ; fazer algumas coisas + + add srcq, mmsize +dec r1q +jg .loop + +RET +``` + +Note como em ```movu m1, [srcq+2*r1q+3+mmsize]``` o assembler pré-calculará o deslocamento correto a usar. Na próxima lição, apresentaremos uma técnica para evitar ter ```add``` e ```dec``` no laço, substituindo-os por um único ```add```. + +**LEA** + +Agora que você domina os offsets, você é capaz de usar a instrução ```lea``` (_**L**oad **E**ffective **A**ddress*_). Com uma única instrução, você é capaz de fazer uma multiplicação e uma adição, o que é então mais rápido que o uso de várias instruções. Há, claro, limitações sobre os dados que você pode multiplicar e adicionar, mas isso não impede ```lea``` de ser uma instrução poderosa. + +```assembly +lea r0q, [base + scale*index + disp] +``` + +Ao contrário de seu nome, **LEA** pode ser usada tanto para operações aritméticas quanto para cálculos de endereços de memória. Você pode fazer algo tão complicado quanto: + +```assembly +lea r0q, [r1q + 8*r2q + 5] +``` + +É importante notar que isso não afeta o conteúdo de ````r1q``` e ```r2q```. Isso também não impacta os registradores *FLAGS* (portanto, você não pode efetuar saltos na saída). O uso de **LEA** evita todas essas instruções e os registradores temporários (este código não tem equivalente porque ```add``` modifica as *FLAGS*): + +```assembly +movq r0q, r1q +movq r3q, r2q +sal r3q, 3 ; deslocamento aritmético à esquerda 3 = * 8 +add r3q, 5 +add r0q, r3q +``` + +Você verá o uso de ```lea``` no uso de muitos endereços antes de laços ou para fazer cálculos como o anterior. Note bem que você não pode fazer todos os tipos de multiplicações e adições, mas as multiplicações por 1, 2, 4 ou 8 e as adições de um deslocamento fixo são operações comuns. + +No exercício, você terá que carregar uma constante e adicionar os valores a um vetor **SIMD** em um laço. + +[Próxima lição](../lesson_03/index.pt-br.md) \ No newline at end of file diff --git a/lesson_03/index.pt-br.md b/lesson_03/index.pt-br.md new file mode 100644 index 0000000..a9cdd59 --- /dev/null +++ b/lesson_03/index.pt-br.md @@ -0,0 +1,203 @@ +**Lição Três de Linguagem Assembly do FFmpeg** + +Vamos explicar um pouco mais de jargão e leia bem esta pequena lição de história. + +**Conjuntos de Instruções** + +Você certamente prestou atenção ao fato de que na lição anterior, falamos sobre **SSE2** que faz parte do conjunto de instruções **SIMD**. Quando uma nova geração de CPU é lançada no mercado, ela pode vir acompanhada de novas instruções e às vezes registradores de tamanhos maiores. A história do conjunto de instruções x86 é tão complexa que existe uma versão simplificada (com várias subhistórias dentro dela): + +* MMX - Lançado em 1997, a primeira versão **SIMD** nos Processadores Intel, registradores de 64 bits, versão histórica. +* SSE (Streaming SIMD Extensions) - Lançado em 1999, registradores de 128 bits. +* SSE2 - Lançado em 2000, várias novas instruções. +* SSE3 - Lançado em 2004, primeiras instruções *horizontais*. +* SSSE3 (Supplemental SSE3) - Lançado em 2006, adição de novas instruções incluindo a mais importante ```pshufb```, sem dúvida a instrução mais importante para processamento de vídeo +* SSE4 - Lançado em 2008, adição de muitas novas instruções, notavelmente mínimos e máximos em modo vetorizado. +* AVX - Lançado em 2011, adição de registradores de 256 bits (apenas para pontos flutuantes) e uma nova sintaxe de três operandos. +* AVX2 - Lançado em 2013, adição de registradores de 256 bits para registradores para instruções inteiras. +* AVX512 - Lançado em 2017, registradores de 512 bits, nova instrução de mascaramento. Seu uso no FFmpeg era muito limitado devido à diminuição da frequência da CPU quando novas instruções eram usadas. Permutação completa de 512 bits com ```vpermb```. +* AVX512ICL - Lançado em 2019, remoção da redução de frequência do processador. +* AVX10 - Por vir. + +É importante notar que conjuntos de instruções podem ser removidos ou adicionados de uma CPU para outra. Por exemplo, **AVX512** foi [removido](https://www.igorslab.de/en/intel-deactivated-avx-512-on-alder-lake-but-fully-questionable-interpretation-of-efficiency-news-editorial/), de forma controversa, na 12ª geração de CPUs Intel. É por isso que o FFmpeg faz detecção de CPU em tempo de execução. O FFmpeg detecta as capacidades do processador no qual está sendo executado. + +Como você viu no exercício, os ponteiros de função são por padrão em **C** e são substituídos por um conjunto de instruções particular. Isso significa que a detecção é realizada uma vez e nunca mais será necessária. Isso contrasta com muitas aplicações proprietárias que programam rigidamente em seu código um conjunto de instruções tornando obsoletos computadores perfeitamente funcionais. Isso também permite ativar ou desativar funções otimizadas em tempo de execução. Este é um dos maiores benefícios do código aberto. + +Programas como o FFmpeg são usados por bilhões de dispositivos ao redor do mundo, alguns deles podem ser muito antigos. Tecnicamente, o FFmpeg é compatível com máquinas que possuem apenas o conjunto de instruções **SSE**, máquinas que datam de 25 anos atrás. +Felizmente, **x86inc.asm** é capaz de indicar se uma instrução não está disponível em um conjunto de instruções particular. + +Para dar uma ideia das compatibilidades, aqui está a disponibilidade dos conjuntos de instruções segundo o [Steam Survey](https://store.steampowered.com/hwsurvey/Steam-Hardware-Software-Survey-Welcome-to-Steam) em novembro de 2024 (obviamente tendencioso em favor dos jogadores): + +| Conjunto de Instruções | Disponibilidade | +| :---- | :---- | +| SSE2 | 100% | +| SSE3 | 100% | +| SSSE3 | 99.86% | +| SSE4.1 | 99.80% | +| AVX | 97.39% | +| AVX2 | 94.44% | +| AVX512 (AVX512 e AVX512ICL combinados) | 14.09% | + +Para uma aplicação como o FFmpeg com bilhões de usuários, mesmo 0.1% deles representa um grande número de usuários e relatórios de bugs em caso de problemas. O FFmpeg possui uma grande infraestrutura de testes para testar cada combinação de CPU/OS/compiladores apresentada no [FATE testsuite](https://fate.ffmpeg.org/?query=subarch:x86_64%2F%2F). Cada commit único é executado em centenas de máquinas para ter certeza de que nada quebra. + +A Intel fornece um manual de conjunto de instruções detalhado aqui: [https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html) + +Pode ser trabalhoso pesquisar em um PDF, uma alternativa online está presente aqui: [https://www.felixcloutier.com/x86/](https://www.felixcloutier.com/x86/) + +Há também uma representação visual das instruções SIMD disponível aqui: [https://www.officedaytime.com/simd512e/](https://www.officedaytime.com/simd512e/) + +Parte do desafio do uso do assembly x86 é encontrar a instrução certa que você precisa. Em alguns casos, instruções podem ser usadas em situações para as quais não foram originalmente feitas. + +**Truques sobre Deslocamentos de Ponteiros** + +Vamos voltar à nossa função original da Lição 1, mas adicionar um argumento de largura à função em C. + +Usamos `ptrdiff_t` para a variável de largura em vez de `int` para garantir que os 32 bits superiores do argumento de 64 bits sejam zerados. Se passássemos diretamente uma largura em `int` na assinatura da função e depois tentássemos usá-la como um `quad` para aritmética de ponteiros (ou seja, usando `widthq`), os 32 bits superiores do registrador poderiam conter valores arbitrários. Poderíamos corrigir isso estendendo o sinal de width com `movsxd` (veja também a macro `movsxdifnidn` em **x86inc.asm**), mas este método é mais simples. + +A função abaixo contém o truque dos deslocamentos de ponteiro: + +```assembly +;static void add_values(const uint8_t *src, const uint8_t *src2, ptrdiff_t width) +INIT_XMM sse2 +cglobal add_values, 3, 3, 2, src, src2, width + add srcq, widthq + add src2q, widthq + neg widthq + +.loop + movu m0, [srcq+widthq] + movu m1, [src2q+widthq] + + paddb m0, m1 + + movu [srcq+widthq], m0 + add widthq, mmsize + jl .loop + + RET +``` + +Vamos revisar isso passo a passo, pois pode ser confuso: + +```assembly + add srcq, widthq + add src2q, widthq + neg widthq +``` + +A largura `widthq` é adicionada a cada ponteiro de modo que cada ponteiro agora aponta para o final do buffer a processar. O sinal de `widthq` é então invertido. + +```assembly + movu m0, [srcq+widthq] + movu m1, [src2q+widthq] +``` + +Os carregamentos são então feitos com `widthq` sendo negativo. Assim, na primeira iteração, `[srcq+widthq]` aponta para o endereço original de `srcq`, ou seja, aponta para o início do buffer. + +```assembly + add widthq, mmsize + jl .loop +``` + +`mmsize` é adicionado a `widthq` negativo, aproximando-o assim de zero. A condição do laço agora é `jl` (*jump if less than zero*). Este truque permite que `widthq` seja usado tanto como deslocamento de ponteiro quanto como contador de laço, economizando assim uma instrução `cmp`. Também permite usar o deslocamento do ponteiro em múltiplos carregamentos e armazenamentos, bem como usar múltiplos dos deslocamentos de ponteiro se necessário (lembre-se disso para o exercício). + +**Alinhamento** + +Em todos os nossos exemplos, usamos `movu` para evitar o assunto do alinhamento. Muitos processadores podem carregar e armazenar dados mais rapidamente se eles estiverem alinhados, ou seja, se o endereço de memória for divisível pelo tamanho do registrador SIMD. Quando possível, tentamos usar carregamentos e armazenamentos alinhados no FFmpeg usando `mova`. + +No FFmpeg, `av_malloc` pode fornecer memória alinhada no heap, e a diretiva de pré-processador C DECLARE_ALIGNED pode fornecer memória alinhada na pilha. Se `mova` for usado com um endereço não alinhado, isso resultará em uma falha de segmentação e a aplicação travará. Também é importante garantir que o valor de alinhamento corresponda ao tamanho do registrador SIMD, ou seja, 16 para xmm, 32 para `ymm` e 64 para `zmm`. + +Aqui está como alinhar o início da seção RODATA em 64 bytes: + +```assembly +SECTION_RODATA 64 +``` + +Note que isso alinha apenas o início da seção RODATA. Bytes de preenchimento podem ser necessários para garantir que o label seguinte permaneça em um limite de 64 bytes. + +**Desempacotamento** + +Outro assunto que evitamos até agora é o estouro. Isso ocorre, por exemplo, quando o valor de um byte excede 255 após uma operação como adição ou multiplicação. Podemos querer realizar uma operação onde precisamos de um valor intermediário maior que um byte (por exemplo, palavras), ou potencialmente queremos deixar os dados nesse tamanho intermediário maior. + +Para bytes não assinados, é aí que as instruções `punpcklbw` (desempacotar bytes em palavras na parte baixa de um pacote) e `punpckhbw` (desempacotar bytes em palavras na parte alta de um pacote) entram. + +Vamos ver como funciona `punpcklbw`. A sintaxe para a versão SSE2 no manual da Intel é a seguinte: + +| PUNPCKLBW xmm1, xmm2/m128 | +| :---- | + +Isso significa que sua fonte (lado direito) pode ser um registrador xmm ou um endereço de memória (m128 significa um endereço de memória com a sintaxe padrão **[base + escala*índice + deslocamento]**), e o destino é um registrador xmm. + +O site officedaytime.com acima tem um bom diagrama mostrando o que acontece: + +![O que é isso](image1.png) + +Você pode ver que os bytes são entrelaçados a partir da metade inferior de cada registrador respectivamente. Mas que relação isso tem com a extensão de faixa? Se o registrador fonte estiver totalmente zerado, isso entrelaça os bytes no registrador de destino com zeros. Isso é chamado de *extensão por zero*, porque os bytes são sem sinal. punpckhbw pode ser usado para fazer a mesma coisa para os bytes superiores. + +Aqui está um trecho mostrando como isso é feito: + +```assembly +pxor m2, m2 ; zerar m2 + +movu m0, [srcq] +movu m1, m0 ; fazer uma cópia de m0 em m1 +punpcklbw m0, m2 +punpckhbw m1, m2 +``` + +```m0``` e ```m1``` agora contêm os bytes originais estendidos a zero em palavras. Na próxima lição, você verá como as instruções de três operandos em AVX tornam o segundo ```movu``` desnecessário. + +**Extensão de Sinal** + +Dados com sinal são um pouco mais complicados. Para estender a faixa de um inteiro com sinal, precisamos usar um processo conhecido como [extensão de sinal](https://en.wikipedia.org/wiki/Sign_extension). Isso consiste em adicionar bits de sinal nos bits mais significativos. Por exemplo: ```-2``` em ```int8_t``` é ```0b11111110```. Para estendê-lo para ```int16_t```, o bit de sinal ```1``` é repetido para obter ```0b1111111111111110```. + +```pcmpgtb``` (packed compare greater than byte) pode ser usado para extensão de sinal. Ao fazer a comparação (0 > byte), todos os bits no byte de destino são definidos como 1 se o byte for negativo, caso contrário os bits no byte de destino são definidos como 0. ```punpckX``` pode ser usado como acima para realizar a extensão de sinal. Se o byte for negativo, o byte correspondente é ```0b11111111``` e caso contrário é ```0x00000000```. O entrelaçamento do valor do byte com a saída de ```pcmpgtb``` realiza uma extensão de sinal em palavra. + +```assembly +pxor m2, m2 ; zerar m2 + +movu m0, [srcq] +movu m1, m0 ; fazer uma cópia de m0 em m1 + +pcmpgtb m2, m0 +punpcklbw m0, m2 +punpckhbw m1, m2 +``` + +Como você pode ver, há uma instrução adicional em comparação com o caso sem sinal. + +**Empacotamento** + +```packuswb``` (empacotar palavra sem sinal em byte) e ```packsswb``` permitem que você passe de palavra para byte. Eles permitem entrelaçar dois registradores SIMD contendo palavras em um único registrador SIMD com um byte. Note que se os valores excederem a faixa de bytes, eles serão saturados (ou seja, limitados ao valor máximo). + +**Embaralhamentos** + +Os embaralhamentos (*shuffles*), também chamados de permutações, são indiscutivelmente a instrução mais importante no processamento de vídeo e ```pshufb``` (packed shuffle bytes), disponível em SSSE3, é a variante mais importante. + +Para cada byte, o byte fonte correspondente é usado como um índice no registrador de destino, exceto quando o bit mais significativo (MSB) está ativado, o byte de destino é zerado. Este é o análogo do seguinte código C (embora em SIMD, as 16 iterações do laço ocorrem em paralelo): + +```c +for(int i = 0; i < 16; i++) { + if(src[i] & 0x80) + dst[i] = 0; + else + dst[i] = dst[src[i]] +} +``` + +Aqui está um exemplo simples em assembly: + +```assembly +SECTION_DATA 64 + +shuffle_mask: db 4, 3, 1, 2, -1, 2, 3, 7, 5, 4, 3, 8, 12, 13, 15, -1 + +section .text + +movu m0, [srcq] +movu m1, [shuffle_mask] +pshufb m0, m1 ; embaralhar m0 baseado em m1 +``` + +Note que -1, para fácil leitura, é usado como o índice de embaralhamento para zerar o byte de saída: ```-1``` como byte é o campo binário ```0b11111111``` (complemento de dois), e assim o bit mais significativo (MSB) (0x80) está ativado. + +[image1]: \ No newline at end of file