asm-lessons Documentation

FFmpeg Assembly Language Lessons

12,177 stars Unknown 1 file · ~39,585 tokens
RAW Doc

File: README.es.md

Bienvenido a la Escuela de Lenguaje Ensamblador de FFmpeg. Has dado el primer paso en el viaje más interesante, desafiante y gratificante de la programación. Estas lecciones te proporcionarán una base sobre cómo se utiliza el ensamblador en FFmpeg y te abrirán los ojos a lo que realmente ocurre en tu ordenador.

Conocimientos necesarios

  • Conocimientos de C, en particular de punteros. Si no sabes C, estudia el libro The C Programming Language
  • Matemáticas de secundaria (escalares vs vectores, suma, multiplicación, etc.)

Lecciones

En este repositorio Git hay lecciones y tareas (aún no subidas) que corresponden a cada lección. Al final de las lecciones, podrás contribuir a FFmpeg.

Hay un servidor Discord disponible para responder preguntas:
https://discord.com/invite/Ks5MhUhqfB

Traducciones


File: README.fr.md

Bienvenue dans la FFmpeg School of Assembly Language. Vous avez fait le premier pas dans le voyage le plus intéressant, le plus stimulant et le plus gratifiant de la programmation. Ces leçons vous donneront des bases sr la façon sur la manière l'assembleur est utilisée dans FFmpeg et vous ouvriront les yeux sur ce qui se passe réellement à l'intérieur de votre ordinateur...

Connaissances requises

  • Connaissance en C, particulièrement les pointeurs. Si le C ne vous est pas familier, vous trouverez toutes les bases dans le livre The C Programming Language.
  • Mathématiques de lycée (scalaire vs vecteur, addition, multiplication, etc...)

Leçons

Dans ce répo Git se trouvent les leçons et des exercices (non inclus actuellement) relatifs à chaque leçon. A la fin de l'ensemble des leçons, vous serez capable de contribuer à FFmpeg.

Un serveur discord est accessible pour vous apporter des réponses :
https://discord.com/invite/Ks5MhUhqfB

Traductions


File: README.md

Welcome to the FFmpeg School of Assembly Language. You have taken the first step on the most interesting, challenging, and rewarding journey in programming. These lessons will give you a grounding in the way assembly language is written in FFmpeg and open your eyes to what's actually going on in your computer.

Required Knowledge

  • Knowledge of C, in particular pointers. If you don't know C, work through The C Programming Language book
  • High School Mathematics (scalar vs vector, addition, multiplication etc)

Lessons

In this Git repository there are lessons and assignments (not uploaded yet) that correspond with each lessons. By the end of the lessons you'll be able to contribute to FFmpeg.

A discord server is available to answer questions:
https://discord.com/invite/Ks5MhUhqfB

Translations


File: README.tr.md

FFmpeg Assembly Dili Okuluna hoş geldiniz. Programlamanın en ilginç, zorlu ve ödüllendirici yolculuğunda ilk adımı attınız. Bu dersler, assembly dilinin FFmpeg'de nasıl yazıldığı konusunda size temel bilgi verecek ve bilgisayarınızda gerçekte neler olup bittiğine gözlerinizi açacaktır.

Gerekli Bilgiler

  • C bilgisi, özellikle pointer'lar. Eğer C bilmiyorsanız, The C Programming Language kitabını çalışın
  • Lise Matematiği (skalar vs vektör, toplama, çarpma vb.)

Dersler

Bu Git deposunda her derse karşılık gelen dersler ve ödevler (henüz yüklenmedi) bulunmaktadır. Derslerin sonunda FFmpeg'e katkıda bulunabileceksiniz.

Soruları yanıtlamak için bir discord sunucusu mevcuttur:
https://discord.com/invite/Ks5MhUhqfB

Çeviriler


File: README.zh.md

欢迎来到 FFmpeg 汇编语言学校。你已经迈出了编程中最有趣、最具挑战、最具回报的一段旅程的第一步。这些课程将为你打下 FFmpeg 中汇编语言编写的基础,并让你大开眼界,了解计算机内部的实际运作方式。

必备知识

  • C 语言知识,特别是指针。如果你不了解 C,请通读 C程序设计语言 这本书
  • 高中数学(标量与向量,加法,乘法等)

课程

本 Git 仓库包含与每节课对应的教程和作业(尚未上传)。在课程结束时,你将能够为 FFmpeg 做出贡献。

有一个 discord 服务器可供提问:
https://discord.com/invite/Ks5MhUhqfB

翻译


File: lesson_01/index.es.md

Lección Uno de Lenguaje Ensamblador FFmpeg

Introducción

Bienvenido a la Escuela de Lenguaje Ensamblador de FFmpeg. Has dado el primer paso en el viaje más interesante, desafiante y gratificante de la programación. Estas lecciones te proporcionarán una base sobre cómo se utiliza el ensamblador en FFmpeg y te abrirán los ojos a lo que realmente ocurre en tu ordenador.

Conocimientos necesarios

  • Conocimientos de C, en particular de punteros. Si no sabes C, estudia el libro The C Programming Language
  • Matemáticas de secundaria (escalares vs vectores, suma, multiplicación, etc.)

¿Qué es el lenguaje ensamblador?

El lenguaje ensamblador es un lenguaje de programación en el que se escribe código que se corresponde directamente con las instrucciones que procesa una CPU. El lenguaje ensamblador legible por humanos, como su nombre indica, se ensamble en datos binarios, conocidos como código máquina, que la CPU puede entender. Es posible que veas el código en lenguaje ensamblador denominado como ensamblador o asm para abreviar.

La gran mayoría del código ensamblador en FFmpeg es lo que se conoce como SIMD, Single Instruction Multiple Data (instrucción única, datos múltiples). SIMD se conoce a veces como programación vectorial. Esto significa que una instrucción concreta opera sobre varios elementos de datos al mismo tiempo. La mayoría de los lenguajes de programación operan sobre un elemento de datos a la vez, lo que se conoce como programación secuencial.

Como habrás adivinado, SIMD se presta bien al procesamiento de imágenes, vídeo y audio, que tienen muchos datos ordenados secuencialmente en la memoria. Hay instrucciones especializadas disponibles en la CPU para ayudarnos a procesar datos secuenciales.

En FFmpeg, verás los términos «función de ensamblador», «SIMD» y «vectorizar» utilizados indistintamente. Todos ellos se refieren a lo mismo: escribir una función en lenguaje ensamblador a mano para procesar múltiples elementos de datos de una sola vez. Algunos proyectos también pueden referirse a ellos como «núcleos de ensamblador».

Todo esto puede parecer complicado, pero es importante recordar que en FFmpeg, los estudiantes de secundaria han escrito código ensamblador. Como con todo, el aprendizaje es un 50 % jerga y un 50 % aprendizaje real.

¿Por qué escribimos en lenguaje ensamblador?
Para acelerar el procesamiento multimedia. Es muy habitual conseguir una mejora de velocidad de 10 veces o más al escribir código ensamblador, lo que resulta especialmente importante cuando se desea reproducir vídeos en tiempo real sin interrupciones. Además, ahorra energía y prolonga la duración de la batería. Cabe señalar que las funciones de codificación y descodificación de vídeo son algunas de las más utilizadas en el mundo, tanto por los usuarios finales como por las grandes empresas en sus centros de datos. Por lo tanto, incluso una pequeña mejora se acumula rápidamente.

