diff --git a/README.es.md b/README.es.md index 7c9a91a..9c88375 100644 --- a/README.es.md +++ b/README.es.md @@ -15,4 +15,5 @@ https://discord.com/invite/Ks5MhUhqfB **Traducciones** * [English](./README.md) -* [Français](./README.fr.md) \ No newline at end of file +* [Français](./README.fr.md) +* [Русский](./README.ru.md) \ No newline at end of file diff --git a/README.fr.md b/README.fr.md index b9b1fa8..34a6372 100644 --- a/README.fr.md +++ b/README.fr.md @@ -15,4 +15,5 @@ https://discord.com/invite/Ks5MhUhqfB **Traductions** * [English](./README.md) -* [Spanish](./README.es.md) \ No newline at end of file +* [Español](./README.es.md) +* [Русский](./README.ru.md) \ No newline at end of file diff --git a/README.md b/README.md index 67c4007..6b0eb1d 100644 --- a/README.md +++ b/README.md @@ -15,4 +15,5 @@ https://discord.com/invite/Ks5MhUhqfB **Translations** * [Français](./README.fr.md) -* [Spanish](./README.es.md) \ No newline at end of file +* [Español](./README.es.md) +* [Русский](./README.ru.md) \ No newline at end of file diff --git a/README.ru.md b/README.ru.md new file mode 100644 index 0000000..b9e2d13 --- /dev/null +++ b/README.ru.md @@ -0,0 +1,19 @@ +Добро пожаловать в Школу языка ассемблера FFmpeg. Вы сделали первый шаг на самом интересном, сложном и полезном пути в программировании. Эти уроки дадут вам основу в том, как язык ассемблера используется в FFmpeg, и откроют ваши глаза на то, что на самом деле происходит в вашем компьютере. + +**Требуемые знания** + +* Знание языка C, в частности указателей. Если вы не знаете C, проработайте книгу [Язык программирования C](https://en.wikipedia.org/wiki/The_C_Programming_Language) +* Школьная математика (скаляр против вектора, сложение, умножение и т.д.) + +**Уроки** + +В этом Git-репозитории есть уроки и задания (пока не загружены), которые соответствуют каждому уроку. К концу уроков вы сможете вносить вклад в FFmpeg. + +Доступен сервер Discord для ответов на вопросы: +https://discord.com/invite/Ks5MhUhqfB + +**Переводы** + +* [English](./README.md) +* [Français](./README.fr.md) +* [Español](./README.es.md) diff --git a/lesson_01/index.ru.md b/lesson_01/index.ru.md new file mode 100644 index 0000000..4450359 --- /dev/null +++ b/lesson_01/index.ru.md @@ -0,0 +1,217 @@ +**Урок первый по языку ассемблера FFmpeg** + +**Введение** + +Добро пожаловать в Школу языка ассемблера FFmpeg. Вы сделали первый шаг на самом интересном, сложном и полезном пути в программировании. Эти уроки дадут вам основу в том, как язык ассемблера используется в FFmpeg, и откроют ваши глаза на то, что на самом деле происходит в вашем компьютере. + +**Требуемые знания** + +* Знание языка C, в частности указателей. Если вы не знаете C, проработайте книгу [Язык программирования C](https://en.wikipedia.org/wiki/The_C_Programming_Language) +* Школьная математика (скаляр против вектора, сложение, умножение и т.д.) + +**Что такое язык ассемблера?** + +Язык ассемблера — это язык программирования, на котором вы пишете код, напрямую соответствующий инструкциям, которые обрабатывает процессор. Человекочитаемый язык ассемблера, как следует из названия, *ассемблируется* в двоичные данные, известные как *машинный код*, который может понять процессор. Вы можете видеть, что код на языке ассемблера называют просто "assembly" или "asm". + +Подавляющее большинство кода на ассемблере в FFmpeg — это то, что известно как *SIMD, Single Instruction Multiple Data (Одна инструкция — множество данных)*. SIMD иногда называют векторным программированием. Это означает, что конкретная инструкция работает с несколькими элементами данных одновременно. Большинство языков программирования работают с одним элементом данных за раз, что известно как скалярное программирование. + +Как вы могли догадаться, SIMD хорошо подходит для обработки изображений, видео и аудио, которые имеют много данных, упорядоченных последовательно в памяти. В процессоре есть специальные инструкции, доступные для помощи нам в обработке последовательных данных. + +В FFmpeg вы увидите термины "функция на ассемблере", "SIMD" и "вектор(изация)", используемые взаимозаменяемо. Все они относятся к одному и тому же: написание функции на языке ассемблера вручную для обработки нескольких элементов данных за один раз. Некоторые проекты могут также называть их "ядрами на ассемблере". + +Все это может звучать сложно, но важно помнить, что в FFmpeg старшеклассники писали код на ассемблере. Как и во всем, обучение — это 50% жаргона и 50% реального обучения. + +**Почему мы пишем на языке ассемблера?** +Чтобы сделать обработку мультимедиа быстрой. Очень часто можно получить улучшение скорости в 10 раз или более от написания кода на ассемблере, что особенно важно, когда нужно воспроизводить видео в реальном времени без заикания. Это также экономит энергию и продлевает время работы батареи. Стоит отметить, что функции кодирования и декодирования видео — одни из наиболее часто используемых функций на земле, как конечными пользователями, так и крупными компаниями в их центрах обработки данных. Поэтому даже небольшое улучшение быстро суммируется. + +Вы часто увидите в Интернете людей, использующих *интринсики*, которые являются C-подобными функциями, которые отображаются на инструкции ассемблера для более быстрой разработки. В FFmpeg мы не используем интринсики, а вместо этого пишем код на ассемблере вручную. Это область противоречий, но интринсики обычно на 10-15% медленнее, чем написанный вручную ассемблер (сторонники интринсиков не согласятся), в зависимости от компилятора. Для FFmpeg каждая крупица дополнительной производительности помогает, поэтому мы пишем непосредственно на коде ассемблера. Есть также аргумент, что интринсики трудно читать из-за их использования "[венгерской нотации](https://en.wikipedia.org/wiki/Hungarian_notation)". + +Вы также можете увидеть *встроенный ассемблер* (т.е. не использующий интринсики), остающийся в нескольких местах в FFmpeg по историческим причинам, или в таких проектах, как ядро Linux из-за очень специфических случаев использования там. Это когда код на ассемблере находится не в отдельном файле, а написан в строке с кодом C. Преобладающее мнение в таких проектах, как FFmpeg, заключается в том, что этот код трудно читать, он не широко поддерживается компиляторами и не поддается обслуживанию. + +Наконец, вы увидите много самопровозглашенных экспертов в Интернете, говорящих, что все это не нужно, и компилятор может сделать всю эту "векторизацию" за вас. По крайней мере для целей обучения игнорируйте их: недавние тесты, например, в [проекте dav1d](https://www.videolan.org/projects/dav1d.html), показали около 2-кратного ускорения от этой автоматической векторизации, в то время как написанные вручную версии могли достичь 8-кратного ускорения. + +**Варианты языка ассемблера** +Эти уроки будут сосредоточены на 64-битном языке ассемблера x86. Он также известен как amd64, хотя он все еще работает на процессорах Intel. Есть другие типы ассемблера для других процессоров, таких как ARM и RISC-V, и потенциально в будущем эти уроки будут расширены, чтобы охватить их. + +Есть два варианта синтаксиса ассемблера x86, которые вы увидите в Интернете: AT&T и Intel. Синтаксис AT&T старше и труднее читается по сравнению с синтаксисом Intel. Поэтому мы будем использовать синтаксис Intel. + +**Вспомогательные материалы** +Вы можете быть удивлены, узнав, что книги или онлайн-ресурсы, такие как Stack Overflow, не особенно полезны в качестве справочников. Это отчасти из-за нашего выбора использовать написанный вручную ассемблер с синтаксисом Intel. Но также потому, что многие онлайн-ресурсы сосредоточены на программировании операционных систем или программировании оборудования, обычно используя не-SIMD код. Ассемблер FFmpeg особенно сосредоточен на высокопроизводительной обработке изображений, и, как вы увидите, это особенно уникальный подход к программированию на ассемблере. Тем не менее, легко понять другие случаи использования ассемблера после завершения этих уроков. + +Многие книги вдаются в множество деталей архитектуры компьютера перед обучением ассемблеру. Это нормально, если вы хотите это изучить, но с нашей точки зрения, это все равно что изучать двигатели перед тем, как научиться водить машину. + +Тем не менее, диаграммы в поздних частях книги "The Art of 64-bit assembly", показывающие инструкции SIMD и их поведение в визуальной форме, полезны: [https://artofasm.randallhyde.com/](https://artofasm.randallhyde.com/) + +Доступен сервер Discord для ответов на вопросы: +[https://discord.com/invite/Ks5MhUhqfB](https://discord.com/invite/Ks5MhUhqfB) + +**Регистры** +Регистры — это области в процессоре, где могут обрабатываться данные. Процессоры не работают с памятью напрямую, а вместо этого данные загружаются в регистры, обрабатываются и записываются обратно в память. В языке ассемблера, как правило, вы не можете напрямую копировать данные из одного места памяти в другое без предварительной передачи этих данных через регистр. + +**Регистры общего назначения** +Первый тип регистра — это то, что известно как регистр общего назначения (GPR). GPR называются регистрами общего назначения, потому что они могут содержать либо данные, в данном случае до 64-битного значения, либо адрес памяти (указатель). Значение в GPR может быть обработано через операции, такие как сложение, умножение, сдвиг и т.д. + +В большинстве книг по ассемблеру целые главы посвящены тонкостям GPR, историческому фону и т.д. Это потому, что GPR важны, когда дело доходит до программирования операционных систем, обратной разработки и т.д. В коде на ассемблере, написанном в FFmpeg, GPR больше похожи на строительные леса, и большую часть времени их сложности не нужны и абстрагированы. + +**Векторные регистры** +Векторные (SIMD) регистры, как следует из названия, содержат несколько элементов данных. Существуют различные типы векторных регистров: + +* mm регистры - регистры MMX, размером 64 бита, исторические и больше не используются +* xmm регистры - регистры XMM, размером 128 бит, широко доступны +* ymm регистры - регистры YMM, размером 256 бит, некоторые сложности при их использовании +* zmm регистры - регистры ZMM, размером 512 бит, ограниченная доступность + +Большинство вычислений в сжатии и распаковке видео основаны на целых числах, поэтому мы остановимся на этом. Вот пример 16 байт в регистре xmm: + +| a | b | c | d | e | f | g | h | i | j | k | l | m | n | o | p | +| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | + +Но это могут быть восемь слов (16-битные целые числа) + +| a | b | c | d | e | f | g | h | +| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | + +Или четыре двойных слова (32-битные целые числа) + +| a | b | c | d | +| :---- | :---- | :---- | :---- | + +Или два четверных слова (64-битные целые числа): + +| a | b | +| :---- | :---- | + +Резюме: + +* **b**ytes (байты) - 8-битные данные +* **w**ords (слова) - 16-битные данные +* **d**oublewords (двойные слова) - 32-битные данные +* **q**uadwords (четверные слова) - 64-битные данные +* **d**ouble **q**uadwords (двойные четверные слова) - 128-битные данные + +Жирные символы будут важны позже. + +**Включение x86inc.asm** +Вы увидите во многих примерах, что мы включаем файл x86inc.asm. X86inc.asm — это легкий уровень абстракции, используемый в FFmpeg, x264 и dav1d, чтобы облегчить жизнь программисту на ассемблере. Он помогает во многих отношениях, но для начала одна из полезных вещей, которую он делает, — это помечает GPR, r0, r1, r2. Это означает, что вам не нужно запоминать какие-либо имена регистров. Как упоминалось ранее, GPR обычно являются просто строительными лесами, поэтому это значительно облегчает жизнь. + +**Простой скалярный фрагмент asm** + +Давайте посмотрим на простой (и очень искусственный) фрагмент скалярного asm (кода на ассемблере, который работает с отдельными элементами данных, по одному за раз, в каждой инструкции), чтобы увидеть, что происходит: + +```assembly +mov r0q, 3 +inc r0q +dec r0q +imul r0q, 5 +``` + +В первой строке *непосредственное значение* 3 (значение, хранящееся непосредственно в самом коде ассемблера, а не значение, полученное из памяти) сохраняется в регистре r0 как четверное слово. Обратите внимание, что в синтаксисе Intel исходный операнд (значение или местоположение, предоставляющее данные, расположенное справа) передается в целевой операнд (местоположение, получающее данные, расположенное слева), очень похоже на поведение memcpy. Вы также можете прочитать это как "r0q = 3", так как порядок тот же. Суффикс "q" r0 обозначает регистр как используемый в качестве четверного слова. inc увеличивает значение так, что r0q содержит 4, dec уменьшает значение обратно до 3. imul умножает значение на 5. Таким образом, в конце r0q содержит 15. + +Обратите внимание, что человекочитаемые инструкции, такие как mov и inc, которые ассемблируются в машинный код ассемблером, известны как *мнемоники*. Вы можете видеть в Интернете и в книгах мнемоники, представленные заглавными буквами, такими как MOV и INC, но они такие же, как версии в нижнем регистре. В FFmpeg мы используем мнемоники в нижнем регистре и сохраняем верхний регистр зарезервированным для макросов. + +**Понимание базовой векторной функции** + +Вот наша первая функция 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 +``` + +Давайте пройдемся по ней строка за строкой: + +```assembly +%include "x86inc.asm" +``` + +Это "заголовок", разработанный в сообществах x264, FFmpeg и dav1d для предоставления помощников, предопределенных имен и макросов (таких как cglobal ниже) для упрощения написания ассемблера. + +```assembly +SECTION .text +``` + +Это обозначает раздел, где размещается код, который вы хотите выполнить. Это в отличие от раздела .data, где вы можете разместить постоянные данные. + +```assembly +;static void add_values(uint8_t *src, const uint8_t *src2) +``` + +Первая строка — это комментарий (точка с запятой ";" в asm похожа на "//" в C), показывающий, как выглядит аргумент функции в C. Вторая строка показывает, как мы инициализируем функцию для использования регистров XMM, используя набор инструкций sse2. Это потому, что paddb — это инструкция sse2. Мы более подробно рассмотрим sse2 на следующем уроке. + +```assembly +INIT_XMM sse2 +``` + +Первая строка — это комментарий (точка с запятой ";" в asm похожа на "//" в C), показывающий, как выглядит аргумент функции в C. Вторая строка показывает, как мы инициализируем функцию для использования регистров XMM, используя набор инструкций sse2. Это потому, что paddb — это инструкция sse2. Мы более подробно рассмотрим sse2 на следующем уроке. + +```assembly +cglobal add_values, 2, 2, 2, src, src2 +``` + +Это важная строка, так как она определяет функцию C с именем "add_values". + +Давайте пройдемся по каждому элементу по очереди: + +* Следующий параметр показывает, что она имеет два аргумента функции. +* Параметр после этого показывает, что мы будем использовать два GPR в этой функции, включая аргументы. В некоторых случаях мы можем захотеть использовать больше GPR, поэтому мы должны сообщить x86util, что нам нужно больше. +* Параметр после этого говорит x86util, сколько регистров XMM мы собираемся использовать. +* Следующие два параметра — это метки для аргументов функции. + +Стоит отметить, что более старый код может не иметь меток для аргументов функции, а вместо этого обращаться к GPR напрямую, используя r0, r1 и т.д. + +```assembly + movu m0, [srcq] + movu m1, [src2q] +``` + +movu — это сокращение для movdqu (переместить двойное четверное слово без выравнивания). Выравнивание будет рассмотрено на другом уроке, но сейчас movu можно рассматривать как 128-битное перемещение из [srcq]. В случае mov скобки означают, что адрес в [srcq] разыменовывается, эквивалент **src в C.* Это то, что известно как загрузка. Обратите внимание, что суффикс "q" относится к размеру указателя *(т.е. в C он представляет *sizeof(*src) == 8 в 64-битных системах, и x86asm достаточно умен, чтобы использовать 32-битные в 32-битных системах), но базовая загрузка — 128-битная. + +Обратите внимание, что мы не ссылаемся на векторные регистры по их полному имени, в данном случае xmm0, а как m0, абстрактная форма. В будущих уроках вы увидите, как это означает, что вы можете написать код один раз и заставить его работать на нескольких размерах регистров SIMD. + +```assembly +paddb m0, m1 +``` + +paddb (читайте это в уме как *p-add-b*) добавляет каждый байт в каждом регистре, как показано ниже. Префикс "p" означает "packed" (упакованный) и используется для идентификации векторных инструкций в отличие от скалярных инструкций. Суффикс "b" показывает, что это побайтовое сложение (сложение байтов). + +| 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 +``` + +Это то, что известно как сохранение. Данные записываются обратно по адресу в указателе srcq. + +```assembly +RET +``` + +Это макрос для обозначения возврата функции. Практически все функции на ассемблере в FFmpeg изменяют данные в аргументах, а не возвращают значение. + +Как вы увидите в задании, мы создаем указатели на функции для функций на ассемблере и используем их там, где они доступны. + +[Следующий урок](../lesson_02/index.ru.md) diff --git a/lesson_02/index.ru.md b/lesson_02/index.ru.md new file mode 100644 index 0000000..b8ac588 --- /dev/null +++ b/lesson_02/index.ru.md @@ -0,0 +1,168 @@ +**Урок второй по языку ассемблера FFmpeg** + +Теперь, когда вы написали свою первую функцию на языке ассемблера, мы введем ветвления и циклы. + +Нам нужно сначала ввести идею меток и переходов. В искусственном примере ниже инструкция jmp перемещает инструкцию кода после ".loop:". ".loop:" известен как *метка*, где точка, предшествующая метке, означает, что это *локальная метка*, эффективно позволяющая вам повторно использовать одно и то же имя метки в нескольких функциях. Этот пример, конечно, показывает бесконечный цикл, но позже мы расширим его до чего-то более реалистичного. + +```assembly +mov r0q, 3 +.loop: + dec r0q + jmp .loop +``` + +Прежде чем создать реалистичный цикл, мы должны ввести регистр *FLAGS*. Мы не будем углубляться в тонкости *FLAGS* слишком сильно (опять же, потому что операции GPR в значительной степени являются строительными лесами), но есть несколько флагов, таких как Zero-Flag, Sign-Flag и Overflow-Flag, которые устанавливаются на основе вывода большинства не-mov инструкций на скалярных данных, таких как арифметические операции и сдвиги. + +Вот пример, где счетчик цикла считает вниз до нуля, и jg (переход, если больше нуля) — это условие цикла. dec r0q устанавливает FLAGS на основе значения r0q после инструкции, и вы можете переходить на основе них. + +```assembly +mov r0q, 3 +.loop: + ; сделать что-то + dec r0q + jg .loop ; перейти, если больше нуля +``` + +Это эквивалентно следующему коду C: + +```c +int i = 3; +do +{ + // сделать что-то + i--; +} while(i > 0) +``` + +Этот код C немного неестественен. Обычно цикл в C пишется так: + +```c +int i; +for(i = 0; i < 3; i++) { + // сделать что-то +} +``` + +Это примерно эквивалентно (нет простого способа соответствовать этому циклу ```for```): + +```assembly +xor r0q, r0q +.loop: + ; сделать что-то + inc r0q + cmp r0q, 3 + jl .loop ; перейти, если (r0q - 3) < 0, т.е. (r0q < 3) +``` + +Есть несколько вещей, на которые следует обратить внимание в этом фрагменте. Во-первых, это ```xor r0q, r0q```, который является обычным способом установки регистра в ноль, что на некоторых системах быстрее, чем ```mov r0q, 0```, потому что, проще говоря, фактической загрузки не происходит. Он также может быть использован на регистрах SIMD с ```pxor m0, m0``` для обнуления всего регистра. Следующее, что следует отметить, — это использование cmp. cmp эффективно вычитает второй регистр из первого (без сохранения значения где-либо) и устанавливает *FLAGS*, но, как указано в комментарии, его можно читать вместе с переходом (jl = перейти, если меньше нуля), чтобы перейти, если ```r0q < 3```. + +Обратите внимание, как в этом фрагменте есть одна дополнительная инструкция (cmp). Вообще говоря, меньше инструкций означает более быстрый код, поэтому предпочтителен более ранний фрагмент. Как вы увидите в будущих уроках, есть больше трюков, используемых для избежания этой дополнительной инструкции и установки *FLAGS* арифметической или другой операцией. Обратите внимание, как мы не пишем ассемблер для точного соответствия циклам C, мы пишем циклы, чтобы сделать их как можно быстрее в ассемблере. + +Вот некоторые распространенные мнемоники перехода, которые вы в конечном итоге будете использовать (*FLAGS* там для полноты, но вам не нужно знать специфику для написания циклов): + +| Мнемоника | Описание | FLAGS | +| :---- | :---- | :---- | +| JE/JZ | Перейти, если равно/ноль | ZF = 1 | +| JNE/JNZ | Перейти, если не равно/не ноль | ZF = 0 | +| JG/JNLE | Перейти, если больше/не меньше или равно (со знаком) | ZF = 0 and SF = OF | +| JGE/JNL | Перейти, если больше или равно/не меньше (со знаком) | SF = OF | +| JL/JNGE | Перейти, если меньше/не больше или равно (со знаком) | SF ≠ OF | +| JLE/JNG | Перейти, если меньше или равно/не больше (со знаком) | ZF = 1 or SF ≠ OF | + +**Константы** + +Давайте посмотрим на несколько примеров, показывающих, как использовать константы: + +```assembly +SECTION_RODATA + +constants_1: db 1,2,3,4 +constants_2: times 2 dw 4,3,2,1 +``` + +* SECTION_RODATA указывает, что это раздел данных только для чтения. (Это макрос, потому что различные выходные форматы файлов, которые используют операционные системы, объявляют это по-разному) +* constants_1: Метка constants_1 определена как ```db``` (объявить байт) - т.е. эквивалентно uint8_t constants_1[4] = {1, 2, 3, 4}; +* constants_2: Это использует макрос ```times 2``` для повторения объявленных слов - т.е. эквивалентно uint16_t constants_2[8] = {4, 3, 2, 1, 4, 3, 2, 1}; + +Эти метки, которые ассемблер преобразует в адрес памяти, затем могут использоваться в загрузках (но не в сохранениях, так как они только для чтения). Некоторые инструкции принимают адрес памяти в качестве операнда, поэтому они могут использоваться без явных загрузок в регистр (есть плюсы и минусы этого). + +**Смещения** + +Смещения — это расстояние (в байтах) между последовательными элементами в памяти. Смещение определяется **размером каждого элемента** в структуре данных. + +Теперь, когда мы можем писать циклы, пришло время получать данные. Но есть некоторые отличия по сравнению с C. Давайте посмотрим на следующий цикл в C: + +```c +uint32_t data[3]; +int i; +for(i = 0; i < 3; i++) { + data[i]; +} +``` + +4-байтовое смещение между элементами data предварительно рассчитывается компилятором C. Но при написании ассемблера вручную вам нужно рассчитывать эти смещения самостоятельно. + +Давайте посмотрим на синтаксис для вычислений адресов памяти. Это применяется ко всем типам адресов памяти: + +```assembly +[base + scale*index + disp] +``` + +* base - Это GPR (обычно указатель из аргумента функции C) +* scale - Это может быть 1, 2, 4, 8. 1 по умолчанию +* index - Это GPR (обычно счетчик цикла) +* disp - Это целое число (до 32-бит). Смещение — это смещение в данных + +x86asm предоставляет константу mmsize, которая позволяет узнать размер регистра SIMD, с которым вы работаете. + +Вот простой (и бессмысленный) пример для иллюстрации загрузки из пользовательских смещений: + +```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] + + ; сделать что-то + + add srcq, mmsize +dec r1q +jg .loop + +RET +``` + +Обратите внимание, как в ```movu m1, [srcq+2*r1q+3+mmsize]``` ассемблер предварительно рассчитает правильную константу смещения для использования. На следующем уроке мы покажем вам трюк, чтобы избежать необходимости делать add и dec в цикле, заменив их одним add. + +**LEA** + +Теперь, когда вы понимаете смещения, вы можете использовать lea (Load Effective Address, загрузить эффективный адрес). Это позволяет вам выполнять умножение и сложение с помощью одной инструкции, что будет быстрее, чем использование нескольких инструкций. Конечно, есть ограничения на то, на что вы можете умножать и что добавлять, но это не мешает lea быть мощной инструкцией. + +```assembly +lea r0q, [base + scale*index + disp] +``` + +Вопреки названию, LEA может использоваться как для обычной арифметики, так и для вычислений адресов. Вы можете сделать что-то настолько сложное, как: + +```assembly +lea r0q, [r1q + 8*r2q + 5] +``` + +Обратите внимание, что это не влияет на содержимое r1q и r2q. Это также не влияет на *FLAGS* (поэтому вы не можете переходить на основе вывода). Использование LEA избегает всех этих инструкций и временных регистров (этот код не эквивалентен, потому что add изменяет *FLAGS*): + +```assembly +movq r0q, r1q +movq r3q, r2q +sal r3q, 3 ; сдвиг арифметический влево 3 = * 8 +add r3q, 5 +add r0q, r3q +``` + +Вы увидите lea, используемый много для настройки адресов перед циклами или выполнения вычислений, подобных приведенным выше. Обратите внимание, конечно, что вы не можете делать все типы умножения и сложения, но умножения на 1, 2, 4, 8 и сложение фиксированного смещения распространены. + +В задании вам нужно будет загрузить константу и добавить значения к вектору SIMD в цикле. + +[Следующий урок](../lesson_03/index.ru.md) diff --git a/lesson_03/index.ru.md b/lesson_03/index.ru.md new file mode 100644 index 0000000..f8a02d8 --- /dev/null +++ b/lesson_03/index.ru.md @@ -0,0 +1,200 @@ +**Урок третий по языку ассемблера FFmpeg** + +Давайте объясним еще немного жаргона и дадим вам краткий исторический урок. + +**Наборы инструкций** + +Возможно, вы видели в предыдущем уроке, что мы говорили о SSE2, который является набором SIMD инструкций. Когда выпускается новое поколение процессора, оно может поставляться с новыми инструкциями и иногда с регистрами большего размера. История набора инструкций x86 очень сложна, поэтому это упрощенная история (существует намного больше подкategorий): + +* MMX - Запущен в 1997 году, первый SIMD в процессорах Intel, 64-битные регистры, исторический +* SSE (Streaming SIMD Extensions) - Запущен в 1999 году, 128-битные регистры +* SSE2 - Запущен в 2000 году, много новых инструкций +* SSE3 - Запущен в 2004 году, первые горизонтальные инструкции +* SSSE3 (Supplemental SSE3) - Запущен в 2006 году, новые инструкции, но самое важное - инструкция перемешивания pshufb, возможно, самая важная инструкция в обработке видео +* SSE4 - Запущен в 2008 году, много новых инструкций, включая упакованные минимум и максимум +* AVX - Запущен в 2011 году, 256-битные регистры (только float) и новый синтаксис с тремя операндами +* AVX2 - Запущен в 2013 году, 256-битные регистры для целочисленных инструкций +* AVX512 - Запущен в 2017 году, 512-битные регистры, новая функция операционной маски. Они имели ограниченное использование в то время в FFmpeg из-за снижения частоты процессора при использовании новых инструкций. Полное 512-битное перемешивание (перестановка) с vpermb. +* AVX512ICL - Запущен в 2019 году, больше нет снижения тактовой частоты +* AVX10 - Предстоящий + +Стоит отметить, что наборы инструкций могут быть удалены так же, как и добавлены в процессоры. Например, AVX512 был [удален](https://www.igorslab.de/en/intel-deactivated-avx-512-on-alder-lake-but-fully-questionable-interpretation-of-efficiency-news-editorial/), что вызвало споры, в процессорах Intel 12-го поколения. По этой причине FFmpeg выполняет обнаружение возможностей процессора во время выполнения. FFmpeg обнаруживает возможности процессора, на котором он запущен. + +Как вы видели в задании, указатели на функции являются C по умолчанию и заменяются конкретным вариантом набора инструкций. Это означает, что обнаружение выполняется один раз и затем никогда больше не требуется. Это в отличие от многих проприетарных приложений, которые жестко кодируют конкретный набор инструкций, делая совершенно функциональный компьютер устаревшим. Это также позволяет включать/выключать оптимизированные функции во время выполнения. Это одно из больших преимуществ открытого исходного кода. + +Программы, такие как FFmpeg, используются на миллиардах устройств по всему миру, некоторые из которых могут быть очень старыми. FFmpeg технически поддерживает машины, поддерживающие только SSE, которым 25 лет! К счастью, x86inc.asm способен сообщить вам, если вы используете инструкцию, которая недоступна в конкретном наборе инструкций. + +Чтобы дать вам представление о реальных возможностях, вот доступность набора инструкций из [Steam Survey](https://store.steampowered.com/hwsurvey/Steam-Hardware-Software-Survey-Welcome-to-Steam) на ноябрь 2024 года (это, очевидно, смещено в сторону геймеров): + +| Набор инструкций | Доступность | +| :---- | :---- | +| SSE2 | 100% | +| SSE3 | 100% | +| SSSE3 | 99.86% | +| SSE4.1 | 99.80% | +| AVX | 97.39% | +| AVX2 | 94.44% | +| AVX512 (Steam не разделяет между AVX512 и AVX512ICL) | 14.09% | + +Для такого приложения, как FFmpeg с миллиардами пользователей, даже 0.1% - это очень большое количество пользователей и сообщений об ошибках, если что-то ломается. FFmpeg имеет обширную инфраструктуру тестирования для проверки вариаций ЦП/ОС/Компилятор в нашем [тестовом наборе FATE](https://fate.ffmpeg.org/?query=subarch:x86_64%2F%2F). Каждый отдельный коммит запускается на сотнях машин, чтобы убедиться, что ничего не ломается. + +Intel предоставляет подробное руководство по набору инструкций здесь: [https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html) + +Поиск по PDF может быть громоздким, поэтому есть неофициальная веб-альтернатива здесь: [https://www.felixcloutier.com/x86/](https://www.felixcloutier.com/x86/) + +Также доступно визуальное представление SIMD инструкций здесь: +[https://www.officedaytime.com/simd512e/](https://www.officedaytime.com/simd512e/) + +Часть вызова ассемблера x86 заключается в поиске правильной инструкции для ваших нужд. В некоторых случаях инструкции могут использоваться способом, для которого они изначально не предназначались. + +**Трюк со смещением указателя** + +Давайте вернемся к нашей оригинальной функции из Урока 1, но добавим аргумент width к функции C. + +Мы используем ptrdiff_t для переменной width вместо int, чтобы убедиться, что верхние 32 бита 64-битного аргумента равны нулю. Если бы мы напрямую передали int width в сигнатуре функции, а затем попытались бы использовать его как quad для арифметики указателей (т.е. используя `widthq`), верхние 32 бита регистра могли бы быть заполнены произвольными значениями. Мы могли бы исправить это, расширив знак width с помощью `movsxd` (также см. макрос `movsxdifnidn` в x86inc.asm), но это более простой способ. + +Функция ниже содержит трюк со смещением указателя: + +```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 +``` + +Давайте пройдем через это шаг за шагом, поскольку это может сбивать с толку: + +```assembly + add srcq, widthq + add src2q, widthq + neg widthq +``` + +Ширина добавляется к каждому указателю таким образом, что каждый указатель теперь указывает на конец буфера, который должен быть обработан. Затем ширина инвертируется. + +```assembly + movu m0, [srcq+widthq] + movu m1, [src2q+widthq] +``` + +Загрузки затем выполняются с widthq, являющимся отрицательным. Таким образом, на первой итерации [srcq+widthq] указывает на исходный адрес srcq, т.е. указывает обратно на начало буфера. + +```assembly + add widthq, mmsize + jl .loop +``` + +mmsize добавляется к отрицательному widthq, приближая его к нулю. Условие цикла теперь jl (переход, если меньше нуля). Этот трюк означает, что widthq используется как смещение указателя **и** как счетчик цикла одновременно, экономя инструкцию cmp. Это также позволяет использовать смещение указателя в нескольких загрузках и сохранениях, а также использовать кратные смещения указателей при необходимости (запомните это для задания). + +**Выравнивание** + +Во всех наших примерах мы использовали movu, чтобы избежать темы выравнивания. Многие процессоры могут загружать и сохранять данные быстрее, если данные выровнены, т.е. если адрес памяти делится на размер SIMD регистра. Где это возможно, мы стараемся использовать выровненные загрузки и сохранения в FFmpeg, используя mova. + +В FFmpeg av_malloc может предоставить выровненную память в куче, а директива препроцессора C DECLARE_ALIGNED может предоставить выровненную память в стеке. Если mova используется с невыровненным адресом, это вызовет ошибку сегментации, и приложение аварийно завершится. Также важно убедиться, что значение выравнивания соответствует размеру SIMD регистра, т.е. 16 с xmm, 32 для ymm и 64 для zmm. + +Вот как выровнять начало секции RODATA на 64 байта: + +```assembly +SECTION_RODATA 64 +``` + +Обратите внимание, что это выравнивает только начало RODATA. Могут потребоваться байты заполнения, чтобы убедиться, что следующая метка остается на 64-байтовой границе. + +**Расширение диапазона** + +Другая тема, которую мы избегали до сих пор, - это переполнение. Это происходит, например, когда значение байта превышает 255 после операции, такой как сложение или умножение. Мы можем захотеть выполнить операцию, где нам нужно промежуточное значение больше байта (например, слова), или потенциально мы хотим оставить данные в этом большем промежуточном размере. + +Для беззнаковых байтов именно здесь появляются punpcklbw (упакованная распаковка младших байтов в слова) и punpckhbw (упакованная распаковка старших байтов в слова). + +Давайте посмотрим, как работает punpcklbw. Синтаксис для версии SSE2 из руководства Intel выглядит следующим образом: + +| PUNPCKLBW xmm1, xmm2/m128 | +| :---- | + +Это означает, что его источник (правая сторона) может быть xmm регистром или адресом памяти (m128 означает адрес памяти со стандартным синтаксисом [base + scale*index + disp]), а назначением - xmm регистр. + +Веб-сайт officedaytime.com выше имеет хорошую диаграмму, показывающую, что происходит: + +![What is this](image1.png) + +Вы можете видеть, что байты чередуются из нижней половины каждого регистра соответственно. Но какое отношение это имеет к расширению диапазона? Если регистр src состоит из всех нулей, это чередует байты в dst с нулями. Это то, что известно как *расширение нулем*, поскольку байты беззнаковые. punpckhbw может использоваться для того же самого со старшими байтами. + +Вот фрагмент, показывающий, как это делается: + +```assembly +pxor m2, m2 ; обнулить m2 + +movu m0, [srcq] +movu m1, m0 ; сделать копию m0 в m1 +punpcklbw m0, m2 +punpckhbw m1, m2 +``` + +```m0``` и ```m1``` теперь содержат исходные байты, расширенные нулем до слов. В следующем уроке вы увидите, как инструкции с тремя операндами в AVX делают второй movu ненужным. + +**Расширение знака** + +Знаковые данные немного сложнее. Для расширения диапазона знакового целого числа нам нужно использовать процесс, известный как [расширение знака](https://en.wikipedia.org/wiki/Sign_extension). Это заполняет MSB знаковым битом. Например: -2 в int8_t равно 0b11111110. Для расширения знака до int16_t MSB 1 повторяется, чтобы получить 0b1111111111111110. + +```pcmpgtb``` (упакованное сравнение больше чем байт) может использоваться для расширения знака. Выполняя сравнение (0 > байт), все биты в байте назначения устанавливаются в 1, если байт отрицательный, иначе биты в байте назначения устанавливаются в 0. punpckX может использоваться, как указано выше, для выполнения расширения знака. Если байт отрицательный, соответствующий байт равен 0b11111111, иначе это 0x00000000. Чередование значения байта с выводом pcmpgtb выполняет расширение знака до слова в результате. + +```assembly +pxor m2, m2 ; обнулить m2 + +movu m0, [srcq] +movu m1, m0 ; сделать копию m0 в m1 + +pcmpgtb m2, m0 +punpcklbw m0, m2 +punpckhbw m1, m2 +``` + +Как вы можете видеть, есть дополнительная инструкция по сравнению с беззнаковым случаем. + +**Упаковка** + +packuswb (упаковка беззнакового слова в байт) и packsswb позволяют вам перейти от слова к байту. Это позволяет вам чередовать два SIMD регистра, содержащих слова, в один SIMD регистр с байтом. Обратите внимание, что если значения превышают диапазон байта, они будут насыщены (т.е. ограничены наибольшим значением). + +**Перемешивания** + +Перемешивания, также известные как перестановки, возможно, являются самой важной инструкцией в обработке видео, а pshufb (упакованное перемешивание байтов), доступное в SSSE3, является самым важным вариантом. + +Для каждого байта соответствующий исходный байт используется как индекс регистра назначения, за исключением случаев, когда MSB установлен, байт назначения обнуляется. Это аналогично следующему коду C (хотя в SIMD все 16 итераций цикла происходят параллельно): + +```c +for(int i = 0; i < 16; i++) { + if(src[i] & 0x80) + dst[i] = 0; + else + dst[i] = dst[src[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 ; перемешать m0 на основе m1 +``` + +Обратите внимание, что -1 для удобства чтения используется как индекс перемешивания для обнуления выходного байта: -1 как байт является битовым полем 0b11111111 (дополнение до двух), и таким образом MSB (0x80) установлен. \ No newline at end of file