A menudo se ve en Internet que la gente utiliza intrínsecos, que son funciones similares a las de C que se asignan a instrucciones de ensamblador para permitir un desarrollo más rápido. En FFmpeg no utilizamos intrínsecos, sino que escribimos el código ensamblador a mano. Se trata de un tema controvertido, pero los intrínsecos suelen ser entre un 10 y un 15 % más lentos que el ensamblador escrito a mano (los defensores de los intrínsecos no estarían de acuerdo), dependiendo del compilador. Para FFmpeg, cada bit de rendimiento adicional ayuda, por lo que escribimos directamente en código ensamblador. También se argumenta que las intrínsecas son difíciles de leer debido al uso de la «[notación húngara] (https://en.wikipedia.org/wiki/Hungarian_notation)».

También es posible que veas ensamblador en línea (es decir, sin usar intrínsecos) en algunos lugares de FFmpeg por razones históricas, o en proyectos como el kernel de Linux debido a casos de uso muy específicos. Aquí es donde el código ensamblador no se encuentra en un archivo separado, sino que se escribe en línea con el código C. La opinión predominante en proyectos como FFmpeg es que este código es difícil de leer, no es ampliamente compatible con los compiladores y es imposible de mantener.

Por último, verás a muchos autoproclamados expertos en Internet diciendo que nada de esto es necesario y que el compilador puede hacer toda esta «vectorización» por ti. Al menos con fines de aprendizaje, ignóralos: pruebas recientes en, por ejemplo, el proyecto dav1d mostraron una aceleración de aproximadamente 2 veces gracias a esta vectorización automática, mientras que las versiones escritas a mano podían alcanzar 8 veces.

Variantes del lenguaje ensamblador
Estas lecciones se centrarán en el lenguaje ensamblador x86 de 64 bits. También se conoce como amd64, aunque sigue funcionando en CPU Intel. Existen otros tipos de ensamblador para otras CPU, como ARM y RISC-V, y es posible que en el futuro estas lecciones se amplíen para abarcarlos.

Hay dos tipos de sintaxis de ensamblador x86 que verás en Internet: AT&T e Intel. La sintaxis AT&T es más antigua y más difícil de leer en comparación con la sintaxis Intel. Por lo tanto, utilizaremos la sintaxis Intel.

Materiales de apoyo
Quizás te sorprenda saber que los libros o recursos en línea como Stack Overflow no son especialmente útiles como referencias. Esto se debe en parte a nuestra decisión de utilizar ensamblador escrito a mano con sintaxis Intel. Pero también a que muchos recursos en línea se centran en la programación de sistemas operativos o hardware, y suelen utilizar código no SIMD. El ensamblador FFmpeg se centra especialmente en el procesamiento de imágenes de alto rendimiento y, como verás, se trata de un enfoque particularmente único de la programación en ensamblador. Dicho esto, una vez completadas estas lecciones, te resultará fácil comprender otros casos de uso del ensamblador.

Muchos libros profundizan en numerosos detalles de la arquitectura informática antes de enseñar ensamblador. Esto está bien si es lo que quieres aprender, pero desde nuestro punto de vista, es como estudiar ingeniería antes de aprender a conducir un coche.

Dicho esto, los diagramas de las últimas partes del libro «The Art of 64-bit assembly» que muestran las instrucciones SIMD y su comportamiento de forma visual son útiles: https://artofasm.randallhyde.com/

Hay un servidor Discord disponible para responder preguntas:
https://discord.com/invite/Ks5MhUhqfB

Registros
Los registros son áreas de la CPU donde se pueden procesar los datos. Las CPU no operan directamente sobre la memoria, sino que los datos se cargan en los registros, se procesan y se vuelven a escribir en la memoria. En lenguaje ensamblador, por lo general, no se pueden copiar datos directamente de una ubicación de memoria a otra sin pasar primero esos datos por un registro.

Registros de propósito general
El primer tipo de registro es el conocido como registro de propósito general (GPR). Los GPR se denominan de propósito general porque pueden contener datos, en este caso hasta un valor de 64 bits, o una dirección de memoria (un puntero). Un valor en un GPR se puede procesar mediante operaciones como suma, multiplicación, desplazamiento, etc.

En la mayoría de los libros sobre ensamblador, hay capítulos enteros dedicados a las sutilezas de los GPR, sus antecedentes históricos, etc. Esto se debe a que los GPR son importantes cuando se trata de programación de sistemas operativos, ingeniería inversa, etc. En el código ensamblador escrito en FFmpeg, los GPR son más bien como andamios y, en la mayoría de los casos, su complejidad no es necesaria y se abstrae.

Registros vectoriales
Los registros vectoriales (SIMD), como su nombre indica, contienen múltiples elementos de datos. Existen varios tipos de registros vectoriales:

  • Registros mm: registros MMX, de 64 bits, históricos y que ya no se utilizan mucho.
  • Registros xmm: registros XMM, de 128 bits, ampliamente disponibles.
  • Registros ymm: registros YMM, de 256 bits, con algunas complicaciones a la hora de utilizarlos.
  • Registros zmm: registros ZMM, de 512 bits, disponibilidad limitada

La mayoría de los cálculos en la compresión y descompresión de vídeo se basan en números enteros, por lo que nos ceñiremos a eso. Aquí hay un ejemplo de 16 bytes en un registro xmm:

a b c d e f g h i j k l m n o p

Pero podrían ser ocho palabras (enteros de 16 bits).

a b c d e f g h

O cuatro palabras dobles (enteros de 32 bits)

a b c d

O dos palabras cuádruples (enteros de 64 bits):

a b

En resumen:

  • bytes: datos de 8 bits
  • words: datos de 16 bits
  • doublewords: datos de 32 bits
  • quadwords: datos de 64 bits
  • double quadwords: datos de 128 bits

Los caracteres en negrita serán importantes más adelante.

x86inc.asm include
Verás que en muchos ejemplos incluimos el archivo x86inc.asm. X86inc.asm es una capa de abstracción ligera que se utiliza en FFmpeg, x264 y dav1d para facilitar el trabajo de los programadores de ensamblador. Ayuda de muchas maneras, pero, para empezar, una de las cosas útiles que hace es etiquetar los GPR, r0, r1, r2. Esto significa que no tienes que recordar ningún nombre de registro. Como se ha mencionado anteriormente, los GPR son generalmente solo andamios, por lo que esto facilita mucho las cosas.

Un fragmento sencillo de código ensamblador escalar

Veamos un fragmento sencillo (y muy artificial) de código ensamblador escalar (código ensamblador que opera sobre elementos de datos individuales, uno a uno, dentro de cada instrucción) para ver qué está pasando:

assembly
mov  r0q, 3  
inc  r0q  
dec  r0q  
imul r0q, 5

En la primera línea, el valor inmediato 3 (un valor almacenado directamente en el propio código ensamblador, en contraposición a un valor obtenido de la memoria) se almacena en el registro r0 como una palabra cuádruple. Ten en cuenta que, en la sintaxis de Intel, el operando de origen (el valor o la ubicación que proporciona los datos, situado a la derecha) se transfiere al operando de destino (la ubicación que recibe los datos, situada a la izquierda), de forma muy similar al comportamiento de memcpy. También puedes leerlo como «r0q = 3», ya que el orden es el mismo. El sufijo «q» de r0 designa que el registro se utiliza como una palabra cuádruple. inc incrementa el valor para que r0q contenga 4, dec decrementa el valor de nuevo a 3. imul multiplica el valor por 5. Así, al final, r0q contiene 15.

Ten en cuenta que las instrucciones legibles para los humanos, como mov e inc, que el ensamblador convierte en código máquina, se conocen como mnemónicos. Es posible que veas en Internet y en libros mnemónicos representados con letras mayúsculas, como MOV e INC, pero son iguales que las versiones en minúsculas. En FFmpeg, utilizamos mnemónicos en minúsculas y reservamos las mayúsculas para las macros.

Comprender una función vectorial básica

Esta es nuestra primera función SIMD:

assembly
%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

Repasemos línea por línea:

assembly
%include "x86inc.asm"

Se trata de un «encabezado» o header desarrollado en las comunidades x264, FFmpeg y dav1d para proporcionar helpers, nombres predefinidos y macros (como cglobal más abajo) que simplifican la escritura de ensamblador.

assembly
SECTION .text

Esto indica la sección donde se coloca el código que se desea ejecutar. Esto contrasta con la sección .data, donde se pueden colocar datos constantes.

assembly
;static void add_values(uint8_t *src, const uint8_t *src2)  
INIT_XMM sse2

La primera línea es un comentario (el punto y coma «;» en asm es como «//» en C) que muestra cómo es el argumento de la función en C. La segunda línea muestra cómo inicializamos la función para utilizar los registros XMM, utilizando el conjunto de instrucciones sse2. Esto se debe a que paddb es una instrucción sse2. Trataremos sse2 con más detalle en la próxima lección.

assembly
cglobal add_values, 2, 2, 2, src, src2

Esta es una línea importante, ya que define una función C llamada «add_values».

Repasemos cada elemento uno por uno:

  • El primer parámetro muestra que la función tiene dos argumentos.
  • El parámetro siguiente muestra que utilizaremos dos GPR para los argumentos. En algunos casos, es posible que queramos utilizar más GPR, por lo que debemos indicar a x86util que necesitamos más.
  • El parámetro siguiente le indica a x86util cuántos registros XMM vamos a utilizar.
  • Los dos parámetros siguientes son etiquetas para los argumentos de la función.

Cabe señalar que es posible que el código más antiguo no tenga etiquetas para los argumentos de la función, sino que se dirija directamente a los GPR utilizando r0, r1, etc.

assembly
movu  m0, [srcq]  
    movu  m1, [src2q]

movu es la abreviatura de movdqu (move double quad unaligned). La alineación se tratará en otra lección, pero por ahora movu puede considerarse como un movimiento de 128 bits desde [srcq]. En el caso de mov, los corchetes significan que la dirección en [srcq] se está desreferenciando, lo equivalente a *src en C. Esto es lo que se conoce como una carga. Ten en cuenta que el sufijo «q» se refiere al tamaño del puntero * (*es decir, en C representa *sizeof(*src) == 8 en sistemas de 64 bits, y x86asm es lo suficientemente inteligente como para usar 32 bits en sistemas de 32 bits), pero la carga subyacente es de 128 bits.

Tenga en cuenta que no nos referimos a los registros vectoriales por su nombre completo, en este caso xmm0, sino como m0, una forma abstracta. En futuras lecciones verá cómo esto significa que puede escribir código una vez y hacer que funcione en múltiples tamaños de registros SIMD.

assembly
paddb m0, m1

paddb (léase en voz alta como p-add-b) suma cada byte de cada registro, tal y como se muestra a continuación. El prefijo «p» significa «empaquetado» o «packed» y se utiliza para identificar las instrucciones vectoriales frente a las instrucciones escalares. El sufijo «b» indica que se trata de una suma por bytes (suma 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

Esto es lo que se conoce como «store». Los datos se vuelven a escribir en la dirección del puntero srcq.

assembly
RET

Esta es una macro para indicar los retornos de la función. Prácticamente todas las funciones de ensamblador en FFmpeg modifican los datos de los argumentos en lugar de devolver un valor.

Como verás en la tarea, creamos punteros de función a funciones de ensamblador y los utilizamos cuando están disponibles.

Siguiente lección


File: lesson_01/index.fr.md

FFmpeg Assembly Language Leçon Une

Introduction

Bienvenue dans la FFmpeg School of Assembly Language. Vous avez fait le premier pas dans le voyage le plus intéressant, le plus stimulant et le plus gratifiant de la programmation. Ces leçons vous donneront des bases sur la façon dont l’assembleur est utilisé dans FFmpeg et vous ouvriront les yeux sur ce qui se passe réellement dans votre ordinateur...

Connaissances requises

  • Connaissance en C, particulièrement les pointeurs. Si le C ne vous est pas familier, vous trouverez toutes les bases dans le livre The C Programming Language.
  • Mathématiques de lycée (scalaire vs vecteur, addition, multiplication, etc...)

Qu'est-ce que l'assembleur ?

L'assembleur est un langage de programmation où le code que vous écrivez correspond directement à des instructions compréhensibles par un CPU. L'assembleur lisible par l'humain est, comme son nom l'indique, assemblé en données binaires, connues sous le nom de langage machine, que le CPU peut comprendre. Le code assembleur est souvent appelé "assembleur" ou "asm" en abrégé.

La grande majorité de l'assembleur de FFmpeg est ce que l'on appelle SIMD, Single Instruction Multiple Data (instruction unique, données multiples). SIMD est parfois désigné sous le terme de programmation vectorielle. Cela signifie qu'une instruction particulière opère sur plusieurs éléments de données simultanément. La plupart des langages de programmation traitent un seul élément de données à la fois, ce que l'on appelle la programmation scalaire.

Comme vous l'avez peut-être deviné, SIMD se prête bien au traitement d'images, de vidéos et d'audio, qui contiennent une grande quantité de données organisées séquentiellement en mémoire. Des instructions spécialisées dans le processeur nous aideront à traiter ces données séquentielles.

Dans FFmpeg, vous verrez que les termes fonction en assembleur, SIMD, vectorisation sont utilisés de manière interchangeable. Ils désignent tous la même chose : écrire une fonction en assembleur à la main pour traiter plusieurs éléments de données en une seule fois. Certains projets peuvent aussi faire référence à des noyaux en assembleur.

Tout cela peut sembler compliqué, mais il est important de rappeler que des lycéens ont écrit de l'assembleur dans FFmpeg. Comme partout, l'apprentissage, c'est 50 % du jargon et 50 % d'apprentissage réel.

Pourquoi écrivons-nous en assembleur ?

Pour rendre le traitement multimédia rapide. Il est très courant d'avoir une vitesse de traitement au moins 10 fois plus rapide en écrivant en assembleur, ce qui est d'autant plus important quand nous voulons lire des vidéos en temps réel sans saccade. Cela permet aussi d'économiser de l'énergie et d'étendre la durée de vie des batteries. Il est important de souligner que les fonctions d'encodage et de décodage sont parmi les fonctions les plus utilisées au monde, aussi bien par les utilisateurs finaux que par les multinationales dans leurs data centers. Donc, même une petite amélioration apporte beaucoup.

Vous verrez souvent, en ligne, des gens utiliser des fonctions intrinsèques, des fonctions ressemblant à du C mais qui sont en fait des instructions en assembleur utilisées pour permettre un développement plus rapide. Dans FFmpeg, nous n'utilisons pas ce genre de fonctions, nous écrivons tout le code en assembleur à la main. C'est un point de discorde, mais les fonctions intrinsèques sont environ 10 à 15 % plus lentes que l'équivalent en assembleur écrit à la main (les partisans de ces fonctions ne seraient pas d'accord), tout dépend du compilateur. Pour FFmpeg, chaque amélioration compte, c'est pourquoi nous écrivons tout le code directement en assembleur. Un argument en notre faveur est l'utilisation de la notation hongroise dans les fonctions intrinsèques, qui complique leur lecture.

Aussi, vous verrez des références à de l'assembleur en ligne (ou assembleur inline), c'est-à-dire n'utilisant pas de fonctions intrinsèques, à quelques endroits dans FFmpeg pour des raisons historiques, ou dans des projets comme le noyau Linux dans des scénarios d'utilisation très spécifiques. Ici, le code assembleur n'est pas dans un fichier séparé mais écrit directement dans des fichiers avec du code en C. Le point de vue majoritaire dans des projets comme FFmpeg est que ce code est difficile à lire, pas largement supporté par les compilateurs et difficile à maintenir.

Enfin, vous verrez beaucoup d'experts autoproclamés sur Internet disant que rien de tout cela n'est nécessaire et que le compilateur peut effectuer cette vectorisation pour vous. Dans une optique d'apprentissage, ignorez-les : des tests récents comme ceux présents sur le projet dav1d ont montré un gain de vitesse d'environ x2 grâce à cette vectorisation automatique, tandis que pour des versions écrites à la main, ce gain montait à x8.

Les variétés d'assembleurs

Ces leçons se concentreront sur l'assembleur x86 64 bits, aussi connu sous le nom d'amd64, bien qu'il fonctionne également sur les CPUs Intel. Il existe autant de types d'assembleurs que de CPUs, comme ceux pour ARM ou RISC-V, avec potentiellement des mises à jour de ces cours pour les inclure.

Il existe deux types de syntaxe pour l'assembleur x86 que vous trouverez en ligne : AT&T et Intel. La première est plus ancienne et plus difficile à lire comparée à la seconde. Nous nous intéresserons à cette dernière.

Matériels supportés

Vous serez peut-être surpris d'entendre que des livres ou des ressources en ligne comme Stack Overflow ne sont pas d'une grande aide comme références. Particulièrement à cause de notre choix d'utiliser de l'assembleur écrit à la main avec la syntaxe Intel. Mais aussi parce que beaucoup de ces ressources en ligne se concentrent sur de la programmation de systèmes d'exploitation ou de la programmation pour du hardware, n'utilisant pas du code SIMD. L'assembleur de FFmpeg est majoritairement axé autour du traitement d'images haute performance, et comme vous le verrez, avec une approche particulière de la programmation en assembleur. Ceci étant dit, il est facile de comprendre d'autres cas d'utilisation de l'assembleur une fois ces leçons terminées.

Beaucoup de livres détaillent différentes architectures d'ordinateur avant de détailler l'assembleur. De notre point de vue, c'est très bien si c'est ce que vous voulez apprendre, mais cela revient à vouloir étudier les moteurs avant d'apprendre à conduire une voiture.

Une fois ceci dit, dans les parties suivantes, les diagrammes du livre The Art of 64-bit Assembly montrant les instructions SIMD et leur comportement sous forme visuelle vous seront très utiles : https://artofasm.randallhyde.com/

Un serveur Discord est disponible pour répondre à vos questions :
https://discord.com/invite/Ks5MhUhqfB

Les registres

Les registres sont des zones du CPU où les données peuvent être traitées. Les CPUs n'interviennent pas directement sur la mémoire : les données sont plutôt chargées dans des registres, traitées, puis écrites à nouveau en mémoire. En assembleur, de manière générale, vous ne pouvez pas copier directement les données d'un emplacement mémoire à un autre sans d'abord passer ces données dans un registre.

Registres à usage général

Le premier type de registre que nous allons rencontrer est connu sous le nom de registre à usage général (GPR). Les GPR sont appelés ainsi car ils peuvent contenir soit des données, soit une valeur allant jusqu'à 64 bits, soit une adresse mémoire (un pointeur). Une valeur dans un GPR peut être traitée par des opérations telles que l'addition, la multiplication, le décalage, etc...

Dans la plupart des livres sur l'assembleur, de nombreux chapitres entiers sont consacrés aux subtilités des GPR, à leur histoire, etc... Car les GPR ont joué un rôle important dans la programmation de systèmes d'exploitation, le reverse engineering, etc... Dans l'assembleur écrit pour FFmpeg, les GPR sont considérés comme des échafaudages et la plupart du temps, leur complexité n'est pas nécessaire et est abstraite.

Registres vectoriels

Les registres vectoriels (SIMD), comme leur nom le suggère, contiennent plusieurs éléments de données. Il existe différents types de registres vectoriels :

  • registres mm : des registres MMX, de taille 64 bits, historiques et peu utilisés de nos jours
  • registres xmm : des registres XMM, de taille 128 bits, largement disponibles
  • registres ymm : des registres YMM, de taille 256 bits, avec quelques complications lors de leur utilisation
  • registres zmm : des registres ZMM, de taille 512 bits, très peu disponibles

La plupart des calculs de compression et de décompression vidéo sont basés sur des entiers, nous nous y limiterons. Voici un exemple d'un registre XMM de 16 octets :

a b c d e f g h i j k l m n o p

Cela pourrait aussi être huit mots (entiers de 16 bits):

a b c d e f g h

Ou quatre double mots (entiers de 32 bits):

a b c d

Ou deux quadruples mots (entiers de 64 bits):

a b

Pour récapituler :

  • bytes - données de 8 bits
  • words - données de 16 bits
  • doublewords - données de 32 bits
  • quadwords - données de 64 bits
  • double quadwords - données de 128 bits

Les caractères en gras seront importants dans la suite.

Header x86inc.asm

Dans beaucoup d'exemples, nous incluons le fichier x86inc.asm. x86inc.asm est une couche d'abstraction légère utilisée dans FFmpeg, x264 et dav1d pour faciliter la vie des développeurs. Elle aide de nombreuses manières, mais l'une des choses utiles qu'elle permet est, entre autres, d'étiqueter les GPR r0, r1 et r2. Vous n'aurez pas à vous souvenir des noms des registres. Comme mentionné précédemment, les GPR sont généralement juste des échafaudages, donc cela rend les choses beaucoup plus simples.

Un extrait simple d'assembleur scalaire

Regardons un extrait simple (et totalement artificiel) de code assembleur scalaire (code assembleur qui opère sur des éléments de données individuels, un à la fois, dans chaque instruction) pour voir ce qui se passe :

assembly
mov  r0q, 3  
inc  r0q  
dec  r0q  
imul r0q, 5

À la première ligne, la valeur immédiate 3 (une valeur stockée directement dans le code assembleur lui-même, contrairement à une valeur récupérée depuis la mémoire) est stockée dans le registre r0 sous forme de quadword. Notez qu'en syntaxe Intel, l'opérande source (la valeur ou l'emplacement fournissant les données, situé à droite) est transféré vers l'opérande de destination (l'emplacement recevant les données, situé à gauche), un peu comme le comportement de memcpy. Vous pouvez aussi le lire comme r0q = 3, puisque l'ordre est le même. Le suffixe q de r0 désigne le registre comme étant utilisé comme quadword. inc incrémente la valeur pour que r0q contienne 4, dec décrémente la valeur pour revenir à 3. imul multiplie la valeur par 5. À la fin, r0q contient donc 15.

Notez que les instructions lisibles par l'humain, comme mov et inc, qui sont assemblées en code machine par l’assembleur, sont appelées mnémoniques. Vous pouvez voir en ligne ou dans des livres ces mnémoniques représentées avec des lettres majuscules comme MOV et INC, mais elles sont identiques à leurs versions en minuscules. Dans FFmpeg, nous utilisons les mnémoniques en minuscules et réservons les majuscules pour les macros.

Comprendre une fonction vectorielle de base

Voici notre première fonction 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

Passons en revue le code ligne par ligne :

assembly
%include "x86inc.asm"

Ce header, développé dans les communautés x264, FFmpeg et dav1d, fournit des auxiliaires, des noms prédéfinis et des macros (comme cglobal ci-dessous) pour simplifier l’écriture du code assembleur.

assembly
SECTION .text

Cela désigne la section où le code que vous voulez exécuter est placé.Cela contraste avec la section .data, où vous pouvez placer des données constantes.

assembly
;static void add_values(const uint8_t *src, const uint8_t *src2);  
INIT_XMM sse2

La première ligne est un commentaire (le point-virgule ; en assembleur est l’équivalent du // en C) montrant à quoi ressemble le prototype de la fonction en C.La seconde ligne montre comment nous initialisons la fonction pour qu’elle utilise des registres XMM, en employant le jeu d’instructions SSE2.
Cela est nécessaire car l’instruction paddb fait partie de SSE2.Nous aborderons SSE2 plus en détail dans la prochaine leçon.

assembly
cglobal add_values, 2, 2, 2, src, src2

Le code précédent montre une ligne importante : la définition d’une fonction C appelée add_values.

Passons en revue chaque élément de la ligne :

  • Le premier paramètre indique que la fonction prend deux arguments (le premier 2).
  • Le deuxième 2 indique que nous allons utiliser deux GPR pour les arguments. Dans certains cas, nous pourrions vouloir en utiliser davantage, et nous devrons donc indiquer à x86util que nous en avons besoin de plus.
  • Le paramètre suivant indique à x86util combien de registres XMM seront utilisés.
  • Les deux derniers paramètres sont les labels utilisés pour les arguments de la fonction add_values.

Il est important de noter que le code plus ancien peut ne pas avoir de labels comme arguments de fonctions, mais plutôt les adresses des registres GPR à la place, en utilisant r0, r1, etc.

assembly
movu  m0, [srcq]  
    movu  m1, [src2q]

movu est une abréviation de movdqu (move double quad unaligned).L’alignement sera abordé dans une autre leçon, mais pour l’instant, vous pouvez considérer movu comme un transfert de 128 bits depuis [srcq].Dans le cas de mov, les crochets [] signifient que l’adresse contenue dans [srcq] est déréférencée, ce qui équivaut à *src en C.C’est ce qu’on appelle un chargement (load).

Le suffixe q fait référence à la taille du pointeur.En C, cela correspond à sizeof(*src) == 8 sur les systèmes 64 bits.L’assembleur x86 est suffisamment intelligent pour utiliser 32 bits sur les systèmes 32 bits, mais l’opération de chargement sous-jacente reste de 128 bits.

Remarquez que nous ne faisons pas référence aux registres vectoriels par leur nom complet (dans ce cas xmm0), mais plutôt sous une forme abstraite comme m0.Dans les prochaines leçons, vous verrez comment cette approche permet d’écrire du code une seule fois et de le faire fonctionner avec plusieurs tailles de registres SIMD.

assembly
paddb m0, m1

paddb (lisez ceci p-add-b) additionne chaque octet dans chaque registre, comme illustré ci-dessous. Le préfixe p signifie packed et sert à distinguer les instructions vectorielles des instructions scalaires. Le suffixe b indique que l'opération est effectuée au niveau des octets (addition d’octets).

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

C’est ce qu’on appelle un stockage (store).Les données sont écrites en mémoire à l’adresse pointée par srcq.

assembly
RET

RET est une macro indiquant que la fonction se termine.Presque toutes les fonctions en assembleur dans FFmpeg modifient les données passées en argument au lieu de retourner une valeur.

Comme vous le verrez dans l’exercice, nous créerons des pointeurs de fonction vers des fonctions en assembleur et les utiliserons lorsque cela sera possible.

Leçon suivante


File: lesson_01/index.md

[Binary file]


File: lesson_01/index.tr.md

FFmpeg Assembly Dili Birinci Ders

Giriş

FFmpeg Assembly Dili Okuluna hoş geldiniz. Programlamanın en ilginç, zorlu ve ödüllendirici yolculuğunda ilk adımı attınız. Bu dersler, assembly dilinin FFmpeg'de nasıl yazıldığı konusunda size temel bilgi verecek ve bilgisayarınızda gerçekte neler olup bittiğine gözlerinizi açacaktır.

Gerekli Bilgiler

  • C bilgisi, özellikle pointer'lar. Eğer C bilmiyorsanız, The C Programming Language kitabını çalışın
  • Lise Matematiği (skalar vs vektör, toplama, çarpma vb.)

Assembly dili nedir?

Assembly dili, CPU'nun işlediği talimatlara doğrudan karşılık gelen kod yazdığınız bir programlama dilidir. İnsan tarafından okunabilir assembly dili, adından da anlaşılacağı gibi, CPU'nun anlayabileceği makine kodu olarak bilinen ikili veriye derlenir. Assembly dili kodunun kısaca "assembly" veya "asm" olarak anıldığını görebilirsiniz.

FFmpeg'deki assembly kodunun büyük çoğunluğu SIMD, Single Instruction Multiple Data (Tek Talimat Çoklu Veri) olarak bilinen türdedir. SIMD bazen vektör programlama olarak da anılır. Bu, belirli bir talimatın aynı anda birden fazla veri öğesi üzerinde çalıştığı anlamına gelir. Çoğu programlama dili bir seferde bir veri öğesi üzerinde çalışır, bu skalar programlama olarak bilinir.

Tahmin edebileceğiniz gibi, SIMD kendisini bellekte sıralı olarak düzenlenmiş çok miktarda veriye sahip görüntü, video ve ses işlemeye iyi uyarlar. CPU'da sıralı veriyi işlememize yardımcı olacak özel talimatlar mevcuttur.

FFmpeg'de "assembly fonksiyonu", "SIMD" ve "vektör(leştirme)" terimlerinin birbirinin yerine kullanıldığını göreceksiniz. Hepsi aynı şeyi ifade eder: Birden fazla veri öğesini tek seferde işlemek için elle assembly dilinde bir fonksiyon yazmak. Bazı projeler bunları "assembly kernel'leri" olarak da adlandırabilir.

Tüm bunlar karmaşık gelebilir, ancak FFmpeg'de lise öğrencilerinin assembly kodu yazdığını hatırlamak önemlidir. Her şeyde olduğu gibi, öğrenme %50 jargon ve %50 gerçek öğrenmedir.

Neden assembly dilinde yazıyoruz?
Multimedya işlemeyi hızlı hale getirmek için. Assembly kodu yazmaktan 10 kat veya daha fazla hız iyileştirmesi elde etmek çok yaygındır, bu özellikle videoları takılma olmadan gerçek zamanlı oynatmak istediğinizde önemlidir. Ayrıca enerji tasarrufu sağlar ve pil ömrünü uzatır. Video kodlama ve kod çözme fonksiyonlarının hem son kullanıcılar hem de büyük şirketlerin veri merkezlerinde dünyadaki en yoğun kullanılan fonksiyonlardan bazıları olduğunu belirtmek gerekir. Bu nedenle küçük bir iyileştirme bile hızla büyük sonuçlar doğurur.

Çevrimiçi olarak, insanların daha hızlı geliştirme için assembly talimatlarına eşlenen C benzeri fonksiyonlar olan intrinsic'ler kullandığını sık sık görürsünüz. FFmpeg'de intrinsic kullanmayız, bunun yerine assembly kodunu elle yazarız. Bu tartışmalı bir alan olmakla birlikte, intrinsic'ler genellikle derleyiciye bağlı olarak elle yazılmış assembly'den yaklaşık %10-15 daha yavaştır (intrinsic destekçileri buna katılmaz). FFmpeg için her parça ekstra performans yardımcı olur, bu yüzden assembly kodunu doğrudan yazarız. Ayrıca intrinsic'lerin "Hungarian Notation" kullanımları nedeniyle okunması zor olduğu argümanı da vardır.

Ayrıca FFmpeg'de tarihsel nedenlerle veya Linux Kernel gibi projelerde çok özel kullanım durumları nedeniyle birkaç yerde inline assembly (yani intrinsic kullanmayan) kalıntıları görebilirsiniz. Bu, assembly kodunun ayrı bir dosyada değil, C kodu ile satır içinde yazıldığı durumdur. FFmpeg gibi projelerdeki genel kanı, bu kodun okunması zor, derleyiciler tarafından yaygın olarak desteklenmeyen ve bakımı yapılamayan kod olduğudur.

Son olarak, çevrimiçi birçok sözde uzmanın bunların hiçbirinin gerekli olmadığını ve derleyicinin sizin için tüm bu "vektörleştirmeyi" yapabileceğini söylediğini göreceksiniz. En azından öğrenme amacıyla, onları görmezden gelin: örneğin dav1d projesi'ndeki son testler bu otomatik vektörleştirmeden yaklaşık 2 kat hızlanma gösterirken, elle yazılmış sürümler 8 kata ulaşabiliyordu.

Assembly dili çeşitleri
Bu dersler x86 64-bit assembly diline odaklanacak. Bu amd64 olarak da bilinir, ancak Intel CPU'larında da çalışır. ARM ve RISC-V gibi diğer CPU'lar için başka assembly türleri vardır ve potansiyel olarak gelecekte bu dersler bunları da kapsayacak şekilde genişletilebilir.

Çevrimiçi göreceğiniz x86 assembly sözdiziminin iki çeşidi vardır: AT&T ve Intel. AT&T Sözdizimi daha eskidir ve Intel sözdizimime kıyasla okunması daha zordur. Bu nedenle Intel sözdizimini kullanacağız.

Destekleyici materyaller
Kitapların veya Stack Overflow gibi çevrimiçi kaynakların referans olarak özellikle yardımcı olmadığını duymak sizi şaşırtabilir. Bu kısmen Intel sözdizimi ile elle yazılmış assembly kullanma seçimimizden kaynaklanır. Ayrıca birçok çevrimiçi kaynak, genellikle SIMD olmayan kod kullanarak işletim sistemi programlama veya donanım programlamaya odaklanır. FFmpeg assembly'si özellikle yüksek performanslı görüntü işlemeye odaklanır ve göreceğiniz gibi assembly programlamaya özellikle benzersiz bir yaklaşımdır. Bununla birlikte, bu dersleri tamamladıktan sonra diğer assembly kullanım durumlarını anlamak kolaydır.

Birçok kitap assembly öğretmeden önce çok sayıda bilgisayar mimarisi detayına girer. Öğrenmek istediğiniz bu ise bu iyidir, ancak bizim bakış açımızdan, araba kullanmayı öğrenmeden önce motorları incelemek gibidir.

Bununla birlikte, "The Art of 64-bit assembly" kitabının sonraki bölümlerinde SIMD talimatlarını ve davranışlarını görsel formda gösteren diyagramlar yardımcıdır: https://artofasm.randallhyde.com/

Soruları yanıtlamak için bir discord sunucusu mevcuttur:
https://discord.com/invite/Ks5MhUhqfB

Register'lar
Register'lar CPU'da verinin işlenebileceği alanlardır. CPU'lar doğrudan bellek üzerinde çalışmaz, bunun yerine veri register'lara yüklenir, işlenir ve belleğe geri yazılır. Assembly dilinde, genellikle, veriyi önce bir register'dan geçirmeden bir bellek konumundan diğerine doğrudan kopyalayamazsınız.

Genel Amaçlı Register'lar
İlk register türü Genel Amaçlı Register (GPR) olarak bilinir. GPR'lar genel amaçlı olarak anılır çünkü bu durumda 64-bit'e kadar bir değer veya bir bellek adresi (bir pointer) içerebilirler. Bir GPR'daki değer toplama, çarpma, kaydırma vb. işlemlerle işlenebilir.

Çoğu assembly kitabında, GPR'ların inceliklerine, tarihsel geçmişe vb. adanmış tüm bölümler vardır. Bunun nedeni GPR'ların işletim sistemi programlama, tersine mühendislik vb. söz konusu olduğunda önemli olmalarıdır. FFmpeg'de yazılan assembly kodunda, GPR'lar daha çok iskelet gibidir ve çoğu zaman karmaşıklıkları gerekli değildir ve soyutlanmıştır.

Vektör register'ları
Vektör (SIMD) register'ları, adından da anlaşılacağı gibi, birden fazla veri öğesi içerir. Çeşitli vektör register türleri vardır:

  • mm register'ları - MMX register'ları, 64-bit boyutunda, tarihsel ve artık pek kullanılmıyor
  • xmm register'ları - XMM register'ları, 128-bit boyutunda, yaygın olarak mevcut
  • ymm register'ları - YMM register'ları, 256-bit boyutunda, bunları kullanırken bazı komplikasyonlar var
  • zmm register'ları - ZMM register'ları, 512-bit boyutunda, sınırlı erişilebilirlik

Video sıkıştırma ve açmadaki hesaplamaların çoğu integer tabanlıdır, bu nedenle buna sadık kalacağız. İşte bir xmm register'ında 16 byte örneği:

a b c d e f g h i j k l m n o p

Ancak sekiz word (16-bit integer) olabilir

a b c d e f g h

Veya dört double word (32-bit integer)

a b c d

Veya iki quadword (64-bit integer):

a b

Özetlemek gerekirse:

  • byte'lar - 8-bit veri
  • word'ler - 16-bit veri
  • doubleword'ler - 32-bit veri
  • quadword'ler - 64-bit veri
  • double quadword'ler - 128-bit veri

Kalın harfler daha sonra önemli olacak.

x86inc.asm include
Birçok örnekte x86inc.asm dosyasını dahil ettiğimizi göreceksiniz. X86inc.asm, FFmpeg, x264 ve dav1d'de bir assembly programcısının hayatını kolaylaştırmak için kullanılan hafif bir soyutlama katmanıdır. Birçok şekilde yardımcı olur, ancak başlangıç için faydalı yaptığı şeylerden biri GPR'ları r0, r1, r2 olarak etiketlemesidir. Bu, herhangi bir register adını hatırlamanız gerekmediği anlamına gelir. Daha önce bahsedildiği gibi, GPR'lar genellikle sadece iskelet olduğu için bu hayatı çok kolaylaştırır.

Basit bir skalar asm parçacığı

Neler olup bittiğini görmek için basit (ve oldukça yapay) bir skalar asm parçacığına (her talimat içinde aynı anda bireysel veri öğeleri üzerinde çalışan assembly kodu) bakalım:

assembly
mov  r0q, 3  
inc  r0q  
dec  r0q  
imul r0q, 5

İlk satırda, immediate değer 3 (bellekten getirilen bir değerin aksine doğrudan assembly kodunun kendisinde saklanan bir değer) r0 register'ına quadword olarak saklanıyor. Intel sözdiziminde, kaynak operand (veriyi sağlayan değer veya konum, sağda yer alır) hedef operand'a (veriyi alan konum, solda yer alır) aktarılır, tıpkı memcpy davranışı gibi. Bunu "r0q = 3" olarak da okuyabilirsiniz, çünkü sıra aynıdır. r0'ın "q" eki register'ın quadword olarak kullanıldığını belirtir. inc değeri artırır böylece r0q 4 içerir, dec değeri 3'e geri azaltır. imul değeri 5 ile çarpar. Sonunda, r0q 15 içerir.

mov ve inc gibi makine koduna derleyici tarafından derlenen insan tarafından okunabilir talimatların mnemonik olarak bilindiğini unutmayın. Çevrimiçi ve kitaplarda MOV ve INC gibi büyük harflerle temsil edilen mnemonikleri görebilirsiniz ancak bunlar küçük harf sürümleri ile aynıdır. FFmpeg'de küçük harf mnemonikleri kullanırız ve büyük harfleri makrolar için saklı tutarız.

Temel bir vektör fonksiyonunu anlama

İşte ilk SIMD fonksiyonumuz:

assembly
%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

Satır satır gözden geçirelim:

assembly
%include "x86inc.asm"

Bu, assembly yazmayı basitleştirmek için yardımcılar, önceden tanımlanmış isimler ve makrolar (aşağıdaki cglobal gibi) sağlamak amacıyla x264, FFmpeg ve dav1d topluluklarında geliştirilen bir "header"dır.

assembly
SECTION .text

Bu, çalıştırmak istediğiniz kodun yerleştirildiği bölümü belirtir. Bu, sabit veri koyabileceğiniz .data bölümünün aksine.

assembly
;static void add_values(uint8_t *src, const uint8_t *src2)  
INIT_XMM sse2

İlk satır fonksiyon argümanının C'de nasıl göründüğünü gösteren bir yorumdur (assembly'de noktalı virgül ";" C'deki "//" gibidir). İkinci satır, paddb bir sse2 talimatı olduğu için sse2 talimat setini kullanarak XMM register'larını nasıl başlattığımızı gösterir. sse2'yi bir sonraki derste daha detaylı ele alacağız.

assembly
cglobal add_values, 2, 2, 2, src, src2

Bu, "add_values" adlı bir C fonksiyonu tanımladığı için önemli bir satırdır.

Her öğeyi tek tek gözden geçirelim:

  • Sonraki parametre iki fonksiyon argümanına sahip olduğunu gösterir.
  • Bundan sonraki parametre argümanlar için iki GPR kullanacağımızı gösterir. Bazı durumlarda daha fazla GPR kullanmak isteyebiliriz, bu nedenle x86util'e daha fazlasına ihtiyacımız olduğunu söylemek zorundayız.
  • Bundan sonraki parametre x86util'e kaç XMM register'ı kullanacağımızı söyler.
  • Takip eden iki parametre fonksiyon argümanları için etiketlerdir.

Eski kodların fonksiyon argümanları için etiket bulundurmayabileceğini, bunun yerine r0, r1 vb. kullanarak GPR'lara doğrudan hitap edebileceğini belirtmek gerekir.

assembly
movu  m0, [srcq]  
    movu  m1, [src2q]

movu, movdqu'nun (move double quad unaligned - hizalanmamış çift quad taşıma) kısaltmasıdır. Hizalama başka bir derste ele alınacak ancak şimdilik movu, [srcq]'den 128-bit taşıma olarak değerlendirilebilir. mov durumunda, köşeli parantezler [srcq]'deki adresin referans alındığı anlamına gelir, C'de *src'nin eşdeğeridir. Bu yükleme olarak bilinir. "q" ekinin pointer'ın boyutunu ifade ettiğini (yani C'de sizeof(*src) == 64-bit sistemlerde 8 ve x86asm 32-bit sistemlerde 32-bit kullanacak kadar akıllı) ancak temel yüklemenin 128-bit olduğunu unutmayın.

Vektör register'larını bu durumda xmm0 olan tam adlarıyla değil, soyutlanmış bir form olan m0 olarak andığımızı unutmayın. Gelecek derslerde bunun kod yazmayı bir kez yapıp birden fazla SIMD register boyutunda çalışmasını nasıl sağladığını göreceksiniz.

assembly
paddb m0, m1

paddb (bunu kafanızda p-add-b olarak okuyun) aşağıda gösterildiği gibi her register'daki her byte'ı topluyor. "p" ön eki "packed" (paketlenmiş) anlamına gelir ve vektör talimatları ile skalar talimatları tanımlamak için kullanılır. "b" eki bunun byte'lık toplama (byte toplamı) olduğunu gösterir.

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

Bu saklama olarak bilinir. Veri srcq pointer'ındaki adrese geri yazılır.

assembly
RET

Bu, fonksiyonun döndüğünü belirten bir makrodur. FFmpeg'deki assembly fonksiyonlarının neredeyse tamamı bir değer döndürmek yerine argümanlardaki veriyi değiştirir.

Ödevde göreceğiniz gibi, assembly fonksiyonlarına fonksiyon pointer'ları oluşturuyoruz ve mevcut olduklarında bunları kullanıyoruz.

Sonraki Ders


File: lesson_01/index.zh.md

[Binary file]


File: lesson_02/index.es.md

Lección Dos de Lenguaje Ensamblador con FFmpeg

Ahora que has escrito tu primera función en lenguaje ensamblador, vamos a introducir las ramificaciones (branches) y los bucles (loops).

Primero necesitamos presentar la idea de etiquetas (labels) y saltos (jumps). En el ejemplo artificial siguiente, la instrucción jmp hace que la ejecución salte a la instrucción que sigue a “.loop:”. “.loop:” se conoce como una etiqueta, y el punto delante del nombre indica que es una etiqueta local, lo que te permite reutilizar el mismo nombre de etiqueta en varias funciones. Este ejemplo, por supuesto, muestra un bucle infinito, pero más adelante lo ampliaremos con algo más realista.

assembly
mov  r0q, 3
.loop:
    dec  r0q
    jmp .loop

Antes de hacer un bucle realista, tenemos que introducir el registro FLAGS. No nos detendremos demasiado en los entresijos de FLAGS (de nuevo, porque las operaciones con GPR son mayormente estructurales), pero hay varias flags como la Zero-Flag, Sign-Flag y Overflow-Flag que se configuran en función del resultado de la mayoría de las instrucciones (excepto mov) sobre datos escalares, como las operaciones aritméticas y los desplazamientos.

Aquí tienes un ejemplo donde el contador del bucle cuenta hacia atrás hasta llegar a cero, y jg (salta si es mayor que cero) es la condición de bucle. dec r0q modifica los FLAGS en función del valor de r0q después de la instrucción, y puedes saltar en base a esos FLAGS.

assembly
mov  r0q, 3
.loop:
    ; hacer algo
    dec  r0q
    jg  .loop ; saltar si es mayor que cero

Esto es equivalente al siguiente código en C:

c
int i = 3;
do
{
   // hacer algo
   i--;
} while(i > 0)

Este código en C es un poco artificial. Normalmente un bucle en C se escribe así:

c
int i;
for(i = 0; i < 3; i++) {
    // hacer algo
}

Esto es aproximadamente equivalente a (no hay una forma directa de emparejar este bucle for):

assembly
xor r0q, r0q
.loop:
    ; hacer algo
    inc r0q
    cmp r0q, 3
    jl  .loop ; saltar si (r0q - 3) < 0, es decir, si (r0q < 3)

Hay varias cosas que destacar en este fragmento. La primera es xor r0q, r0q, que es una forma común de poner un registro a cero, y en algunos sistemas es más rápida que mov r0q, 0, porque, simplificando, no se realiza ninguna carga real. También se puede usar en registros SIMD con pxor m0, m0 para poner a cero todo un registro. Lo siguiente a observar es el uso de cmp. cmp resta efectivamente el segundo registro del primero (sin almacenar el resultado en ningún sitio) y configura los FLAGS, pero como se indica en el comentario, se puede leer junto con el salto (jl = saltar si menor que cero) para saltar si r0q < 3.

Fíjate cómo hay una instrucción extra (cmp) en este fragmento. En general, menos instrucciones significa código más rápido, por lo que se prefiere el fragmento anterior. Como verás en futuras lecciones, hay trucos adicionales para evitar esta instrucción extra y hacer que los FLAGS se configuren con una operación aritmética u otra instrucción. Observa que no estamos escribiendo ensamblador para que coincida exactamente con los bucles en C, sino para que sean lo más rápidos posible en ensamblador.

Aquí tienes algunos mnemónicos de salto comunes que acabarás usando (FLAGS incluidos por completitud, aunque no necesitas saber los detalles para escribir bucles):

Mnemónico Descripción FLAGS
JE/JZ Saltar si Igual/Cero ZF = 1
JNE/JNZ Saltar si No Igual/No Cero ZF = 0
JG/JNLE Saltar si Mayor/No Menor o Igual (con signo) ZF = 0 y SF = OF
JGE/JNL Saltar si Mayor o Igual/No Menor (con signo) SF = OF
JL/JNGE Saltar si Menor/No Mayor o Igual (con signo) SF ≠ OF
JLE/JNG Saltar si Menor o Igual/No Mayor (con signo) ZF = 1 o SF ≠ OF

Constantes

Veamos algunos ejemplos que muestran cómo usar constantes:

assembly
SECTION_RODATA

constants_1: db 1,2,3,4
constants_2: times 2 dw 4,3,2,1
  • SECTION_RODATA especifica que esta es una sección de datos de solo lectura. (Es un macro porque diferentes formatos de archivo de salida usados por los sistemas operativos lo declaran de forma diferente)
  • constants_1: La etiqueta constants_1 se define como db (declarar byte), es decir, equivalente a uint8_t constants_1[4] = {1, 2, 3, 4};
  • constants_2: Esto usa el macro times 2 para repetir las palabras declaradas, es decir, equivalente a uint16_t constants_2[8] = {4, 3, 2, 1, 4, 3, 2, 1};

Estas etiquetas, que el ensamblador convierte en direcciones de memoria, pueden usarse en cargas (pero no en escrituras, ya que son de solo lectura). Algunas instrucciones aceptan una dirección de memoria como operando, por lo que pueden usarse sin necesidad de cargar explícitamente en un registro (esto tiene ventajas e inconvenientes).

Offsets (Desplazamientos)

Los desplazamientos son la distancia (en bytes) entre elementos consecutivos en memoria. El desplazamiento lo determina el tamaño de cada elemento en la estructura de datos.

Ahora que sabemos escribir bucles, es hora de obtener datos. Pero hay algunas diferencias en comparación con C. Observemos el siguiente bucle en C:

c
uint32_t data[3];
int i;
for(i = 0; i < 3; i++) {
    data[i];
}

El desplazamiento de 4 bytes entre elementos de data lo calcula de antemano el compilador de C. Pero cuando escribes ensamblador a mano, debes calcular estos desplazamientos tú mismo.

Veamos la sintaxis para el cálculo de direcciones de memoria. Esto se aplica a todo tipo de direcciones:

assembly
[base + scale*index + disp]
  • base – Es un GPR (normalmente un puntero pasado como argumento desde C)
  • scale – Puede ser 1, 2, 4, 8. El valor por defecto es 1
  • index – Es un GPR (normalmente un contador de bucle)
  • disp – Es un entero (hasta 32 bits). El desplazamiento es un offset dentro de los datos

x86asm proporciona la constante mmsize, que indica el tamaño del registro SIMD con el que estás trabajando.

Aquí tienes un ejemplo simple (y sin sentido práctico) para ilustrar cómo cargar usando desplazamientos personalizados:

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]

     ; hacer algunas cosas

     add srcq, mmsize
dec r1q
jg .loop

RET

Fíjate cómo en movu m1, [srcq+2*r1q+3+mmsize], el ensamblador precalculará la constante de desplazamiento adecuada a usar. En la próxima lección te mostraremos un truco para evitar tener que hacer add y dec dentro del bucle, reemplazándolos por una sola instrucción add.

LEA

Ahora que entiendes los desplazamientos, puedes usar lea (Load Effective Address). Esto te permite realizar multiplicaciones y sumas en una sola instrucción, lo cual será más rápido que usar múltiples instrucciones. Por supuesto, hay limitaciones sobre con qué puedes multiplicar y qué puedes sumar, pero eso no impide que lea sea una instrucción muy potente.

assembly
lea r0q, [base + scale*index + disp]

Contrario a lo que sugiere su nombre, LEA se puede usar para aritmética normal además del cálculo de direcciones. Puedes hacer algo tan complicado como:

assembly
lea r0q, [r1q + 8*r2q + 5]

Ten en cuenta que esto no afecta al contenido de r1q ni de r2q. Tampoco afecta a los FLAGS (así que no puedes saltar en función del resultado). Usar LEA evita todas estas instrucciones y registros temporales (este código no es equivalente porque add sí modifica los FLAGS):

assembly
movq r0q, r1q
movq r3q, r2q
sal  r3q, 3 ; desplazamiento aritmético a la izquierda en 3 = * 8
add  r3q, 5
add  r0q, r3q

Verás lea usado a menudo para preparar direcciones antes de bucles o realizar cálculos como el anterior. Ten en cuenta, por supuesto, que no puedes hacer cualquier tipo de multiplicación y suma, pero multiplicaciones por 1, 2, 4, 8 y la suma de un desplazamiento fijo son muy comunes.

En el ejercicio tendrás que cargar una constante y sumar los valores a un vector SIMD dentro de un bucle.

Lección siguiente


File: lesson_02/index.fr.md

FFmpeg Assembly Language Leçon Deux

Maintenant que vous avez écrit votre première fonction en assembleur, nous allons vous présenter les branchements (branch) et les boucles (loop).

Dans un premier temps, nous devons vous introduire les notions de labels et de jumps (sauts). Regardons l'exemple suivant :

assembly
mov  r0q, 3
.loop:
    dec  r0q
    jmp .loop

L'instruction jmp déplace l'exécution du code après .loop:. .loop: est ce qu'on appelle un label. Le point (.) préfixant le label indique que ce label est un label local. Cela signifie que vous pouvez réutiliser le même nom de label dans plusieurs fonctions sans provoquer de conflits.

Cette portion de code est un exemple de boucle infinie, nous l'améliorerons dans la suite des leçons pour avoir un exemple beaucoup plus réaliste.

Avant de faire une boucle réaliste, nous devons introduire le registre FLAGS. Nous n'allons pas rentrer dans les détails complexes des FLAGS (car les opérations sur les GPR sont principalement des échafaudages) mais sachez qu'il existe plusieurs flags comme le Zero Flag (Z ou ZF, pour indicateur de zéro), le Sign Flag (SF, pour indicateur de signe), et l'Overflow Flag (OF, pour indicateur de débordement) qui sont un ensemble d'indicateurs basés sur la sortie de la majorité des instructions (à l'exception des instructions de type mov) sur des données scalaires comme des opérations arithmétiques et des décalages (shifts).

Voici un exemple où le compteur r0q de la boucle décrémente jusqu'à zéro et où jg (jump if greater than zero) est utilisé comme condition d'arrêt. L'instruction dec r0q met à jour les FLAGS en fonction des valeurs de r0q après chaque décrémentation et vous rebouclez jusqu'à ce que r0q atteigne zéro, moment où la boucle s'arrête.

assembly
mov  r0q, 3
.loop:
    ; do something
    dec  r0q
    jg  .loop ; jump if greater than zero

L'équivalent en C est le suivant :

c
int i = 3;
do
{
   // do something
   i--;
} while(i > 0)

Ce code en C est un peu contre-intuitif. Normalement, une boucle en C s'écrit de cette manière :

c
int i;
for(i = 0; i < 3; i++) {
    // do something
}

Les deux exemples sont quasiment équivalents à la portion en assembleur suivante (il n'y a pas de manière simple de trouver l'équivalent à cette boucle for):

assembly
xor r0q, r0q
.loop:
    inc r0q
    ; do something
    cmp r0q, 3
    jl  .loop ; jump if (r0q - 3) < 0, i.e (r0q < 3)

Plusieurs choses sont à examiner sur cet extrait de code. Dans un premier temps, xor r0q, r0q est une manière simple d'assigner un registre à zéro, ce qui est plus rapide sur certains systèmes que mov r0q, 0, parce qu'il n'y a aucun chargement de données.
Cela peut aussi être utilisé avec des registres SIMD avec la ligne pxor m0, m0 pour mettre à zéro un registre entier. La chose suivante à noter est l'utilisation de cmp. cmp peut effectivement soustraire le second registre du premier (sans avoir à sauvegarder la valeur quelque part) et met à jour les FLAGS, mais comme l'indique le commentaire, cela peut être lu avec l'instruction de saut jl (saut si inférieur à zéro) pour effectuer un saut si r0q < 3.

Notez la présence d'une instruction supplémentaire (````cmp```) dans ce court extrait. Généralement, peu d'instructions équivaut à du code plus rapide, c'est pourquoi l'exemple précédent est préféré. Comme vous le verrez plus tard, il existe plusieurs astuces pour éviter des instructions supplémentaires et faire en sorte que les FLAGS soient définis par une opération arithmétique ou une autre opération. Notez que nous n'écrivons pas de l'assembleur pour correspondre exactement aux boucles en C, nous écrivons des boucles en assembleur pour les rendre les plus rapides en assembleur.

Voici quelques mnémoniques de saut que vous finirez par utiliser (les registres FLAGS sont présentés là pour tout couvrir, mais vous n'aurez pas à tout connaître pour écrire des boucles) :

Mnémonique Signification Description FLAGS
JE/JZ Jump if Equal/Zero Saut si Égal / Zéro ZF = 1
JNE/JNZ Jump if Not Equal/Not Zero Saut si Non Égalité / Non Zéro ZF = 0
JG/JNLE Jump if Greater/Not Less or Equal (signed) Saut si Supériorité / Non Infériorité ou Égalité (signé) ZF = 0 and SF = OF
JGE/JNL Jump if Greater or Equal/Not Less (signed) Saut si Supériorité ou Égalité / Non Infériorité (signé) SF = OF
JL/JNGE Jump if Less/Not Greater or Equal (signed) Saut si Infériorité / Non Supériorité ou Égalité (signé) SF ≠ OF
JLE/JNG Jump if Less or Equal/Not Greater (signed) Saut si Infériorité ou Égalité / Non Supériorité (signé) ZF = 1 or SF ≠ OF

Constantes

Plongeons dans quelques exemples sur l'utilisation des constantes:

assembly
SECTION_RODATA

constants_1: db 1,2,3,4
constants_2: times 2 dw 4,3,2,1
  • SECTION_RODATA indique qu'il s'agit d'une section en lecture seule. (C'est une macro car les différents formats de fichier de sortie utilisés par les systèmes d'exploitation les déclarent différemment.)
  • le label constants_1 est défini comme étant un db (declared bytes), c'est équivalent à uint8_t constants_1[4] = {1, 2, 3, 4};
  • constants_2 utilise la macro times 2 pour répéter la définition de mot (dw pour declared word), c'est équivalent à uint16_t constants_2[8] = {4, 3, 2, 1, 4, 3, 2, 1};

Ces labels, que l'assembleur convertit en adresse mémoire, peuvent être utilisés pour les chargements (mais pas pour des stockages car ils sont en lecture seule). Quelques instructions acceptent une adresse mémoire comme opérande, ce qui permet de les utiliser sans chargement explicite dans un registre (il y a des avantages et des inconvénients à ceci).

Offsets (déplacements)

Les offsets (déplacements) sont les distances (en bytes) entre deux éléments consécutifs en mémoire. L'offset est déterminé par la taille de chaque élément dans la structure de données.

Maintenant que nous sommes capables d'écrire des boucles, il est temps de récupérer des données. Mais il existe des différences par rapport au C. Regardons la boucle suivante en C:

c
uint32_t data[3];
int i;
for(i = 0; i < 3; i++) {
    data[i];
}

L'offset de 4 bytes entre chaque élément de données est pré-calculé par le compilateur. Mais en l'écrivant à la main en assembleur, vous devrez calculer cet offset vous-même.

Regardons la syntaxe du calcul des adresses mémoires. Cela s'applique à tout type d'adresses mémoires:

assembly
[base + scale*index + disp]
  • base - Il s'agit d'un GPR (généralement un pointeur provenant d'un argument d'une fonction en C).
  • scale - Entier valant 1, 2, 4, 8. La valeur par défaut est 1.
  • index - un registre GPR (le compteur d'une boucle).
  • disp - Entier (stocké jusqu'à au plus 32 bits). disp est un décalage dans les données.

x86asm fournit la constante mmsize, qui vous indique la taille du registre SIMD avec lequel vous travaillez.

Voici un simple (et pas très logique) exemple d'illustration du chargement avec des déplacements différents:

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]

     ; do some things

     add srcq, mmsize
dec r1q
jg .loop

RET

Notez comment dans movu m1, [srcq+2*r1q+3+mmsize] l'assembleur pré-calculera le déplacement correct à utiliser. Dans la prochaine leçon, nous vous présenterons une astuce pour éviter d'avoir add et dec dans la boucle, en les remplaçant par un unique add.

LEA

Maintenant que vous maîtrisez les offsets, vous êtes capable d'utiliser l'instruction lea (*_**L**oad Effective Address_*). Avec une seule instruction, vous êtes capable de faire une multiplication et une addition, ce qui est alors plus rapide que l'utilisation de plusieurs instructions. Il y a, bien sûr, des limitations sur les données que vous pouvez multiplier et ajouter mais cela n'empêche pas lea d'être une instruction puissante.

assembly
lea r0q, [base + scale*index + disp]

Contrairement à son nom, LEA peut être utilisée autant pour des opérations arithmétiques que pour des calculs d'adresses mémoires. Vous pouvez faire quelque chose d'aussi compliqué que:

assembly
lea r0q, [r1q + 8*r2q + 5]

Il est important de noter que cela n'affecte pas le contenu de r1q et r2q. Cela n'impacte pas non plus les registres FLAGS (par conséquent, vous ne pouvez pas effectuer de saut sur la sortie). L'utilisation des LEA évite toutes ces instructions et les registres temporaires (ce code n'a pas d'équivalent car add modifie les FLAGS):

assembly
movq r0q, r1q
movq r3q, r2q
sal  r3q, 3 ; shift arithmetic left 3 = * 8
add  r3q, 5
add  r0q, r3q

Vous verrez l'utilisation de lea dans le calcul de beaucoup d'adresses avant des boucles ou pour faire des calculs comme le précédent. Notez bien que vous ne pouvez pas faire tous types de multiplications et d'additions, mais les multiplications par 1, 2, 4 ou 8 et les additions d'un décalage fixe sont des opérations courantes.

Dans l'exercice, vous aurez à charger une constante et à en additionner les valeurs à un vecteur SIMD dans une boucle.

Leçon suivante


File: lesson_02/index.md

FFmpeg Assembly Language Lesson Two

Now that you’ve written your first assembly language function, we will now introduce branches and loops.

We need to first introduce the idea of labels and jumps. In the artificial example below, the jmp instruction moves the code instruction to after “.loop:”. “.loop:” is known as a label, with the dot prefixing the label meaning it’s a local label, effectively allowing you to reuse the same label name across multiple functions. This example, of course, shows an infinite loop, but we’ll extend this later to something more realistic.

assembly
mov  r0q, 3
.loop:
    dec  r0q
    jmp .loop

Before making a realistic loop we have to introduce the FLAGS register. We won’t dwell on the intricacies of FLAGS too much (again because GPR operations are largely scaffolding) but there are several flags such as Zero-Flag, Sign-Flag and Overflow-Flag which are set based on the output of most non-mov instructions on scalar data such as arithmetic operations and shifts.

Here’s an example where the loop counter counts down until zero and jg (jump if greater than zero) is the loop condition. dec r0q sets the FLAGs based on the value of r0q after the instruction and you can jump based on them.

assembly
mov  r0q, 3
.loop:
    ; do something
    dec  r0q
    jg  .loop ; jump if greater than zero

This is equivalent to the following C code:

c
int i = 3;
do
{
   // do something
   i--;
} while(i > 0)

This C code is a bit unnatural. Usually a loop in C is written like this:

c
int i;
for(i = 0; i < 3; i++) {
    // do something
}

This is roughly equivalent to (there's no simple way of matching this for loop):

assembly
xor r0q, r0q
.loop:
    ; do something
    inc r0q
    cmp r0q, 3
    jl  .loop ; jump if (r0q - 3) < 0, i.e (r0q < 3)

There are several things to point out in this snippet. First is xor r0q, r0q which is a common way of setting a register to zero, that on some systems is faster than mov r0q, 0, because, put simply, there is no actual load taking place. It can also be used on SIMD registers with pxor m0, m0 to zero out an entire register. The next thing to note is the use of cmp. cmp effectively subtracts the second register from the first (without storing the value anywhere) and sets FLAGS, but as per the comment, it can be read together with the jump, (jl = jump if less than zero) to jump if r0q < 3.

Note how there is one extra instruction (cmp) in this snippet. Generally speaking, fewer instructions means faster code, which is why the earlier snippet is preferred. As you’ll see in future lessons, there are more tricks used to avoid this extra instruction and have FLAGS be set by arithmetic or another operation. Note how we are not writing assembly to match C loops exactly, we write loops to make them as fast as possible in assembly.

Here are some common jump mnemonics you’ll end up using (FLAGS are there for completeness, but you don’t need to know the specifics to write loops):

Mnemonic Description FLAGS
JE/JZ Jump if Equal/Zero ZF = 1
JNE/JNZ Jump if Not Equal/Not Zero ZF = 0
JG/JNLE Jump if Greater/Not Less or Equal (signed) ZF = 0 and SF = OF
JGE/JNL Jump if Greater or Equal/Not Less (signed) SF = OF
JL/JNGE Jump if Less/Not Greater or Equal (signed) SF ≠ OF
JLE/JNG Jump if Less or Equal/Not Greater (signed) ZF = 1 or SF ≠ OF

Constants

Let’s look at some examples showing how to use constants:

assembly
SECTION_RODATA

constants_1: db 1,2,3,4
constants_2: times 2 dw 4,3,2,1
  • SECTION_RODATA specifies this is a read-only data section. (This is a macro because different output file formats that operating systems use declare this differently)
  • constants_1: The label constants_1, is defined as db (declare byte) - i.e equivalent to uint8_t constants_1[4] = {1, 2, 3, 4};
  • constants_2: This uses the times 2 macro to repeat the declared words - i.e equivalent to uint16_t constants_2[8] = {4, 3, 2, 1, 4, 3, 2, 1};

These labels, which the assembler converts to a memory address, can then be used in loads (but not stores as they are read-only). Some instructions take a memory address as an operand so they can be used without explicit loads into a register (there are pros and cons to this).

Offsets

Offsets are the distance (in bytes) between consecutive elements in memory. The offset is determined by the size of each element in the data structure.

Now that we're able to write loops, it’s time to fetch data. But there are some differences compared to C. Let’s look at the following loop in C:

c
uint32_t data[3];
int i;
for(i = 0; i < 3; i++) {
    data[i];
}

The 4-byte offset between elements of data is precalculated by the C compiler. But when handwriting assembly you need to calculate these offsets yourself.

Let’s look at the syntax for memory address calculations. This applies to all types of memory addresses:

assembly
[base + scale*index + disp]
  • base - This is a GPR (usually a pointer from a C function argument)
  • scale - This can be 1, 2, 4, 8. 1 is the default
  • index - This is a GPR (usually a loop counter)
  • disp - This is an integer (up to 32-bit). Displacement is an offset into the data

x86asm provides the constant mmsize, which lets you know the size of the SIMD register you are working with.

Here’s a simple (and nonsensical) example to illustrate loading from custom offsets:

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]

     ; do some things

     add srcq, mmsize
dec r1q
jg .loop

RET

Note how in movu m1, [srcq+2*r1q+3+mmsize] the assembler will precalculate the right displacement constant to use. In the next lesson we’ll show you a trick to avoid having to do add and dec in the loop, replacing them with a single add.

LEA

Now that you understand offsets you are able to use lea (Load Effective Address). This lets you perform multiplication and addition with one instruction, which is going to be faster than using multiple instructions. There are, of course, limitations on what you can multiply by and add to but this does not stop lea being a powerful instruction.

assembly
lea r0q, [base + scale*index + disp]

Contrary to the name, LEA can be used for normal arithmetic as well as address calculations. You can do something as complicated as:

assembly
lea r0q, [r1q + 8*r2q + 5]

Note that this does not affect the contents of r1q and r2q. It also doesn’t affect FLAGS (so you can’t jump based on the output). Using LEA avoids all these instructions and temporary registers (this code is not equivalent because add changes FLAGS):

assembly
movq r0q, r1q
movq r3q, r2q
sal  r3q, 3 ; shift arithmetic left 3 = * 8
add  r3q, 5
add  r0q, r3q

You’ll see lea used a lot to set up addresses before loops or perform calculations like the above. Note of course, that you can’t do all types of multiply and addition, but multiplications by 1, 2, 4, 8 and addition of a fixed offset is common.

In the assignment you’ll have to load a constant and add the values to a SIMD vector in a loop.

Next Lesson


File: lesson_02/index.tr.md

FFmpeg Assembly Dili İkinci Ders

Artık ilk assembly dili fonksiyonunuzu yazdığınıza göre, şimdi dallanma ve döngüleri tanıtacağız.

Önce etiket ve atlama fikrini tanıtmamız gerekiyor. Aşağıdaki yapay örnekte, jmp talimatı kod talimatını ".loop:" dan sonrasına taşır. ".loop:" etiket olarak bilinir, etiketin önündeki nokta bunun yerel etiket olduğu anlamına gelir, bu da aynı etiket adını birden fazla fonksiyon boyunca yeniden kullanmanıza olanak tanır. Bu örnek tabii ki sonsuz bir döngü gösteriyor, ancak bunu daha sonra daha gerçekçi bir şeye genişleteceğiz.

assembly
mov  r0q, 3
.loop:
    dec  r0q
    jmp .loop

Gerçekçi bir döngü yapmadan önce FLAGS register'ını tanıtmamız gerekiyor. FLAGS'ın inceliklerine çok fazla dalacağımız yok (yine GPR işlemleri büyük ölçüde iskelet olduğu için) ancak Sıfır-Bayrağı, İşaret-Bayrağı ve Taşma-Bayrağı gibi birkaç bayrak vardır ve bunlar skalar veri üzerinde aritmetik işlemler ve kaydırmalar gibi mov olmayan talimatların çıktısına göre ayarlanır.

İşte döngü sayacının sıfıra kadar geriye saydığı ve jg'nin (sıfırdan büyükse atla) döngü koşulu olduğu bir örnek. dec r0q talimat sonrasında r0q değerine göre FLAGS'ı ayarlar ve bunlara göre atlayabilirsiniz.

assembly
mov  r0q, 3
.loop:
    ; bir şey yap
    dec  r0q
    jg  .loop ; sıfırdan büyükse atla

Bu aşağıdaki C koduna eşdeğerdir:

c
int i = 3;
do
{
   // bir şey yap
   i--;
} while(i > 0)

Bu C kodu biraz doğal değil. Genellikle C'de bir döngü şöyle yazılır:

c
int i;
for(i = 0; i < 3; i++) {
    // bir şey yap
}

Bu kabaca şuna eşdeğerdir (bu for döngüsünü eşleştirmenin basit bir yolu yoktur):

assembly
xor r0q, r0q
.loop:
    ; bir şey yap
    inc r0q
    cmp r0q, 3
    jl  .loop ; (r0q - 3) < 0 ise atla, yani (r0q < 3)

Bu parçacıkta işaret edilecek birkaç şey vardır. İlki xor r0q, r0q olup, bu bir register'ı sıfıra ayarlamanın yaygın bir yoludur ve bazı sistemlerde mov r0q, 0'dan daha hızlıdır, çünkü basitçe söylemek gerekirse gerçek bir yükleme gerçekleşmez. pxor m0, m0 ile SIMD register'larında tüm register'ı sıfırlamak için de kullanılabilir. Dikkat edilecek bir diğer şey ise cmp kullanımıdır. cmp etkili olarak ikinci register'ı birinciden çıkarır (değeri herhangi bir yerde saklamadan) ve FLAGS'ı ayarlar, ancak yorumda da belirtildiği gibi, r0q < 3 ise atlamak için atlama ile birlikte okunabilir (jl = sıfırdan küçükse atla).

Bu parçacıkta bir ekstra talimat (cmp) olduğuna dikkat edin. Genel olarak, daha az talimat daha hızlı kod anlamına gelir, bu yüzden önceki parçacık tercih edilir. Gelecek derslerde göreceğiniz gibi, bu ekstra talimatı önlemek ve FLAGS'ın aritmetik veya başka bir işlemle ayarlanmasını sağlamak için daha fazla numara kullanılır. Assembly'de C döngülerini tam olarak eşleştirmek için assembly yazmadığımıza, assembly'de mümkün olduğunca hızlı hale getirmek için döngüler yazdığımıza dikkat edin.

İşte kullanacağınız bazı yaygın atlama mnemonikleri (FLAGS eksiksizlik için oradadır, ancak döngü yazmak için ayrıntıları bilmenize gerek yoktur):

Mnemonic Açıklama FLAGS
JE/JZ Eşitse/Sıfırsa Atla ZF = 1
JNE/JNZ Eşit Değilse/Sıfır Değilse Atla ZF = 0
JG/JNLE Büyükse/Küçük veya Eşit Değilse Atla (işaretli) ZF = 0 and SF = OF
JGE/JNL Büyük veya Eşitse/Küçük Değilse Atla (işaretli) SF = OF
JL/JNGE Küçükse/Büyük veya Eşit Değilse Atla (işaretli) SF ≠ OF
JLE/JNG Küçük veya Eşitse/Büyük Değilse Atla (işaretli) ZF = 1 or SF ≠ OF

Sabitler

Sabitlerin nasıl kullanılacağını gösteren bazı örneklere bakalım:

assembly
SECTION_RODATA

constants_1: db 1,2,3,4
constants_2: times 2 dw 4,3,2,1
  • SECTION_RODATA bunun salt okunur veri bölümü olduğunu belirtir. (Bu bir makrodur çünkü işletim sistemlerinin kullandığı farklı çıktı dosya formatları bunu farklı şekilde beyan eder)
  • constants_1: constants_1 etiketi db (declare byte - byte beyan et) olarak tanımlanır - yani uint8_t constants_1[4] = {1, 2, 3, 4}; eşdeğeri
  • constants_2: Bu, beyan edilen word'leri tekrarlamak için times 2 makrosunu kullanır - yani uint16_t constants_2[8] = {4, 3, 2, 1, 4, 3, 2, 1}; eşdeğeri

Derleyicinin bir bellek adresine dönüştürdüğü bu etiketler daha sonra yüklemelerde kullanılabilir (salt okunur oldukları için saklamalarda değil). Bazı talimatlar operand olarak bir bellek adresi alır, bu nedenle register'a açık yükleme olmadan kullanılabilirler (bunun artıları ve eksileri vardır).

Offset'ler

Offset'ler bellekteki ardışık öğeler arasındaki mesafedir (byte olarak). Offset, veri yapısındaki her öğenin boyutu ile belirlenir.

Artık döngü yazabildiğimize göre, veri almaya zaman. Ancak C ile karşılaştırıldığında bazı farklar var. Şu C döngüsüne bakalım:

c
uint32_t data[3];
int i;
for(i = 0; i < 3; i++) {
    data[i];
}

Veri öğeleri arasındaki 4-byte offset C derleyicisi tarafından önceden hesaplanır. Ancak assembly'yi elle yazarken bu offset'leri kendiniz hesaplamanız gerekir.

Bellek adresi hesaplamaları için sözdizimini inceleyelim. Bu her türlü bellek adresi için geçerlidir:

assembly
[base + scale*index + disp]
  • base - Bu bir GPR'dır (genellikle bir C fonksiyon argümanından gelen pointer)
  • scale - Bu 1, 2, 4, 8 olabilir. 1 varsayılandır
  • index - Bu bir GPR'dır (genellikle bir döngü sayacı)
  • disp - Bu bir integer'dır (32-bit'e kadar). Displacement veri içine bir offset'tir

x86asm, çalıştığınız SIMD register'ının boyutunu bilmenizi sağlayan mmsize sabitini sağlar.

Özel offset'lerden yüklemeyi göstermek için basit (ve anlamsız) bir örnek:

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]

     ; bazı şeyler yap

     add srcq, mmsize
dec r1q
jg .loop

RET

movu m1, [srcq+2*r1q+3+mmsize]'de derleyicinin kullanılacak doğru displacement sabitini önceden hesaplayacağına dikkat edin. Bir sonraki derste döngüde add ve dec yapmaktan kaçınma numarasını göstereceğiz, bunları tek bir add ile değiştireceğiz.

LEA

Artık offset'leri anladığınıza göre lea (Load Effective Address) kullanabilirsiniz. Bu, çarpma ve toplamayı tek talimat ile yapmanıza olanak tanır, bu da birden fazla talimat kullanmaktan daha hızlı olacaktır. Tabii ki çarpabileceğiniz ve toplayabileceğiniz şeylerde sınırlamalar vardır ancak bu lea'nın güçlü bir talimat olmasını engellemez.

assembly
lea r0q, [base + scale*index + disp]

Adına rağmen, LEA normal aritmetik için ve adres hesaplamaları için kullanılabilir. Şu kadar karmaşık bir şey yapabilirsiniz:

assembly
lea r0q, [r1q + 8*r2q + 5]

Bunun r1q ve r2q içeriklerini etkilemediğine dikkat edin. Ayrıca FLAGS'ı da etkilemez (bu nedenle çıktıya göre atlayamazsınız). LEA kullanmak tüm bu talimatları ve geçici register'ları önler (bu kod eşdeğer değildir çünkü add FLAGS'ı değiştirir):

assembly
movq r0q, r1q
movq r3q, r2q
sal  r3q, 3 ; shift arithmetic left 3 = * 8
add  r3q, 5
add  r0q, r3q

lea'nın döngülerden önce adresler ayarlamak veya yukarıdaki gibi hesaplamalar yapmak için çok kullanıldığını göreceksiniz. Tabii ki her türlü çarpma ve toplama yapamazsınız, ancak 1, 2, 4, 8 ile çarpmalar ve sabit bir offset toplamı yaygındır.

Ödevde bir sabit yüklemeniz ve değerleri bir döngüde bir SIMD vektörüne eklemeniz gerekecek.

Sonraki Ders


File: lesson_02/index.zh.md

[Binary file]


File: lesson_03/index.es.md

Lección Tres de Ensamblador con FFmpeg

Vamos a explicar algo más de jerga y darte una breve lección de historia.

Conjuntos de instrucciones

Como viste en la lección anterior, hablamos de SSE2, que es un conjunto de instrucciones SIMD. Cuando se lanza una nueva generación de CPU, puede venir con nuevas instrucciones y, a veces, tamaños de registro más grandes. La historia del conjunto de instrucciones x86 es muy compleja, así que aquí tienes una versión simplificada (hay muchas más subcategorías):

  • MMX - Lanzado en 1997, primer SIMD en procesadores Intel, registros de 64 bits, histórico
  • SSE (Streaming SIMD Extensions) - Lanzado en 1999, registros de 128 bits
  • SSE2 - Lanzado en 2000, muchas instrucciones nuevas
  • SSE3 - Lanzado en 2004, primeras instrucciones horizontales
  • SSSE3 (Supplemental SSE3) - Lanzado en 2006, nuevas instrucciones, pero lo más importante es la instrucción de barajado pshufb, probablemente la más importante en el procesamiento de vídeo
  • SSE4 - Lanzado en 2008, muchas instrucciones nuevas incluyendo mínimo y máximo empaquetados
  • AVX - Lanzado en 2011, registros de 256 bits (sólo float) y nueva sintaxis de tres operandos
  • AVX2 - Lanzado en 2013, registros de 256 bits para instrucciones enteras
  • AVX512 - Lanzado en 2017, registros de 512 bits, nueva funcionalidad de máscaras de operación. Tuvieron uso limitado en FFmpeg en su momento debido a la bajada de frecuencia del CPU al usar nuevas instrucciones. Barajado completo de 512 bits (permute) con vpermb
  • AVX512ICL - Lanzado en 2019, sin reducción de frecuencia del reloj
  • AVX10 - Próximamente

Vale la pena señalar que los conjuntos de instrucciones pueden eliminarse además de añadirse en las CPU. Por ejemplo, AVX512 fue eliminado (de forma polémica) en las CPU Intel de 12.ª generación. Por esta razón, FFmpeg realiza detección de CPU en tiempo de ejecución. FFmpeg detecta las capacidades de la CPU en la que se está ejecutando.

Como viste en el ejercicio, los punteros a funciones son por defecto en C y se reemplazan por una variante del conjunto de instrucciones específico. Esto significa que la detección se hace una sola vez y no necesita repetirse. Esto contrasta con muchas aplicaciones propietarias que codifican un conjunto de instrucciones fijo, haciendo que un ordenador perfectamente funcional quede obsoleto. Esto también permite activar/desactivar funciones optimizadas en tiempo de ejecución. Esta es una de las grandes ventajas del software libre.

Programas como FFmpeg se usan en miles de millones de dispositivos en todo el mundo, algunos de los cuales pueden ser muy antiguos. Técnicamente, FFmpeg admite máquinas que solo soportan SSE, ¡que tienen 25 años! Por suerte, x86inc.asm puede avisarte si utilizas una instrucción no disponible en un conjunto concreto.

Para que tengas una idea de las capacidades reales, aquí está la disponibilidad de conjuntos de instrucciones según la encuesta de Steam en noviembre de 2024 (obviamente sesgada hacia jugadores):

Conjunto de Instrucciones Disponibilidad
SSE2 100%
SSE3 100%
SSSE3 99.86%
SSE4.1 99.80%
AVX 97.39%
AVX2 94.44%
AVX512 (Steam no distingue entre AVX512 y AVX512ICL) 14.09%

Para una aplicación como FFmpeg con miles de millones de usuarios, incluso un 0,1% es un número muy grande de usuarios e informes de errores si algo falla. FFmpeg tiene una infraestructura de pruebas muy amplia para verificar las variaciones de CPU/SO/Compilador en nuestra suite de pruebas FATE. Cada commit se ejecuta en cientos de máquinas para asegurarse de que nada se rompe.

Intel proporciona un manual detallado de conjuntos de instrucciones aquí: https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html

Puede ser tedioso buscar en un PDF, así que existe una alternativa web no oficial aquí: https://www.felixcloutier.com/x86/

También hay una representación visual de instrucciones SIMD disponible aquí:
https://www.officedaytime.com/simd512e/

Parte del reto del ensamblador x86 es encontrar la instrucción adecuada para lo que necesitas. En algunos casos, las instrucciones pueden usarse de formas distintas a las previstas originalmente.

Trucos con desplazamientos de punteros

Volvamos a nuestra función original de la Lección 1, pero ahora añadimos un argumento de anchura (width) a la función en C.

Usamos ptrdiff_t para la variable width en lugar de int para asegurarnos de que los 32 bits superiores del argumento de 64 bits están a cero. Si pasásemos directamente un int como ancho en la firma de la función y luego intentásemos usarlo como un quad para aritmética de punteros (es decir, usando widthq), los 32 bits superiores del registro podrían contener valores arbitrarios. Podríamos solucionar esto con movsxd para extender el signo (ver también el macro movsxdifnidn en x86inc.asm), pero esta forma es más sencilla.

La siguiente función contiene el truco de desplazamiento de punteros:

assembly
;static void add_values(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

Veámoslo paso a paso porque puede resultar confuso:

assembly
add srcq, widthq
   add src2q, widthq
   neg widthq

La anchura se suma a cada puntero, de modo que ahora apuntan al final del búfer a procesar. Luego, width se niega.

assembly
movu  m0, [srcq+widthq]
    movu  m1, [src2q+widthq]

Las cargas se hacen con widthq siendo negativo. Así que en la primera iteración, [srcq+widthq] apunta a la dirección original de srcq, es decir, al inicio del búfer.

assembly
add   widthq, mmsize
    jl .loop

Se suma mmsize a widthq (negativo), acercándolo a cero. La condición del bucle ahora es jl (saltar si menor que cero). Este truco hace que widthq se use como desplazamiento de puntero y como contador de bucle al mismo tiempo, ahorrando una instrucción cmp. También permite usar el desplazamiento en múltiples cargas y escrituras, e incluso en múltiplos del desplazamiento si es necesario (recuerda esto para el ejercicio).

Alineación

En todos nuestros ejemplos hemos usado movu para evitar el tema de la alineación. Muchas CPU pueden cargar y almacenar datos más rápido si los datos están alineados, es decir, si la dirección de memoria es divisible por el tamaño del registro SIMD. Cuando es posible, en FFmpeg intentamos usar cargas y escrituras alineadas con mova.

En FFmpeg, av_malloc puede proporcionar memoria alineada en el heap, y la directiva del preprocesador C DECLARE_ALIGNED puede hacerlo en la stack. Si se usa mova con una dirección no alineada, causará un fallo de segmentación y la aplicación se cerrará. También es importante asegurarse de que el valor de alineación corresponde al tamaño del registro SIMD: 16 con xmm, 32 con ymm y 64 con zmm.

Así es como se alinea el inicio de la sección RODATA a 64 bytes:

assembly
SECTION_RODATA 64

Ten en cuenta que esto sólo alinea el comienzo de RODATA. Puede ser necesario añadir bytes de relleno para que la siguiente etiqueta siga alineada a 64 bytes.

Expansión de rango

Otro tema que hemos evitado hasta ahora es el desbordamiento. Esto ocurre, por ejemplo, cuando el valor de un byte supera 255 tras una operación como suma o multiplicación. Puede que queramos realizar una operación donde el valor intermedio sea mayor que un byte (por ejemplo, word), o incluso mantener los datos en ese tamaño mayor.

Para bytes sin signo, aquí entran punpcklbw (desempaqueta bytes bajos en words) y punpckhbw (desempaqueta bytes altos en words).

Veamos cómo funciona punpcklbw. La sintaxis para la versión SSE2 según el manual de Intel es:

PUNPCKLBW xmm1, xmm2/m128

Esto significa que la fuente (lado derecho) puede ser un registro xmm o una dirección de memoria (m128 = dirección de memoria con la sintaxis estándar [base + scale*index + disp]), y el destino un registro xmm.

El sitio web de officedaytime.com tiene un buen diagrama que muestra lo que ocurre:

Puedes ver que los bytes se intercalan desde la mitad inferior de cada registro respectivamente. ¿Pero qué tiene que ver esto con la extensión de rango? Si el registro fuente está lleno de ceros, se intercalan los bytes del destino con ceros. Esto se conoce como extensión con ceros, ya que los bytes son sin signo. punpckhbw puede usarse para hacer lo mismo con los bytes altos.

Aquí tienes un fragmento que muestra cómo se hace:

assembly
pxor      m2, m2 ; poner m2 a cero

movu      m0, [srcq]
movu      m1, m0 ; copiar m0 en m1
punpcklbw m0, m2
punpckhbw m1, m2

m0 y m1 ahora contienen los bytes originales extendidos con ceros a word. En la próxima lección verás cómo las instrucciones de tres operandos en AVX hacen innecesaria la segunda movu.

Extensión de signo

Los datos con signo son un poco más complejos. Para extender el rango de un entero con signo, necesitamos usar un proceso conocido como extensión de signo. Esto rellena los bits más significativos (MSB) con el bit de signo. Por ejemplo: -2 en int8_t es 0b11111110. Para extenderlo a int16_t, se repite el MSB (1), dando 0b1111111111111110.

pcmpgtb (comparación empaquetada mayor que, por byte) puede usarse para extensión de signo. Al hacer la comparación (0 > byte), todos los bits del byte destino se ponen a 1 si el byte es negativo, y a 0 si no lo es. Luego punpckX puede usarse como antes para extender el signo. Si el byte es negativo, el correspondiente es 0b11111111, y si no, es 0x00000000. Intercalar el byte original con el resultado de pcmpgtb extiende el signo a word como resultado.

assembly
pxor      m2, m2 ; poner m2 a cero

movu      m0, [srcq]
movu      m1, m0 ; copiar m0 en m1

pcmpgtb   m2, m0
punpcklbw m0, m2
punpckhbw m1, m2

Como puedes ver, hay una instrucción extra comparado con el caso sin signo.

Empaquetado

packuswb (empaquetar word sin signo a byte) y packsswb permiten ir de word a byte. Permiten intercalar dos registros SIMD que contienen word en un solo registro SIMD de byte. Ten en cuenta que si los valores superan el rango de byte, se saturan (es decir, se limitan al valor máximo).

Shuffles (barajados)

Los shuffles, también conocidos como permutaciones, son posiblemente la instrucción más importante en procesamiento de vídeo, y pshufb (barajado empaquetado de bytes), disponible en SSSE3, es su variante más importante.

Para cada byte, el byte fuente correspondiente se usa como índice del registro destino, excepto cuando el MSB está activado: entonces el byte destino se pone a cero. Es análogo al siguiente código en C (aunque en SIMD las 16 iteraciones ocurren en paralelo):

c
uint8_t tmp[16];
memcpy(tmp, dst, 16);
for(int i = 0; i < 16; i++) {
    if(src[i] & 0x80)
        dst[i] = 0;
    else
        dst[i] = tmp[src[i]];
}

Aquí tienes un ejemplo simple en ensamblador:

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 ; baraja m0 según m1

Fíjate que se usa -1 para facilitar la lectura como índice de barajado que pone a cero el byte de salida: -1 como byte es el campo de bits 0b11111111 (complemento a dos), y por tanto el MSB (0x80) está activado.


File: lesson_03/index.fr.md

FFmpeg Assembly Language Leçon Trois

Expliquons un peu plus de jargon et lisez bien cette petite leçon d'histoire.

Jeux d'instructions

Vous avez sûrement prêté attention au fait que dans la leçon précédente, nous avons parlé de SSE2 qui fait partie du jeu d'instructions SIMD. Quand une nouvelle génération de CPU sort sur le marché, elle peut être accompagnée de nouvelles instructions et quelquefois de registres de tailles plus grandes. L'histoire du jeu d'instruction x86 est tellement complexe qu'il en existe une version simplifiée (avec plusieurs sous-histoires dans celle-ci) :

  • MMX - Lancée en 1997, la première version SIMD chez Intel Processors, registres de 64 bits, version historique.
  • SSE (Streaming SIMD Extensions) - Lancée en 1999, registres de 128 bits.
  • SSE2 - Lancée en 2000, plusieurs nouvelles instructions.
  • SSE3 - Lancée en 2004, premières instructions horizontales.
  • SSSE3 (Supplemental SSE3) - Lancée en 2006, ajout de nouvelles instructions dont la plus importante pshufb, sans doute l'instruction la plus importante pour le traitement vidéo.
  • SSE4 - Lancée en 2008, ajout de nombreuses nouvelles instructions, notamment les minimums et maximums en mode vectorisé.
  • AVX - Lancée en 2011, ajout de registres à 256 bits (uniquement pour les flottants) et d'une nouvelle syntaxe à trois opérandes.
  • AVX2 - Lancée en 2013, ajout de registres à 256 bits pour les instructions entières.
  • AVX512 - Lancée en 2017, registres à 512 bits, nouvelle instruction de masquage. Leur utilisation dans FFmpeg était très limitée en raison de la diminution de la fréquence du CPU quand de nouvelles instructions étaient utilisées. Permutation complète de 512 bits avec vpermb.
  • AVX512ICL - Lancé en 2019, suppression de la réduction de fréquence du processeur.
  • AVX10 - À venir.

Il est important de noter que des jeux d'instructions peuvent être supprimés ou ajoutés d'un CPU à l'autre. Par exemple, AVX512 a été supprimé, de manière controversée, dans la 12ème génération de CPU Intel. C'est pour cette raison que FFmpeg fait de la détection du CPU à l'exécution. FFmpeg détecte les capacités du processeur sur lequel il s'exécute.

Comme vous l'avez vu dans l'exercice, les pointeurs de fonctions sont par défaut en C et sont remplacés par un jeu d'instruction particulier. Cela signifie que la détection est réalisée une fois et ne sera plus jamais requise. Cela contraste avec beaucoup d'applications propriétaires qui programment en dur dans leur code un jeu d'instruction, rendant obsolètes des ordinateurs parfaitement fonctionnels. Cela permet aussi d'activer ou de désactiver des fonctions optimisées à l'exécution. C'est l'un des plus grands avantages de l'open source.

Des programmes comme FFmpeg sont utilisés par des milliards d'appareils autour du monde, certains d'entre eux sont peut-être très âgés. Techniquement, FFmpeg est compatible avec des machines possédant uniquement le jeu d'instruction SSE, machines qui datent d'il y a 25 ans.
Heureusement, x86inc.ams est capable de vous indiquer si une instruction n'est pas disponible dans un jeu d'instruction particulier.

Pour vous donner une idée des compatibilités, voici la disponibilité des jeux d'instructions selon le Steam Survey en novembre 2024 (évidemment biaisé en faveur des joueurs) :

Jeu d'instruction Disponibilité
SSE2 100%
SSE3 100%
SSSE3 99.86%
SSE4.1 99.80%
AVX 97.39%
AVX2 94.44%
AVX512 (AVX512 et AVX512ICL confondus) 14.09%

Pour une application comme FFmpeg avec des milliards d'utilisateurs, même 0.1% d'entre eux représente un grand nombre d'utilisateurs et de rapports de bug en cas de problème. FFmpeg possède une grande infrastructure de tests pour tester chaque combinaison de CPU/OS/compilateurs présentée sur FATE testsuite. Chaque simple commit est exécuté sur des centaines de machines pour être certain que rien ne casse.

Intel fournit un manuel de jeu d'instructions détaillé ici : https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html

Cela peut être laborieux de chercher sur un PDF, une alternative en ligne est présente ici : https://www.felixcloutier.com/x86/

Il y a aussi une représentation visuelle des instructions SIMD disponible ici : https://www.officedaytime.com/simd512e/

Une partie du défi de l'utilisation de l'assembleur x86 est de trouver la bonne instruction dont vous avez besoin. Dans certains cas, des instructions peuvent être utilisées dans des cas de figure pour lesquelles elles n'ont pas été faites originellement.

Astuces sur les décalages de pointeurs

Revenons à notre fonction originale de la Leçon 1, mais ajoutons un argument de largeur à la fonction en C.

Nous utilisons ptrdiff_t pour la variable de largeur au lieu de int afin de garantir que les 32 bits supérieurs de l’argument 64 bits sont mis à zéro. Si nous passions directement une largeur en int dans la signature de la fonction, puis tentions de l’utiliser comme un quad pour l’arithmétique des pointeurs (c’est-à-dire en utilisant widthq), les 32 bits supérieurs du registre pourraient contenir des valeurs arbitraires. Nous pourrions corriger cela en étendant le signe de width avec movsxd (voir aussi la macro movsxdifnidn dans x86inc.asm), mais cette méthode est plus simple.

La fonction ci-dessous contient l’astuce des décalages de pointeur :

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

Passons en revue cela étape par étape, car cela peut être déroutant :

assembly
add srcq, widthq
   add src2q, widthq
   neg widthq

La largeur widthq est ajoutée à chaque pointeur de sorte que chaque pointeur pointe maintenant vers la fin du tampon à traiter. Le signe de widthq est ensuite inversé.

assembly
movu  m0, [srcq+widthq]
    movu  m1, [src2q+widthq]

Les chargements sont ensuite effectués avec widthq étant négatif. Ainsi, lors de la première itération, [srcq+widthq] pointe vers l'adresse originale de srcq, c'est-à-dire qu'il pointe vers le début du tampon.

assembly
add   widthq, mmsize
    jl .loop

mmsize est ajouté à widthq négatif, le rapprochant ainsi de zéro. La condition de boucle est maintenant jl (jump if less than zero). Cette astuce permet à widthq d’être utilisé à la fois comme décalage de pointeur et comme compteur de boucle, économisant ainsi une instruction cmp. Elle permet également d’utiliser le décalage du pointeur dans plusieurs chargements et stockages, ainsi que d'utiliser des multiples des décalages de pointeur si nécessaire (retenez cela pour l'exercice).

Alignement

Dans tous nos exemples, nous avons utilisé movu pour éviter le sujet de l’alignement. De nombreux processeurs peuvent charger et stocker des données plus rapidement si celles-ci sont alignées, c’est-à-dire si l’adresse mémoire est divisible par la taille du registre SIMD. Lorsque c’est possible, nous essayons d’utiliser des chargements et des stockages alignés dans FFmpeg en utilisant mova.

Dans FFmpeg, av_malloc peut fournir une mémoire alignée sur le tas, et la directive de préprocesseur C DECLARE_ALIGNED peut fournir une mémoire alignée sur la pile. Si mova est utilisé avec une adresse non alignée, cela entraînera une faute de segmentation et l’application plantera. Il est également important de s’assurer que la valeur d’alignement correspond à la taille du registre SIMD, c’est-à-dire 16 pour xmm, 32 pour ymm et 64 pour zmm.

Voici comment aligner le début de la section RODATA sur 64 octets :

assembly
SECTION_RODATA 64

Notez que cela aligne uniquement le début de la section RODATA. Des octets de remplissage peuvent être nécessaires pour s'assurer que le label suivant reste sur une limite de 64 octets.

Décompactage

Un autre sujet que nous avons évité jusqu'à présent est le débordement. Cela se produit, par exemple, lorsque la valeur d'un octet dépasse 255 après une opération comme l'addition ou la multiplication. Nous pouvons vouloir effectuer une opération où nous avons besoin d'une valeur intermédiaire plus grande qu'un octet (par exemple, des mots), ou potentiellement nous voulons laisser les données dans cette taille intermédiaire plus grande.

Pour les octets non signés, c'est là que les instructions punpcklbw (décompacter des octets en mots dans le bas d'un paquet) et punpckhbw (décompacter des octets en mots dans le haut d'un paquet) interviennent.

Voyons comment fonctionne punpcklbw. La syntaxe pour la version SSE2 dans le manuel Intel est la suivante :

PUNPCKLBW xmm1, xmm2/m128

Cela signifie que sa source (côté droit) peut être un registre xmm ou une adresse mémoire (m128 signifie une adresse mémoire avec la syntaxe standard [base + échelle*index + déplacement]), et la destination est un registre xmm.

Le site web officedaytime.com ci-dessus a un bon diagramme montrant ce qui se passe :

Vous pouvez voir que les octets sont entrelacés à partir de la moitié inférieure de chaque registre respectivement. Mais quel rapport cela a-t-il avec l'extension de plage ? Si le registre source est entièrement nul, cela entrelace les octets dans le registre de destination avec des zéros. C'est ce qu'on appelle l'extension par zéro, car les octets sont sans signe. punpckhbw peut être utilisé pour faire la même chose pour les octets supérieurs.

Voici un extrait montrant comment cela est fait :

assembly
pxor      m2, m2 ; zero out m2

movu      m0, [srcq]
movu      m1, m0 ; make a copy of m0 in m1
punpcklbw m0, m2
punpckhbw m1, m2

m0 et m1 contiennent maintenant les octets d'origine étendus à zéro en mots. Dans la prochaine leçon, vous verrez comment les instructions à trois opérandes en AVX rendent le deuxième movu inutile.

Extension de signe

Les données signées sont un peu plus compliquées. Pour étendre la plage d'un entier signé, nous devons utiliser un processus connu sous le nom d'extension de signe. Cela consiste à ajouter des bits de signe dans les bits les plus significatifs. Par exemple : -2 en int8_t est 0b11111110. Pour l'étendre à int16_t, le bit de signe 1 est répété pour obtenir 0b1111111111111110.

pcmpgtb (packed compare greater than byte) peut être utilisé pour l'extension de signe. En effectuant la comparaison (0 > octet), tous les bits dans l'octet de destination sont définis sur 1 si l'octet est négatif, sinon les bits dans l'octet de destination sont définis sur 0. punpckX peut être utilisé comme ci-dessus pour effectuer l'extension de signe. Si l'octet est négatif, l'octet correspondant est 0b11111111 et sinon il est 0x00000000. L'entrelacement de la valeur de l'octet avec la sortie de pcmpgtb effectue une extension de signe en mot.

assembly
pxor      m2, m2 ; zero out m2

movu      m0, [srcq]
movu      m1, m0 ; make a copy of m0 in m1

pcmpgtb   m2, m0
punpcklbw m0, m2
punpckhbw m1, m2

Comme vous pouvez le voir, il y a une instruction supplémentaire par rapport au cas non signé.

Packing

packuswb (pack unsigned word to byte) et packsswb vous permettent de passer du mot au byte. Ils vous permettent d'entrelacer deux registres SIMD contenant des mots en un seul registre SIMD avec des bytes. Notez que si les valeurs dépassent la plage des bytes, elles seront saturées (c'est-à-dire clampées à la valeur maximale).

Shuffles

Les mélanges (shuffles), également appelés permutations, sont sans doute l'instruction la plus importante dans le traitement vidéo et pshufb (packed shuffle bytes), disponible dans SSSE3, est la variante la plus importante.

Pour chaque byte, le byte source correspondant est utilisé comme un index dans le registre de destination, sauf lorsque le bit de poids fort (MSB) est activé, le byte de destination est mis à zéro. C'est l'analogue du code C suivant (bien que dans SIMD, les 16 itérations de boucle se produisent en parallèle) :

c
uint8_t tmp[16];
memcpy(tmp, dst, 16);
for(int i = 0; i < 16; i++) {
    if(src[i] & 0x80)
        dst[i] = 0;
    else
        dst[i] = tmp[src[i]];
}

Voici un exemple simple en assembleur:

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 ; shuffle m0 based on m1

Notez que -1, pour une lecture facile, est utilisé comme l'indice de mélange pour mettre à zéro le byte de sortie : -1 en tant que byte est le champ binaire 0b11111111 (complément à deux), et ainsi le bit de poids fort (MSB) (0x80) est activé.


File: lesson_03/index.md

FFmpeg Assembly Lesson Three

Let’s explain some more jargon and give you a short history lesson.

Instruction Sets

You may have seen in the previous lesson we talked about SSE2 which is a set of SIMD instructions. When a new CPU generation is released it may come with new instructions and sometimes larger register sizes. The history of the x86 instruction set is very complex so this is a simplified history (there are many more subcategories):

  • MMX - Launched in 1997, first SIMD in Intel Processors, 64-bit registers, historic
  • SSE (Streaming SIMD Extensions) - Launched in 1999, 128-bit registers
  • SSE2 - Launched in 2000, many new instructions
  • SSE3 - Launched in 2004, first horizontal instructions
  • SSSE3 (Supplemental SSE3) - Launched in 2006, new instructions but most importantly pshufb shuffle instruction, arguably the most important instruction in video processing
  • SSE4 - Launched in 2008, many new instructions including packed minimum and maximum.
  • AVX - Launched in 2011, 256-bit registers (float only) and new three-operand syntax
  • AVX2 - Launched in 2013, 256-bit registers for integer instructions
  • AVX512 - Launched in 2017, 512-bit registers, new operation mask feature. These had limited use at the time in FFmpeg because of CPU frequency downscaling when new instructions were used. Full 512-bit shuffle (permute) with vpermb.
  • AVX512ICL - Launched 2019, no more clock frequency downscaling.
  • AVX10 - Upcoming

It’s worth noting that instruction sets can be removed as well as added to CPUs. For example AVX512 was removed, controversially, in 12th Generation Intel CPUs. It’s for this reason that FFmpeg does runtime CPU detection. FFmpeg detects the capabilities of the CPU it’s running on.

As you saw in the assignment, function pointers are C by default and are replaced with a particular instruction set variant. This means detection is done once and then never needs to be done again. This is in contrast to many proprietary applications which hardcode a particular instruction set making a perfectly functional computer obsolete. This also allows optimised functions to be turned on/off at runtime. This is one of the big benefits of open source.

Programs like FFmpeg are used on billions of devices around the world, some of which may be very old. FFmpeg technically supports machines supporting SSE only, which are 25 years old! Thankfully x86inc.asm is capable of telling you if you use an instruction that’s not available in a particular instruction set.

To give you an idea of real-world capabilities, here is the instruction set availability from the Steam Survey as of November 2024 (this is obviously biased towards gamers):

Instruction Set Availability
SSE2 100%
SSE3 100%
SSSE3 99.86%
SSE4.1 99.80%
AVX 97.39%
AVX2 94.44%
AVX512 (Steam does not separate between AVX512 and AVX512ICL) 14.09%

For an application like FFmpeg with billions of users, even 0.1% is a very large number of users and bug reports if something breaks. FFmpeg has extensive testing infrastructure for testing the variations of CPU/OS/Compiler in our FATE testsuite. Every single commit is run on hundreds of machines to make sure nothing breaks.

Intel provides a detailed instruction set manual here: https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html

It can be cumbersome to search through a PDF so there is an unofficial web based alternative here: https://www.felixcloutier.com/x86/

There is also a visual representation of SIMD instructions available here:
https://www.officedaytime.com/simd512e/

Part of the challenge of x86 assembly is finding the right instruction for your needs. In some cases instructions can be used in a way they were not originally intended.

Pointer offset trickery

Let’s go back to our original function from Lesson 1, but add a width argument to the C function.

We use ptrdiff_t for the width variable instead of int to make sure that the upper 32-bits of the 64-bit argument are zero. If we directly passed an int width in the function signature, and then attempted to use it as a quad for pointer arithmetic (i.e. using widthq) the upper 32-bits of the register can be filled with arbitrary values. We could fix this by sign extending width with movsxd (also see macro movsxdifnidn in x86inc.asm), but this is an easier way.

The function below has the pointer offset trickery in it:

assembly
;static void add_values(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

Let’s go through this step by step as it can be confusing:

assembly
add srcq, widthq
   add src2q, widthq
   neg widthq

The width is added to each pointer such that each pointer now points to the end of the buffer to be processed. The width is then negated.

assembly
movu  m0, [srcq+widthq]
    movu  m1, [src2q+widthq]

The loads are then done with widthq being negative. So on the first iteration [srcq+widthq] points to the original address of srcq, i.e points back to the beginning of the buffer.

assembly
add   widthq, mmsize
    jl .loop

mmsize is added to the negative widthq bringing it closer to zero. The loop condition is now jl (jump if less than zero). This trick means widthq is used as a pointer offset and as a loop counter at the same time, saving a cmp instruction. It also allows the pointer offset to be used in multiple loads and stores, as well as using multiples of the pointer offsets if needed (remember this for the assignment).

Alignment

In all our examples we have been using movu to avoid the topic of alignment. Many CPUs can load and store data faster if the data is aligned, i.e if the memory address is divisible by the SIMD register size. Where possible we try to use aligned loads and stores in FFmpeg using mova.

In FFmpeg, av_malloc is able to provide aligned memory on the heap and the DECLARE_ALIGNED C preprocessor directive can provide aligned memory on the stack. If mova is used with an unaligned address, it will cause a segmentation fault and the application will crash. It’s also important to be sure that the alignment value corresponds to the SIMD register size, i.e 16 with xmm, 32 for ymm and 64 for zmm.

Here is how to align the beginning of the RODATA section to 64-bytes:

assembly
SECTION_RODATA 64

Note that this just aligns the beginning of RODATA. Padding bytes might be needed to make sure the next label remains on a 64-byte boundary.

Range expansion

Another topic we have avoided until now is overflowing. This happens, for example, when the value of a byte goes beyond 255 after an operation like addition or multiplication. We may want to perform an operation where we need an intermediate value larger than a byte (e.g words), or potentially we want to leave the data in that larger intermediate size.

For unsigned bytes, this is where punpcklbw (packed unpack low bytes to words) and punpckhbw (packed unpack high bytes to words) comes in.

Let’s look at how punpcklbw works. The syntax for the SSE2 version from the Intel Manual is as follows:

PUNPCKLBW xmm1, xmm2/m128

This means its source (right hand side) can be an xmm register or a memory address (m128 means a memory address with the standard [base + scale*index + disp]) syntax and the destination an xmm register.

The officedaytime.com website above has a good diagram showing what’s going on:

You can see that bytes are interleaved from the lower half of each register respectively. But what has this got to do with range extension? If the src register is all zeros this interleaves the bytes in dst with zeros. This is what is known as zero extension as the bytes are unsigned. punpckhbw can be used to do the same thing for the high bytes.

Here is a snippet showing how this is done:

assembly
pxor      m2, m2 ; zero out m2

movu      m0, [srcq]
movu      m1, m0 ; make a copy of m0 in m1
punpcklbw m0, m2
punpckhbw m1, m2

m0 and m1 now contain the original bytes zero extended to words. In the next lesson you’ll see how three-operand instructions in AVX make the second movu unnecessary.

Sign extension

Signed data is a bit more complicated. To range extend a signed integer, we need to use a process known as sign extension. This pads the MSBs with the sign bit. For example: -2 in int8_t is 0b11111110. To sign extend it to int16_t the MSB of 1 is repeated to make 0b1111111111111110.

pcmpgtb (packed compare greater than byte) can be used for sign extension. By doing the comparison (0 > byte), all the bits in the destination byte are set to 1 if the byte is negative, otherwise the bits in the destination byte are set to 0. punpckX can be used as above to perform the sign extension. If the byte is negative the corresponding byte is 0b11111111 and otherwise it’s 0x00000000. Interleaving the byte value with the output of pcmpgtb performs a sign extension to word as a result.

assembly
pxor      m2, m2 ; zero out m2

movu      m0, [srcq]
movu      m1, m0 ; make a copy of m0 in m1

pcmpgtb   m2, m0
punpcklbw m0, m2
punpckhbw m1, m2

As you can see there is an extra instruction compared to the unsigned case.

Packing

packuswb (pack unsigned word to byte) and packsswb lets you go from word to byte. It lets you interleave two SIMD registers containing words into one SIMD register with a byte. Note that if the values exceed the byte range, they will be saturated (i.e clamped at the largest value).

Shuffles

Shuffles, also known as permutes, are arguably the most important instruction in video processing and pshufb (packed shuffle bytes), available in SSSE3, is the most important variant.

For each byte the corresponding source byte is used as an index of the destination register, except when the MSB is set the destination byte is zeroed. It’s analogous to the following C code (although in SIMD all 16 loop iterations happen in parallel):

c
uint8_t tmp[16];
memcpy(tmp, dst, 16);
for(int i = 0; i < 16; i++) {
    if(src[i] & 0x80)
        dst[i] = 0;
    else
        dst[i] = tmp[src[i]];
}

Here’s a simple assembly example:

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 ; shuffle m0 based on m1

Note that -1 for easy reading is used as the shuffle index to zero out the output byte: -1 as a byte is the 0b11111111 bitfield (two’s complement), and thus the MSB (0x80) is set.


File: lesson_03/index.tr.md

FFmpeg Assembly Üçüncü Ders

Biraz daha jargon açıklayalım ve size kısa bir tarih dersi verelim.

Talimat Setleri

Önceki derste SSE2'den bahsettiğimizi, bunun bir SIMD talimat seti olduğunu görmüş olabilirsiniz. Yeni bir CPU nesli piyasaya çıktığında yeni talimatlar ve bazen daha büyük register boyutları ile gelebilir. x86 talimat setinin tarihçesi çok karmaşıktır, bu nedenle bu basitleştirilmiş bir tarihçedir (çok daha fazla alt kategori vardır):

  • MMX - 1997'de piyasaya çıktı, Intel İşlemcilerinde ilk SIMD, 64-bit register'lar, tarihsel
  • SSE (Streaming SIMD Extensions) - 1999'da piyasaya çıktı, 128-bit register'lar
  • SSE2 - 2000'de piyasaya çıktı, birçok yeni talimat
  • SSE3 - 2004'te piyasaya çıktı, ilk yatay talimatlar
  • SSSE3 (Supplemental SSE3) - 2006'da piyasaya çıktı, yeni talimatlar ancak en önemlisi pshufb shuffle talimatı, tartışmasız video işlemede en önemli talimat
  • SSE4 - 2008'de piyasaya çıktı, paketlenmiş minimum ve maksimum dahil birçok yeni talimat.
  • AVX - 2011'de piyasaya çıktı, 256-bit register'lar (sadece float) ve yeni üç-operand sözdizimi
  • AVX2 - 2013'te piyasaya çıktı, integer talimatları için 256-bit register'lar
  • AVX512 - 2017'de piyasaya çıktı, 512-bit register'lar, yeni işlem maskesi özelliği. Yeni talimatlar kullanıldığında CPU frekans düşürme nedeniyle o zamanlar FFmpeg'de sınırlı kullanımları vardı. vpermb ile tam 512-bit shuffle (permute).
  • AVX512ICL - 2019'da piyasaya çıktı, artık clock frekans düşürme yok.
  • AVX10 - Yakında

Talimat setlerinin CPU'lara eklenmesinin yanı sıra kaldırılabileceğini de belirtmek gerekir. Örneğin AVX512, 12. Nesil Intel CPU'larında tartışmalı şekilde kaldırılmıştır. Bu nedenle FFmpeg çalışma zamanı CPU algılaması yapar. FFmpeg üzerinde çalıştığı CPU'nun yeteneklerini algılar.

Ödevde gördüğünüz gibi, fonksiyon pointer'ları varsayılan olarak C'dir ve belirli bir talimat seti varyantı ile değiştirilir. Bu, algılamanın bir kez yapıldığı ve bir daha asla yapılması gerekmediği anlamına gelir. Bu, belirli bir talimat setini hardcode eden ve mükemmel çalışan bir bilgisayarı eskiten birçok tescilli uygulamanın aksine. Bu ayrıca optimize edilmiş fonksiyonların çalışma zamanında açılıp/kapatılmasına olanak tanır. Bu, açık kaynak kodun büyük faydalarından biridir.

FFmpeg gibi programlar dünya çapında milyarlarca cihazda kullanılır, bunların bazıları çok eski olabilir. FFmpeg teknik olarak 25 yaşındaki sadece SSE destekleyen makineleri destekler! Neyse ki x86inc.asm belirli bir talimat setinde mevcut olmayan bir talimat kullanıp kullanmadığınızı söyleyebilir.

Gerçek dünya yetenekleri hakkında bir fikir vermek için, Kasım 2024 itibariyle Steam Anketi'nden talimat seti kullanılabilirliği (bu açıkça oyunculara yönelik önyargılıdır):

Talimat Seti Kullanılabilirlik
SSE2 %100
SSE3 %100
SSSE3 %99.86
SSE4.1 %99.80
AVX %97.39
AVX2 %94.44
AVX512 (Steam AVX512 ve AVX512ICL arasında ayrım yapmıyor) %14.09

Milyarlarca kullanıcısı olan FFmpeg gibi bir uygulama için, %0.1 bile çok büyük bir kullanıcı sayısıdır ve bir şey bozulursa hata raporları gelir. FFmpeg, FATE test paketimizde CPU/OS/Derleyici varyasyonlarını test etmek için kapsamlı test altyapısına sahiptir. Her commit hiçbir şeyin bozulmadığından emin olmak için yüzlerce makinede çalıştırılır.

Intel detaylı talimat seti kılavuzunu burada sağlar: https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html

PDF'te arama yapmak zahmetli olabilir, bu nedenle burada resmi olmayan web tabanlı bir alternatif var: https://www.felixcloutier.com/x86/

SIMD talimatlarının görsel temsili de burada mevcuttur:
https://www.officedaytime.com/simd512e/

x86 assembly'nin zorluklarından biri ihtiyaçlarınız için doğru talimatı bulmaktır. Bazı durumlarda talimatlar orijinal amaçlarından farklı şekilde kullanılabilir.

Pointer offset numarası

  1. Dersten orijinal fonksiyonumuza geri dönelim, ancak C fonksiyonuna bir width argümanı ekleyelim.

Width değişkeni için int yerine ptrdiff_t kullanıyoruz, 64-bit argümanın üst 32-bitinin sıfır olduğundan emin olmak için. Eğer fonksiyon imzasında doğrudan bir int width geçirsek ve sonra bunu pointer aritmetiği için quad olarak kullanmaya çalışsak (widthq kullanarak) register'ın üst 32-biti rastgele değerlerle doldurulabilir. Bunu movsxd ile width'i işaret genişleterek düzeltebilirdik (x86inc.asm'daki movsxdifnidn makrosuna da bakın), ancak bu daha kolay bir yol.

Aşağıdaki fonksiyon pointer offset numarasını içerir:

assembly
;static void add_values(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

Kafa karıştırabileceği için bunu adım adım gözden geçirelim:

assembly
add srcq, widthq
   add src2q, widthq
   neg widthq

Width her pointer'a eklenir böylece her pointer artık işlenecek buffer'ın sonunu işaret eder. Width daha sonra negatif yapılır.

assembly
movu  m0, [srcq+widthq]
    movu  m1, [src2q+widthq]

Yüklemeler daha sonra widthq negatif olacak şekilde yapılır. Bu nedenle ilk iterasyonda [srcq+widthq] srcq'nun orijinal adresini işaret eder, yani buffer'ın başlangıcına geri işaret eder.

assembly
add   widthq, mmsize
    jl .loop

mmsize negatif widthq'ya eklenir ve onu sıfıra yaklaştırır. Döngü koşulu artık jl'dir (sıfırdan küçükse atla). Bu numara widthq'nun aynı anda pointer offset ve döngü sayacı olarak kullanılması anlamına gelir, cmp talimatından tasarruf sağlar. Ayrıca pointer offset'inin birden fazla yüklemede ve saklamada kullanılmasına ve gerektiğinde pointer offset'lerinin katlarının kullanılmasına olanak tanır (ödev için bunu hatırlayın).

Hizalama

Tüm örneklerde hizalama konusunu önlemek için movu kullanıyorduk. Birçok CPU, veri hizalanmışsa, yani bellek adresi SIMD register boyutuna bölünebiliyorsa veriyi daha hızlı yükleyip saklayabilir. Mümkün olduğunda FFmpeg'de mova kullanarak hizalanmış yükleme ve saklamaları kullanmaya çalışırız.

FFmpeg'de av_malloc heap'te hizalanmış bellek sağlayabilir ve DECLARE_ALIGNED C ön işlemci direktifi stack'te hizalanmış bellek sağlayabilir. mova hizalanmamış bir adresle kullanılırsa, segmentasyon hatası oluşturacak ve uygulama çökecektir. Ayrıca hizalama değerinin SIMD register boyutuna karşılık geldiğinden emin olmak önemlidir, yani xmm ile 16, ymm için 32 ve zmm için 64.

RODATA bölümünün başlangıcını 64-byte'a hizalamak için:

assembly
SECTION_RODATA 64

Bunun sadece RODATA'nın başlangıcını hizaladığını unutmayın. Bir sonraki etiketin 64-byte sınırında kalmasını sağlamak için dolgu byte'ları gerekebilir.

Aralık genişletmesi

Şimdiye kadar kaçındığımız bir diğer konu da taşmadır. Bu, örneğin toplama veya çarpma gibi bir işlemden sonra bir byte'ın değeri 255'i geçtiğinde olur. Ara değerin bir byte'dan (örn. word'ler) daha büyük olmasına ihtiyaç duyabiliriz veya potansiyel olarak veriyi o daha büyük ara boyutta bırakmak isteyebiliriz.

İşaretsiz byte'lar için punpcklbw (packed unpack low bytes to words - paketlenmiş düşük byte'ları word'lere açma) ve punpckhbw (packed unpack high bytes to words - paketlenmiş yüksek byte'ları word'lere açma) devreye girer.

punpcklbw'nin nasıl çalıştığına bakalım. Intel Kılavuzundan SSE2 sürümü için sözdizimi şöyledir:

PUNPCKLBW xmm1, xmm2/m128

Bu, kaynağının (sağ taraf) bir xmm register'ı veya bellek adresi (m128, standart [base + scale*index + disp] sözdizimi ile bir bellek adresi anlamına gelir) ve hedefin bir xmm register'ı olabileceği anlamına gelir.

Yukarıdaki officedaytime.com web sitesi neler olup bittiğini gösteren iyi bir diyagrama sahiptir:

Byte'ların her register'ın alt yarısından sırasıyla interleave edildiğini görebilirsiniz. Peki bunun aralık genişletmesi ile ne ilgisi var? Eğer src register'ı tamamen sıfırsa, bu dst'deki byte'ları sıfırlarla interleave eder. Bu, byte'lar işaretsiz olduğu için sıfır genişletmesi olarak bilinir. punpckhbw yüksek byte'lar için aynı şeyi yapmak için kullanılabilir.

Bunun nasıl yapıldığını gösteren bir parçacık:

assembly
pxor      m2, m2 ; m2'yi sıfırla

movu      m0, [srcq]
movu      m1, m0 ; m0'ın bir kopyasını m1'de yap
punpcklbw m0, m2
punpckhbw m1, m2

m0 ve m1 artık orijinal byte'ları word'lere sıfır genişletilmiş olarak içerir. Bir sonraki derste AVX'deki üç-operand talimatlarının ikinci movu'yu nasıl gereksiz kıldığını göreceksiniz.

İşaret genişletmesi

İşaretli veri biraz daha karmaşık. İşaretli bir integer'ı aralık genişletmek için işaret genişletmesi olarak bilinen bir işlem kullanmamız gerekir. Bu MSB'leri işaret biti ile doldurur. Örneğin: int8_t'de -2, 0b11111110'dur. int16_t'ye işaret genişletmek için 1'in MSB'si 0b1111111111111110 yapmak için tekrarlanır.

pcmpgtb (packed compare greater than byte - paketlenmiş byte'dan büyük karşılaştırma) işaret genişletmesi için kullanılabilir. (0 > byte) karşılaştırmasını yaparak, eğer byte negatifse hedef byte'daki tüm bitler 1'e ayarlanır, aksi takdirde hedef byte'daki bitler 0'a ayarlanır. punpckX yukarıdaki gibi işaret genişletmesi gerçekleştirmek için kullanılabilir. Eğer byte negatifse karşılık gelen byte 0b11111111'dir ve aksi takdirde 0x00000000'dır. Byte değerini pcmpgtb'nin çıktısı ile interleave etmek sonuç olarak word'e işaret genişletmesi gerçekleştirir.

assembly
pxor      m2, m2 ; m2'yi sıfırla

movu      m0, [srcq]
movu      m1, m0 ; m0'ın bir kopyasını m1'de yap

pcmpgtb   m2, m0
punpcklbw m0, m2
punpckhbw m1, m2

Gördüğünüz gibi işaretsiz duruma kıyasla bir ekstra talimat var.

Paketleme

packuswb (pack unsigned word to byte - işaretsiz word'ü byte'a paketleme) ve packsswb word'den byte'a gitmenizi sağlar. Word içeren iki SIMD register'ını byte içeren bir SIMD register'ında interleave etmenizi sağlar. Değerler byte aralığını aşarsa, doyurulacaklarını (yani en büyük değerde kısıtlanacaklarını) unutmayın.

Shuffle'lar

Shuffle'lar, permute olarak da bilinirler, tartışmasız video işlemede en önemli talimattır ve SSSE3'te mevcut olan pshufb (packed shuffle bytes - paketlenmiş byte shuffle), en önemli varyantıdır.

Her byte için karşılık gelen kaynak byte hedef register'ının bir indeksi olarak kullanılır, MSB ayarlandığında hariç hedef byte sıfırlanır. Aşağıdaki C koduna benzerdir (SIMD'de tüm 16 döngü iterasyonu paralel olarak gerçekleşir):

c
uint8_t tmp[16];
memcpy(tmp, dst, 16);
for(int i = 0; i < 16; i++) {
    if(src[i] & 0x80)
        dst[i] = 0;
    else
        dst[i] = tmp[src[i]];
}

İşte basit bir assembly örneği:

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 ; m1'e göre m0'ı shuffle et

Kolay okunabilirlik için çıktı byte'ını sıfırlamak için shuffle indeksi olarak -1 kullanıldığını unutmayın: byte olarak -1, 0b11111111 bit alanıdır (iki'nin tümleyeni), ve bu nedenle MSB (0x80) ayarlanmıştır.


File: lesson_03/index.zh.md

[Binary file]