From 87c7846ca3707d0b0e34e6b6b3293f8ec7ba3300 Mon Sep 17 00:00:00 2001 From: Zachary Wang Date: Wed, 2 Apr 2025 20:49:27 +1100 Subject: [PATCH 01/11] Simplified and traditional Chinese for lesson 1 --- lesson_01/index_zh-hans.md | 225 +++++++++++++++++++++++++++++++++++++ lesson_01/index_zh-hant.md | 215 +++++++++++++++++++++++++++++++++++ 2 files changed, 440 insertions(+) create mode 100644 lesson_01/index_zh-hans.md create mode 100644 lesson_01/index_zh-hant.md diff --git a/lesson_01/index_zh-hans.md b/lesson_01/index_zh-hans.md new file mode 100644 index 0000000..e952252 --- /dev/null +++ b/lesson_01/index_zh-hans.md @@ -0,0 +1,225 @@ +**FFmpeg 汇编语言第一课** + +**介绍** + +欢迎来到FFmpeg汇编语言学校。你已经向着编程中最有趣、最具挑战性和最有回报的旅程迈出了第一步。这些课程将为你奠定FFmpeg中汇编语言的编写基础,并让你了解计算机中实际发生的情况。 + +**所需知识** + +* C语言知识,特别是指针。如果你不了解C语言,建议学习[《C程序设计语言》](https://zh.wikipedia.org/wiki/C程序设计语言_(书))一书 +* 高中数学(标量与向量、加法、乘法等) + +**什么是汇编语言 (Assembly language)?** + +汇编语言 (Assembly language) 是一种编程语言,你编写的代码直接对应于CPU处理的指令。人类可读的汇编语言,顾名思义,被*汇编 (assembled)* 成CPU能够理解的二进制数据,称为*机器码 (machine code)*。你可能会看到汇编语言代码被简称为"assembly"或"asm"。 + +FFmpeg中的绝大多数汇编代码都是所谓的*SIMD,单指令多数据 (Single Instruction Multiple Data)*。SIMD有时也被称为向量编程 (vector programming)。这意味着特定指令能同时对多个数据元素进行操作。大多数编程语言一次只处理一个数据元素,称为标量编程 (scalar programming)。 + +你可能已经猜到,SIMD非常适合处理图像、视频和音频,因为这些数据在内存中按顺序排列。CPU中有专门的指令帮助我们处理顺序数据。 + +在FFmpeg中,你会看到"汇编函数 (assembly function)"、"SIMD"和"向量(化)"这些术语被互换使用。它们都指的是同一件事:通过手写汇编语言函数一次性地处理多个数据元素。一些项目也可能将这些称为"汇编内核 (assembly kernels)"。 + +所有这些听起来可能比较复杂,但谨记,在FFmpeg项目中,高中生也编写了汇编代码。与所有事物一样,学习是50%术语和50%实际学习。 + +**为什么我们用汇编语言编写?** + +为了使多媒体处理得更快。编写汇编代码通常能带来10倍或以上的速度提升,这对于实时播放视频而不卡顿尤为重要。它还能节省能源并延长电池寿命。值得注意的是,视频编码和解码是全球最常用的功能之一,无论是普通用户还是大型企业的数据中心都用到它们。因此,即使是微小的改进,也能迅速积累出显著的效果。 + +在网上,你会经常看到人们使用*内联函数 (intrinsics)*,这些C语言风格的函数映射到汇编指令,以便更快地开发。在 FFmpeg 中,我们不使用内联函数,而是手写汇编代码。这是一个有争议的部分,但内联函数通常比手写汇编慢约10-15%(内联函数支持者可能会不同意),这取决于编译器。对于 FFmpeg 来说,每一点额外的性能提升都有用,这就是为什么我们直接使用汇编代码。还有一种观点认为,内联函数由于使用了"[匈牙利命名法](https://zh.wikipedia.org/wiki/匈牙利命名法)"而难以阅读。 + +出于历史原因,你可能还会在 FFmpeg 的一些地方看到*内联汇编 (inline assembly)*(即不使用内联函数),或者在 Linux 内核等项目中因为非常特殊的应用场景而看到内联汇编。这是指汇编代码不写在单独的文件中,在像 FFmpeg 这样的项目中,主流观点认为这种代码难以阅读,缺乏广泛的编译器支持,并且难以维护。 + +最后,你会看到很多自称专家的人在网上说这些都是没必要的,编译器可以为你完成所有这些"向量化"工作。至少以学习的目的,请忽视他们:例如,[dav1d项目](https://www.videolan.org/projects/dav1d.html)的最近测试显示,这种自动向量化带来了约2倍的速度提升,而手写版本可以达到8倍。 + +**汇编语言的风格** + +本课程将专注于 x86 64 位汇编语言,它也被称为 amd64,尽管它同样适用于 Intel 处理器。此外,不同的 CPU 也有其他类型的汇编语言,例如 ARM 和 RISC-V,未来这些课程可能会扩展到涵盖这些架构。 + +网上常见的 x86 汇编风格有两种:AT&T 和 Intel。AT&T 语法较早,但相较于 Intel 语法更难阅读。因此,我们将使用 Intel 风格。 + +**辅助材料** +你可能会惊讶地发现,书籍或在线资源(如 Stack Overflow)作为参考并不是特别有用。部分原因是我们选择手写 Intel 汇编。但也因为很多在线资源都集中在操作系统编程或硬件编程上,通常使用非 SIMD 代码。而 FFmpeg 的汇编特别侧重于高性能图像处理,正如你将看到的,它采用了一种独特的汇编编程方法。不过,一旦你完成这些课程,理解其他汇编应用场景就很容易了。 + +许多书籍在教汇编之前会深入介绍计算机架构细节。如果你想学这些,那很好,但从我们的角度来看,这就像在学习开车之前研究发动机。 + +话虽如此,《64位汇编的艺术》 (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) + +**寄存器 (Register)** +寄存器是 CPU 内用于处理数据的区域。CPU 并不会直接对内存进行操作,而是先将数据加载到寄存器中进行处理,然后再写回内存。在汇编语言中,通常不能直接在两个内存位置之间复制数据,而必须先通过寄存器中转。 + +**通用寄存器 (General Purpose Register)** +第一种寄存器被称为通用寄存器(GPR)。之所以称为‘通用’,是因为它们既可以存储数据(最大支持 64 位值),也可以存储内存地址(指针)。GPR 中的值可以进行各种操作,如加法、乘法、位移等。 + +在大多数汇编书籍中,会有整章内容专门讲解 GPR 的细节和历史背景等。这是因为在操作系统编程、逆向工程等领域,GPR 是非常重要的。然而,在 FFmpeg 中编写的汇编代码里,GPR 更像是脚手架,大多数情况下不需要它们的复杂性,并且被抽象出来。 + +**向量寄存器 (Vector registers)** +向量 (SIMD) 寄存器,顾名思义,包含多个数据元素。不同类型的向量寄存器包括: + +* mm registers - MMX registers, 64-bit sized, historic and not used much any more +* xmm registers - XMM registers, 128-bit sized, widely available +* ymm registers - YMM registers, 256-bit sized, some complications when using these +* zmm registers - ZMM registers, 512-bit sized, limited availability +* mm寄存器 - MMX寄存器,64位大小,历史悠久,目前已不常使用 +* xmm寄存器 - XMM寄存器,128位大小,广泛可用 +* ymm寄存器 - YMM寄存器,256位大小,使用时有一些复杂性 +* zmm寄存器 - ZMM寄存器,512位大小,可用性有限 + +视频压缩和解压缩中的大多数计算是基于整数的,因此我们将继续使用整数类型。以下是 xmm 寄存器中 16 字节 (bytes) 数据的一个例子: + +| a | b | c | d | e | f | g | h | i | j | k | l | m | n | o | p | +| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | + +但它也可以是八个字 (words)(16位 (bit) 整数) + +| a | b | c | d | e | f | g | h | +| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | + +或四个双字 (double words)(32位整数) + +| a | b | c | d | +| :---- | :---- | :---- | :---- | + +或两个四字 (quadwords)(64位整数): + +| a | b | +| :---- | :---- | + +回顾一下: + + +* **b**ytes 字节 - 8位数据 +* **w**ords 字 - 16位数据 +* **d**oublewords 双字 - 32位数据 +* **q**uadwords 四字 - 64位数据 +* **d**ouble **q**uadwords 双四字 - 128位数据 + +加粗的字符稍后会很重要。 + +**x86inc.asm 包含文件** +You’ll see in many examples we include the file x86inc.asm. X86inc.asm is a lightweight abstraction layer used in FFmpeg, x264, and dav1d to make an assembly programmer's life easier. It helps in many ways, but to begin with, one of the useful things it does is it labels GPRs, r0, r1, r2. This means you don’t have to remember any register names. As mentioned before, GPRs are generally just scaffolding so this makes life a lot easier. + +在许多示例中,你会看到我们包含了文件 x86inc.asm。x86inc.asm 是 FFmpeg、x264 和 dav1d 中使用的轻量级抽象层,旨在简化汇编程序员的工作。它在许多方面提供帮助,但首先,它的一个有用功能是给 GPR 寄存器标记为 r0、r1、r2。这样,你就不必记住任何寄存器的名称。如前所述,GPR 通常只是脚手架,所以这种做法大大简化了编程过程。 + +**一个简单的标量汇编 (scalar asm) 片段** + +Let’s look at a simple (and very much artificial) snippet of scalar asm (assembly code that operates on individual data items, one at a time, within each instruction) to see what’s going on: + +让我们看一段简单(且非常人工构造)的标量汇编代码(每条指令一次只对单个数据项进行操作的汇编代码),看看发生了什么: + +```assembly +mov r0q, 3 +inc r0q +dec r0q +imul r0q, 5 +``` + +在第一行中,*立即数 (immediate value)* 3(直接存储在汇编代码中的值,而不是从内存中获取的值)被存储到寄存器 r0 中,作为一个四字(quadword)值。注意,在 Intel 风格中,源操作数(提供数据的值或位置,位于右侧)被传送到目标操作数(接收数据的位置,位于左侧),类似于 memcpy 的行为。你也可以理解为‘r0q = 3’,因为顺序是相同的。r0 的 ‘q’ 后缀表示该寄存器作为四字 (**q**uadword) 来用。‘inc’ 操作将值增加,使 r0q 包含 4,‘dec’ 操作将值减少回 3,‘imul’ 将值乘以 5。因此,最终 r0q 包含 15。 + +请注意,汇编器 (assembler) 将人类可读的指令(如 mov 和 inc)汇编成机器代码,这些被称为*助记符 (mnemonics)*。你可能会在网上和书籍中看到助记符以大写字母表示,如 MOV 和 INC,但它们与小写版本完全相同。在 FFmpeg 中,我们使用小写助记符,并将大写保留给宏 (macro) 定义。 + +**理解基本的向量函数 (vector function)** + +以下是我们的第一个 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 部分相对,你可以在 .data 部分放置常量数据。 + +```assembly +;static void add_values(uint8_t *src, const uint8_t *src2) +INIT_XMM sse2 +``` + +第一行是注释(在汇编中,分号 ‘;’ 的作用类似于 C 语言中的 ‘//’),用于展示该函数的 C 语言参数形式。第二行用于初始化函数,使其能够使用 XMM 寄存器,并指定使用 sse2 指令集。这是因为 paddb 是一个 sse2 指令。我们将在下一课中更详细地介绍 sse2。 + +```assembly +cglobal add_values, 2, 2, 2, src, src2 +``` + +这一行至关重要,它定义了一个名为 add_values 的 C 语言函数。 + +让我们逐项解析: + +* 下一个参数指定了该函数有两个参数。 +* 再下一个参数指定这两个参数将使用两个 GPR 进行传递。在某些情况下,我们可能需要更多的 GPR,因此必须告诉 x86util 我们需要额外的寄存器。 +* 再下一个参数指定了 x86util 需要多少个 XMM 寄存器。 +* 最后两个参数是函数参数的标签。 + +值得注意的是,旧代码可能不会为函数参数添加标签,而是直接使用 r0、r1 等通用寄存器(GPR)来访问参数。 + +```assembly + movu m0, [srcq] + movu m1, [src2q] +``` + +movu 是 movdqu(move double quad unaligned 移动双四字未对齐)的简写。关于对齐的内容我们会在后续课程中讲解,但目前可以将 movu 视为从 [srcq] 进行 128 位数据传输。对于 mov 指令,方括号表示对 [srcq] 指向的地址进行解引用,相当于 *C 语言中的 \*src*,这就是所谓的‘加载’(load)。需要注意的是,q 后缀表示指针的大小(即在 C 语言中在 64 位系统上 sizeof(*src) == 8,x86 汇编在 32 位系统上则会自动调整为 32 位),但实际加载的数据仍然是 128 位的。 + +需要注意的是,我们并不使用向量寄存器的完整名称(例如 xmm0),而是使用抽象形式的 m0。在后续课程中,你会看到这种方式的优势 —— 它允许你编写一次代码,并在不同大小的 SIMD 寄存器上通用适用。 + +```assembly +paddb m0, m1 +``` + +paddb (read this in your head as *p-add-b*) is adding each byte in each register as shown below. The “p” prefix stands for “packed” and is used to identify vector instructions vs scalar instructions. The “b” suffix shows that this is bytewise addition (addition of bytes). + +paddb(可以在脑海中读作 *p-add-b*)会对每个寄存器中的每个字节进行相加,如下所示。“p” 前缀代表 “packed”(打包),用于区分向量指令和标量指令。“b” 后缀表示这是 按字节(bytewise) 进行的加法运算。 + +| 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 +``` + +这是一个表示函数返回的宏 (macro)。在 FFmpeg 中,几乎所有汇编函数都会直接修改参数中的数据,而不是返回一个值。 + +在后续的作业中,你会看到,我们会为汇编函数创建函数指针,并在可用的地方使用它们。 + +[下一课](../lesson_02/index_zh-hans.md) diff --git a/lesson_01/index_zh-hant.md b/lesson_01/index_zh-hant.md new file mode 100644 index 0000000..e4f45d1 --- /dev/null +++ b/lesson_01/index_zh-hant.md @@ -0,0 +1,215 @@ +**FFmpeg 組合語言第一課** + +**介紹** + +歡迎來到FFmpeg組合語言學校。你已經朝著程式設計中最有趣、最具挑戰性和最有回報的旅程邁出了第一步。這些課程將為你奠定FFmpeg中組合語言的編寫基礎,並讓你了解電腦中實際發生的情況。 + +**所需知識** + +* C語言知識,特別是指標。如果你不了解C語言,建議學習[《C程式設計語言》](https://zh.wikipedia.org/wiki/C程序设计语言_(书))一書 +* 高中數學(純量與向量、加法、乘法等) + +**什麼是組合語言 (Assembly language)?** + +組合語言 (Assembly language) 是一種程式設計語言,你編寫的程式碼直接對應於CPU處理的指令。人類可讀的組合語言,顧名思義,被*組譯 (assembled)* 成CPU能夠理解的二進位資料,稱為*機器碼 (machine code)*。你可能會看到組合語言程式碼被簡稱為"assembly"或"asm"。 + +FFmpeg中的絕大多數組合程式碼都是所謂的*SIMD,單指令多資料 (Single Instruction Multiple Data)*。SIMD有時也被稱為向量程式設計 (vector programming)。這意味著特定指令能同時對多個資料元素進行操作。大多數程式設計語言一次只處理一個資料元素,稱為純量程式設計 (scalar programming)。 + +你可能已經猜到,SIMD非常適合處理影像、視訊和音訊,因為這些資料在記憶體中按順序排列。CPU中有專門的指令幫助我們處理順序資料。 + +在FFmpeg中,你會看到"組合函式 (assembly function)"、"SIMD"和"向量(化)"這些術語被互換使用。它們都指的是同一件事:通過手寫組合語言函式一次性地處理多個資料元素。一些專案也可能將這些稱為"組合核心 (assembly kernels)"。 + +所有這些聽起來可能比較複雜,但謹記,在FFmpeg專案中,高中生也編寫了組合程式碼。與所有事物一樣,學習是50%術語和50%實際學習。 + +**為什麼我們用組合語言編寫?** + +為了使多媒體處理得更快。編寫組合程式碼通常能帶來10倍或以上的速度提升,這對於即時播放視訊而不卡頓尤為重要。它還能節省能源並延長電池壽命。值得注意的是,視訊編碼和解碼是全球最常用的功能之一,無論是普通使用者還是大型企業的資料中心都用到它們。因此,即使是微小的改進,也能迅速積累出顯著的效果。 + +在網路上,你會經常看到人們使用*內聯函式 (intrinsics)*,這些C語言風格的函式映射到組合指令,以便更快地開發。在 FFmpeg 中,我們不使用內聯函式,而是手寫組合程式碼。這是一個有爭議的部分,但內聯函式通常比手寫組合慢約10-15%(內聯函式支持者可能會不同意),這取決於編譯器。對於 FFmpeg 來說,每一點額外的效能提升都有用,這就是為什麼我們直接使用組合程式碼。還有一種觀點認為,內聯函式由於使用了"[匈牙利命名法](https://zh.wikipedia.org/wiki/匈牙利命名法)"而難以閱讀。 + +出於歷史原因,你可能還會在 FFmpeg 的一些地方看到*內聯組合 (inline assembly)*(即不使用內聯函式),或者在 Linux 核心等專案中因為非常特殊的應用場景而看到內聯組合。這是指組合程式碼不寫在單獨的檔案中,在像 FFmpeg 這樣的專案中,主流觀點認為這種程式碼難以閱讀,缺乏廣泛的編譯器支援,並且難以維護。 + +最後,你會看到很多自稱專家的人在網路上說這些都是沒必要的,編譯器可以為你完成所有這些"向量化"工作。至少以學習的目的,請忽視他們:例如,[dav1d專案](https://www.videolan.org/projects/dav1d.html)的最近測試顯示,這種自動向量化帶來了約2倍的速度提升,而手寫版本可以達到8倍。 + +**組合語言的風格** + +本課程將專注於 x86 64 位組合語言,它也被稱為 amd64,儘管它同樣適用於 Intel 處理器。此外,不同的 CPU 也有其他類型的組合語言,例如 ARM 和 RISC-V,未來這些課程可能會擴展到涵蓋這些架構。 + +網路上常見的 x86 組合風格有兩種:AT&T 和 Intel。AT&T 語法較早,但相較於 Intel 語法更難閱讀。因此,我們將使用 Intel 風格。 + +**輔助材料** +你可能會驚訝地發現,書籍或線上資源(如 Stack Overflow)作為參考並不是特別有用。部分原因是我們選擇手寫 Intel 組合。但也因為很多線上資源都集中在作業系統程式設計或硬體程式設計上,通常使用非 SIMD 程式碼。而 FFmpeg 的組合特別側重於高效能圖像處理,正如你將看到的,它採用了一種獨特的組合程式設計方法。不過,一旦你完成這些課程,理解其他組合應用場景就很容易了。 + +許多書籍在教組合之前會深入介紹電腦架構細節。如果你想學這些,那很好,但從我們的角度來看,這就像在學習開車之前研究引擎。 + +話雖如此,《64位組合的藝術》 (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) + +**暫存器 (Register)** +暫存器是 CPU 內用於處理資料的區域。CPU 並不會直接對記憶體進行操作,而是先將資料載入到暫存器中進行處理,然後再寫回記憶體。在組合語言中,通常不能直接在兩個記憶體位置之間複製資料,而必須先通過暫存器中轉。 + +**通用暫存器 (General Purpose Register)** +第一種暫存器被稱為通用暫存器(GPR)。之所以稱為'通用',是因為它們既可以儲存資料(最大支援 64 位元值),也可以儲存記憶體位址(指標)。GPR 中的值可以進行各種操作,如加法、乘法、位移等。 + +在大多數組合書籍中,會有整章內容專門講解 GPR 的細節和歷史背景等。這是因為在作業系統程式設計、逆向工程等領域,GPR 是非常重要的。然而,在 FFmpeg 中編寫的組合程式碼裡,GPR 更像是鷹架,大多數情況下不需要它們的複雜性,並且被抽象出來。 + +**向量暫存器 (Vector registers)** +向量 (SIMD) 暫存器,顧名思義,包含多個資料元素。不同類型的向量暫存器包括: + +* mm暫存器 - MMX暫存器,64位元大小,歷史悠久,目前已不常使用 +* xmm暫存器 - XMM暫存器,128位元大小,廣泛可用 +* ymm暫存器 - YMM暫存器,256位元大小,使用時有一些複雜性 +* zmm暫存器 - ZMM暫存器,512位元大小,可用性有限 + +視訊壓縮和解壓縮中的大多數計算是基於整數的,因此我們將繼續使用整數類型。以下是 xmm 暫存器中 16 位元組 (bytes) 資料的一個例子: + +| a | b | c | d | e | f | g | h | i | j | k | l | m | n | o | p | +| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | + +但它也可以是八個字 (words)(16位元 (bit) 整數) + +| a | b | c | d | e | f | g | h | +| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | + +或四個雙字 (double words)(32位元整數) + +| a | b | c | d | +| :---- | :---- | :---- | :---- | + +或兩個四字 (quadwords)(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 通常只是鷹架,所以這種做法大大簡化了程式設計過程。 + +**一個簡單的純量組合 (scalar asm) 片段** + +讓我們看一段簡單(且非常人工構造)的純量組合程式碼(每條指令一次只對單個資料項進行操作的組合程式碼),看看發生了什麼: + +```assembly +mov r0q, 3 +inc r0q +dec r0q +imul r0q, 5 +``` + +在第一行中,*立即值 (immediate value)* 3(直接儲存在組合程式碼中的值,而不是從記憶體中獲取的值)被儲存到暫存器 r0 中,作為一個四字(quadword)值。注意,在 Intel 風格中,來源運算元(提供資料的值或位置,位於右側)被傳送到目標運算元(接收資料的位置,位於左側),類似於 memcpy 的行為。你也可以理解為'r0q = 3',因為順序是相同的。r0 的 'q' 後綴表示該暫存器作為四字 (**q**uadword) 來用。'inc' 操作將值增加,使 r0q 包含 4,'dec' 操作將值減少回 3,'imul' 將值乘以 5。因此,最終 r0q 包含 15。 + +請注意,組譯器 (assembler) 將人類可讀的指令(如 mov 和 inc)組譯成機器碼,這些被稱為*助記符 (mnemonics)*。你可能會在網路上和書籍中看到助記符以大寫字母表示,如 MOV 和 INC,但它們與小寫版本完全相同。在 FFmpeg 中,我們使用小寫助記符,並將大寫保留給巨集 (macro) 定義。 + +**理解基本的向量函式 (vector function)** + +以下是我們的第一個 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 部分相對,你可以在 .data 部分放置常數資料。 + +```assembly +;static void add_values(uint8_t *src, const uint8_t *src2) +INIT_XMM sse2 +``` + +第一行是註解(在組合中,分號 ';' 的作用類似於 C 語言中的 '//'),用於展示該函式的 C 語言參數形式。第二行用於初始化函式,使其能夠使用 XMM 暫存器,並指定使用 sse2 指令集。這是因為 paddb 是一個 sse2 指令。我們將在下一課中更詳細地介紹 sse2。 + +```assembly +cglobal add_values, 2, 2, 2, src, src2 +``` + +這一行至關重要,它定義了一個名為 add_values 的 C 語言函式。 + +讓我們逐項解析: + +* 下一個參數指定了該函式有兩個參數。 +* 再下一個參數指定這兩個參數將使用兩個 GPR 進行傳遞。在某些情況下,我們可能需要更多的 GPR,因此必須告訴 x86util 我們需要額外的暫存器。 +* 再下一個參數指定了 x86util 需要多少個 XMM 暫存器。 +* 最後兩個參數是函式參數的標籤。 + +值得注意的是,舊程式碼可能不會為函式參數添加標籤,而是直接使用 r0、r1 等通用暫存器(GPR)來存取參數。 + +```assembly + movu m0, [srcq] + movu m1, [src2q] +``` + +movu 是 movdqu(move double quad unaligned 移動雙四字未對齊)的簡寫。關於對齊的內容我們會在後續課程中講解,但目前可以將 movu 視為從 [srcq] 進行 128 位元資料傳輸。對於 mov 指令,方括號表示對 [srcq] 指向的位址進行解參照,相當於 *C 語言中的 \*src*,這就是所謂的'載入'(load)。需要注意的是,q 後綴表示指標的大小(即在 C 語言中在 64 位元系統上 sizeof(*src) == 8,x86 組合在 32 位元系統上則會自動調整為 32 位元),但實際載入的資料仍然是 128 位元的。 + +需要注意的是,我們並不使用向量暫存器的完整名稱(例如 xmm0),而是使用抽象形式的 m0。在後續課程中,你會看到這種方式的優勢 —— 它允許你編寫一次程式碼,並在不同大小的 SIMD 暫存器上通用適用。 + +```assembly +paddb m0, m1 +``` + +paddb(可以在腦海中讀作 *p-add-b*)會對每個暫存器中的每個位元組進行相加,如下所示。"p" 前綴代表 "packed"(打包),用於區分向量指令和純量指令。"b" 後綴表示這是 按位元組(bytewise) 進行的加法運算。 + +| 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 +``` + +這是一個表示函式返回的巨集 (macro)。在 FFmpeg 中,幾乎所有組合函式都會直接修改參數中的資料,而不是返回一個值。 + +在後續的作業中,你會看到,我們會為組合函式創建函式指標,並在可用的地方使用它們。 + +[下一課](../lesson_02/index_zh-hant.md) From 638e4aa0c8d5c999d4e88977896d98a1d6cfd5b0 Mon Sep 17 00:00:00 2001 From: Zachary Wang Date: Wed, 2 Apr 2025 21:01:12 +1100 Subject: [PATCH 02/11] Simplified and traditional Chinese translation on README --- README_zh-Hans.md | 13 +++++++++++++ README_zh-Hant.md | 13 +++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 README_zh-Hans.md create mode 100644 README_zh-Hant.md diff --git a/README_zh-Hans.md b/README_zh-Hans.md new file mode 100644 index 0000000..d2d1768 --- /dev/null +++ b/README_zh-Hans.md @@ -0,0 +1,13 @@ +欢迎来到 FFmpeg 汇编语言学院!你已经踏上了一段既有趣又充满挑战,同时回报丰厚的编程旅程。这些课程将帮助你掌握在 FFmpeg 中编写汇编语言的基础,并让你深入了解计算机内部的实际运作方式。 + +**必备知识** + +* C 语言基础,尤其是指针。如果你不了解 C 语言,建议先学习 《C 程序设计语言》 这本书。 +* 高中数学(例如标量与向量的区别、加法、乘法等)。 + +**课程内容** + +在这个 Git 仓库中,你可以找到课程内容及对应的作业(作业暂未上传)。完成所有课程后,你将具备为 FFmpeg 贡献代码的能力。 + +我们有个 Discord 服务器用来解答问题: +https://discord.com/invite/Ks5MhUhqfB \ No newline at end of file diff --git a/README_zh-Hant.md b/README_zh-Hant.md new file mode 100644 index 0000000..f9f3aeb --- /dev/null +++ b/README_zh-Hant.md @@ -0,0 +1,13 @@ +歡迎來到 FFmpeg 組合語言學院!你已經踏上了一段既有趣又充滿挑戰,同時回報豐厚的程式設計旅程。這些課程將幫助你掌握在 FFmpeg 中編寫組合語言的基礎,並讓你深入了解電腦內部的實際運作方式。 + +**必備知識** + +* C 語言基礎,尤其是指標。如果你不了解 C 語言,建議先學習《C 程式設計語言》這本書。 +* 高中數學(例如純量與向量的區別、加法、乘法等)。 + +**課程內容** + +在這個 Git 儲存庫中,你可以找到課程內容及對應的作業(作業暫未上傳)。完成所有課程後,你將具備為 FFmpeg 貢獻程式碼的能力。 + +我們有個 Discord 伺服器用來解答問題: +https://discord.com/invite/Ks5MhUhqfB \ No newline at end of file From 8fd5ab79533af2626377daeb0f2cd510c7369409 Mon Sep 17 00:00:00 2001 From: Zachary Wang Date: Wed, 2 Apr 2025 21:03:14 +1100 Subject: [PATCH 03/11] space & rtn issue --- README_zh-Hans.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_zh-Hans.md b/README_zh-Hans.md index d2d1768..0babe08 100644 --- a/README_zh-Hans.md +++ b/README_zh-Hans.md @@ -9,5 +9,5 @@ 在这个 Git 仓库中,你可以找到课程内容及对应的作业(作业暂未上传)。完成所有课程后,你将具备为 FFmpeg 贡献代码的能力。 -我们有个 Discord 服务器用来解答问题: +我们有个 Discord 服务器用来解答问题: https://discord.com/invite/Ks5MhUhqfB \ No newline at end of file From 13003e0d61896563614cd07ef4c389249915263a Mon Sep 17 00:00:00 2001 From: Zachary Wang Date: Wed, 2 Apr 2025 21:31:20 +1100 Subject: [PATCH 04/11] update Chinese book name --- lesson_01/index_zh-hans.md | 2 +- lesson_01/index_zh-hant.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lesson_01/index_zh-hans.md b/lesson_01/index_zh-hans.md index e952252..5b3d402 100644 --- a/lesson_01/index_zh-hans.md +++ b/lesson_01/index_zh-hans.md @@ -42,7 +42,7 @@ FFmpeg中的绝大多数汇编代码都是所谓的*SIMD,单指令多数据 (S 许多书籍在教汇编之前会深入介绍计算机架构细节。如果你想学这些,那很好,但从我们的角度来看,这就像在学习开车之前研究发动机。 -话虽如此,《64位汇编的艺术》 (The Art of 64-bit assembly) 一书后半部分的图解,以可视化方式展示 SIMD 指令及其行为,还是很有用的:[https://artofasm.randallhyde.com/](https://artofasm.randallhyde.com/) +话虽如此,《64位汇编语言的编程艺术》 (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) diff --git a/lesson_01/index_zh-hant.md b/lesson_01/index_zh-hant.md index e4f45d1..b9a5339 100644 --- a/lesson_01/index_zh-hant.md +++ b/lesson_01/index_zh-hant.md @@ -42,7 +42,7 @@ FFmpeg中的絕大多數組合程式碼都是所謂的*SIMD,單指令多資料 許多書籍在教組合之前會深入介紹電腦架構細節。如果你想學這些,那很好,但從我們的角度來看,這就像在學習開車之前研究引擎。 -話雖如此,《64位組合的藝術》 (The Art of 64-bit assembly) 一書後半部分的圖解,以可視化方式展示 SIMD 指令及其行為,還是很有用的:[https://artofasm.randallhyde.com/](https://artofasm.randallhyde.com/) +話雖如此,《64位組合語言的程式設計藝術》 (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) From 8f725797c94590348dca084fda77a2a96bc7ea1e Mon Sep 17 00:00:00 2001 From: Zachary Wang Date: Wed, 2 Apr 2025 21:34:53 +1100 Subject: [PATCH 05/11] update traditional Chinese version book name --- lesson_01/index_zh-hant.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lesson_01/index_zh-hant.md b/lesson_01/index_zh-hant.md index b9a5339..adce41c 100644 --- a/lesson_01/index_zh-hant.md +++ b/lesson_01/index_zh-hant.md @@ -42,7 +42,7 @@ FFmpeg中的絕大多數組合程式碼都是所謂的*SIMD,單指令多資料 許多書籍在教組合之前會深入介紹電腦架構細節。如果你想學這些,那很好,但從我們的角度來看,這就像在學習開車之前研究引擎。 -話雖如此,《64位組合語言的程式設計藝術》 (The Art of 64-bit assembly) 一書後半部分的圖解,以可視化方式展示 SIMD 指令及其行為,還是很有用的:[https://artofasm.randallhyde.com/](https://artofasm.randallhyde.com/) +話雖如此,《64位元組合語言的程式設計藝術》 (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) From 03a66cf2eea1fd1eaffce5408307abee54dc3961 Mon Sep 17 00:00:00 2001 From: Zachary Wang Date: Wed, 2 Apr 2025 21:49:23 +1100 Subject: [PATCH 06/11] update correct Chinese (s & t) punctuation --- lesson_01/index_zh-hans.md | 21 ++++++++------------- lesson_01/index_zh-hant.md | 20 ++++++++++---------- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/lesson_01/index_zh-hans.md b/lesson_01/index_zh-hans.md index 5b3d402..3e8e0a4 100644 --- a/lesson_01/index_zh-hans.md +++ b/lesson_01/index_zh-hans.md @@ -11,13 +11,13 @@ **什么是汇编语言 (Assembly language)?** -汇编语言 (Assembly language) 是一种编程语言,你编写的代码直接对应于CPU处理的指令。人类可读的汇编语言,顾名思义,被*汇编 (assembled)* 成CPU能够理解的二进制数据,称为*机器码 (machine code)*。你可能会看到汇编语言代码被简称为"assembly"或"asm"。 +汇编语言 (Assembly language) 是一种编程语言,你编写的代码直接对应于CPU处理的指令。人类可读的汇编语言,顾名思义,被*汇编 (assembled)* 成CPU能够理解的二进制数据,称为*机器码 (machine code)*。你可能会看到汇编语言代码被简称为 “assembly” 或 “asm” 。 FFmpeg中的绝大多数汇编代码都是所谓的*SIMD,单指令多数据 (Single Instruction Multiple Data)*。SIMD有时也被称为向量编程 (vector programming)。这意味着特定指令能同时对多个数据元素进行操作。大多数编程语言一次只处理一个数据元素,称为标量编程 (scalar programming)。 你可能已经猜到,SIMD非常适合处理图像、视频和音频,因为这些数据在内存中按顺序排列。CPU中有专门的指令帮助我们处理顺序数据。 -在FFmpeg中,你会看到"汇编函数 (assembly function)"、"SIMD"和"向量(化)"这些术语被互换使用。它们都指的是同一件事:通过手写汇编语言函数一次性地处理多个数据元素。一些项目也可能将这些称为"汇编内核 (assembly kernels)"。 +在FFmpeg中,你会看到 “汇编函数 (assembly function)” 、 “SIMD” 和 “向量(化)” 这些术语被互换使用。它们都指的是同一件事:通过手写汇编语言函数一次性地处理多个数据元素。一些项目也可能将这些称为 “汇编内核 (assembly kernels)” 。 所有这些听起来可能比较复杂,但谨记,在FFmpeg项目中,高中生也编写了汇编代码。与所有事物一样,学习是50%术语和50%实际学习。 @@ -25,11 +25,11 @@ FFmpeg中的绝大多数汇编代码都是所谓的*SIMD,单指令多数据 (S 为了使多媒体处理得更快。编写汇编代码通常能带来10倍或以上的速度提升,这对于实时播放视频而不卡顿尤为重要。它还能节省能源并延长电池寿命。值得注意的是,视频编码和解码是全球最常用的功能之一,无论是普通用户还是大型企业的数据中心都用到它们。因此,即使是微小的改进,也能迅速积累出显著的效果。 -在网上,你会经常看到人们使用*内联函数 (intrinsics)*,这些C语言风格的函数映射到汇编指令,以便更快地开发。在 FFmpeg 中,我们不使用内联函数,而是手写汇编代码。这是一个有争议的部分,但内联函数通常比手写汇编慢约10-15%(内联函数支持者可能会不同意),这取决于编译器。对于 FFmpeg 来说,每一点额外的性能提升都有用,这就是为什么我们直接使用汇编代码。还有一种观点认为,内联函数由于使用了"[匈牙利命名法](https://zh.wikipedia.org/wiki/匈牙利命名法)"而难以阅读。 +在网上,你会经常看到人们使用*内联函数 (intrinsics)*,这些C语言风格的函数映射到汇编指令,以便更快地开发。在 FFmpeg 中,我们不使用内联函数,而是手写汇编代码。这是一个有争议的部分,但内联函数通常比手写汇编慢约10-15%(内联函数支持者可能会不同意),这取决于编译器。对于 FFmpeg 来说,每一点额外的性能提升都有用,这就是为什么我们直接使用汇编代码。还有一种观点认为,内联函数由于使用了 “[匈牙利命名法](https://zh.wikipedia.org/wiki/匈牙利命名法)” 而难以阅读。 出于历史原因,你可能还会在 FFmpeg 的一些地方看到*内联汇编 (inline assembly)*(即不使用内联函数),或者在 Linux 内核等项目中因为非常特殊的应用场景而看到内联汇编。这是指汇编代码不写在单独的文件中,在像 FFmpeg 这样的项目中,主流观点认为这种代码难以阅读,缺乏广泛的编译器支持,并且难以维护。 -最后,你会看到很多自称专家的人在网上说这些都是没必要的,编译器可以为你完成所有这些"向量化"工作。至少以学习的目的,请忽视他们:例如,[dav1d项目](https://www.videolan.org/projects/dav1d.html)的最近测试显示,这种自动向量化带来了约2倍的速度提升,而手写版本可以达到8倍。 +最后,你会看到很多自称专家的人在网上说这些都是没必要的,编译器可以为你完成所有这些“向量化”工作。至少以学习的目的,请忽视他们:例如,[dav1d项目](https://www.videolan.org/projects/dav1d.html)的最近测试显示,这种自动向量化带来了约2倍的速度提升,而手写版本可以达到8倍。 **汇编语言的风格** @@ -99,14 +99,11 @@ FFmpeg中的绝大多数汇编代码都是所谓的*SIMD,单指令多数据 (S 加粗的字符稍后会很重要。 **x86inc.asm 包含文件** -You’ll see in many examples we include the file x86inc.asm. X86inc.asm is a lightweight abstraction layer used in FFmpeg, x264, and dav1d to make an assembly programmer's life easier. It helps in many ways, but to begin with, one of the useful things it does is it labels GPRs, r0, r1, r2. This means you don’t have to remember any register names. As mentioned before, GPRs are generally just scaffolding so this makes life a lot easier. 在许多示例中,你会看到我们包含了文件 x86inc.asm。x86inc.asm 是 FFmpeg、x264 和 dav1d 中使用的轻量级抽象层,旨在简化汇编程序员的工作。它在许多方面提供帮助,但首先,它的一个有用功能是给 GPR 寄存器标记为 r0、r1、r2。这样,你就不必记住任何寄存器的名称。如前所述,GPR 通常只是脚手架,所以这种做法大大简化了编程过程。 **一个简单的标量汇编 (scalar asm) 片段** -Let’s look at a simple (and very much artificial) snippet of scalar asm (assembly code that operates on individual data items, one at a time, within each instruction) to see what’s going on: - 让我们看一段简单(且非常人工构造)的标量汇编代码(每条指令一次只对单个数据项进行操作的汇编代码),看看发生了什么: ```assembly @@ -116,7 +113,7 @@ dec r0q imul r0q, 5 ``` -在第一行中,*立即数 (immediate value)* 3(直接存储在汇编代码中的值,而不是从内存中获取的值)被存储到寄存器 r0 中,作为一个四字(quadword)值。注意,在 Intel 风格中,源操作数(提供数据的值或位置,位于右侧)被传送到目标操作数(接收数据的位置,位于左侧),类似于 memcpy 的行为。你也可以理解为‘r0q = 3’,因为顺序是相同的。r0 的 ‘q’ 后缀表示该寄存器作为四字 (**q**uadword) 来用。‘inc’ 操作将值增加,使 r0q 包含 4,‘dec’ 操作将值减少回 3,‘imul’ 将值乘以 5。因此,最终 r0q 包含 15。 +在第一行中,*立即数 (immediate value)* 3(直接存储在汇编代码中的值,而不是从内存中获取的值)被存储到寄存器 r0 中,作为一个四字(quadword)值。注意,在 Intel 风格中,源操作数(提供数据的值或位置,位于右侧)被传送到目标操作数(接收数据的位置,位于左侧),类似于 memcpy 的行为。你也可以理解为“r0q = 3”,因为顺序是相同的。r0 的 “q” 后缀表示该寄存器作为四字 (**q**uadword) 来用。“inc” 操作将值增加,使 r0q 包含 4,“dec” 操作将值减少回 3,“imul” 将值乘以 5。因此,最终 r0q 包含 15。 请注意,汇编器 (assembler) 将人类可读的指令(如 mov 和 inc)汇编成机器代码,这些被称为*助记符 (mnemonics)*。你可能会在网上和书籍中看到助记符以大写字母表示,如 MOV 和 INC,但它们与小写版本完全相同。在 FFmpeg 中,我们使用小写助记符,并将大写保留给宏 (macro) 定义。 @@ -148,7 +145,7 @@ cglobal add_values, 2, 2, 2, src, src2 %include "x86inc.asm" ``` -这是一个由x264、FFmpeg 和 dav1d 社区开发的"头文件",提供帮助函数、预定义名称和宏(如下面的cglobal)以简化汇编编写。 +这是一个由x264、FFmpeg 和 dav1d 社区开发的“头文件”,提供帮助函数、预定义名称和宏(如下面的cglobal)以简化汇编编写。 ```assembly SECTION .text @@ -161,7 +158,7 @@ SECTION .text INIT_XMM sse2 ``` -第一行是注释(在汇编中,分号 ‘;’ 的作用类似于 C 语言中的 ‘//’),用于展示该函数的 C 语言参数形式。第二行用于初始化函数,使其能够使用 XMM 寄存器,并指定使用 sse2 指令集。这是因为 paddb 是一个 sse2 指令。我们将在下一课中更详细地介绍 sse2。 +第一行是注释(在汇编中,分号 “;” 的作用类似于 C 语言中的 “//”),用于展示该函数的 C 语言参数形式。第二行用于初始化函数,使其能够使用 XMM 寄存器,并指定使用 sse2 指令集。这是因为 paddb 是一个 sse2 指令。我们将在下一课中更详细地介绍 sse2。 ```assembly cglobal add_values, 2, 2, 2, src, src2 @@ -183,7 +180,7 @@ cglobal add_values, 2, 2, 2, src, src2 movu m1, [src2q] ``` -movu 是 movdqu(move double quad unaligned 移动双四字未对齐)的简写。关于对齐的内容我们会在后续课程中讲解,但目前可以将 movu 视为从 [srcq] 进行 128 位数据传输。对于 mov 指令,方括号表示对 [srcq] 指向的地址进行解引用,相当于 *C 语言中的 \*src*,这就是所谓的‘加载’(load)。需要注意的是,q 后缀表示指针的大小(即在 C 语言中在 64 位系统上 sizeof(*src) == 8,x86 汇编在 32 位系统上则会自动调整为 32 位),但实际加载的数据仍然是 128 位的。 +movu 是 movdqu(move double quad unaligned 移动双四字未对齐)的简写。关于对齐的内容我们会在后续课程中讲解,但目前可以将 movu 视为从 [srcq] 进行 128 位数据传输。对于 mov 指令,方括号表示对 [srcq] 指向的地址进行解引用,相当于 *C 语言中的 \*src*,这就是所谓的“加载”(load)。需要注意的是,q 后缀表示指针的大小(即在 C 语言中在 64 位系统上 sizeof(*src) == 8,x86 汇编在 32 位系统上则会自动调整为 32 位),但实际加载的数据仍然是 128 位的。 需要注意的是,我们并不使用向量寄存器的完整名称(例如 xmm0),而是使用抽象形式的 m0。在后续课程中,你会看到这种方式的优势 —— 它允许你编写一次代码,并在不同大小的 SIMD 寄存器上通用适用。 @@ -191,8 +188,6 @@ movu 是 movdqu(move double quad unaligned 移动双四字未对齐)的简 paddb m0, m1 ``` -paddb (read this in your head as *p-add-b*) is adding each byte in each register as shown below. The “p” prefix stands for “packed” and is used to identify vector instructions vs scalar instructions. The “b” suffix shows that this is bytewise addition (addition of bytes). - paddb(可以在脑海中读作 *p-add-b*)会对每个寄存器中的每个字节进行相加,如下所示。“p” 前缀代表 “packed”(打包),用于区分向量指令和标量指令。“b” 后缀表示这是 按字节(bytewise) 进行的加法运算。 | a | b | c | d | e | f | g | h | i | j | k | l | m | n | o | p | diff --git a/lesson_01/index_zh-hant.md b/lesson_01/index_zh-hant.md index adce41c..dfa3056 100644 --- a/lesson_01/index_zh-hant.md +++ b/lesson_01/index_zh-hant.md @@ -11,13 +11,13 @@ **什麼是組合語言 (Assembly language)?** -組合語言 (Assembly language) 是一種程式設計語言,你編寫的程式碼直接對應於CPU處理的指令。人類可讀的組合語言,顧名思義,被*組譯 (assembled)* 成CPU能夠理解的二進位資料,稱為*機器碼 (machine code)*。你可能會看到組合語言程式碼被簡稱為"assembly"或"asm"。 +組合語言 (Assembly language) 是一種程式設計語言,你編寫的程式碼直接對應於CPU處理的指令。人類可讀的組合語言,顧名思義,被*組譯 (assembled)* 成CPU能夠理解的二進位資料,稱為*機器碼 (machine code)*。你可能會看到組合語言程式碼被簡稱為「assembly」或「asm」。 FFmpeg中的絕大多數組合程式碼都是所謂的*SIMD,單指令多資料 (Single Instruction Multiple Data)*。SIMD有時也被稱為向量程式設計 (vector programming)。這意味著特定指令能同時對多個資料元素進行操作。大多數程式設計語言一次只處理一個資料元素,稱為純量程式設計 (scalar programming)。 你可能已經猜到,SIMD非常適合處理影像、視訊和音訊,因為這些資料在記憶體中按順序排列。CPU中有專門的指令幫助我們處理順序資料。 -在FFmpeg中,你會看到"組合函式 (assembly function)"、"SIMD"和"向量(化)"這些術語被互換使用。它們都指的是同一件事:通過手寫組合語言函式一次性地處理多個資料元素。一些專案也可能將這些稱為"組合核心 (assembly kernels)"。 +在FFmpeg中,你會看到「組合函式 (assembly function)」、「SIMD」和「向量(化)」這些術語被互換使用。它們都指的是同一件事:通過手寫組合語言函式一次性地處理多個資料元素。一些專案也可能將這些稱為「組合核心 (assembly kernels)」。 所有這些聽起來可能比較複雜,但謹記,在FFmpeg專案中,高中生也編寫了組合程式碼。與所有事物一樣,學習是50%術語和50%實際學習。 @@ -25,11 +25,11 @@ FFmpeg中的絕大多數組合程式碼都是所謂的*SIMD,單指令多資料 為了使多媒體處理得更快。編寫組合程式碼通常能帶來10倍或以上的速度提升,這對於即時播放視訊而不卡頓尤為重要。它還能節省能源並延長電池壽命。值得注意的是,視訊編碼和解碼是全球最常用的功能之一,無論是普通使用者還是大型企業的資料中心都用到它們。因此,即使是微小的改進,也能迅速積累出顯著的效果。 -在網路上,你會經常看到人們使用*內聯函式 (intrinsics)*,這些C語言風格的函式映射到組合指令,以便更快地開發。在 FFmpeg 中,我們不使用內聯函式,而是手寫組合程式碼。這是一個有爭議的部分,但內聯函式通常比手寫組合慢約10-15%(內聯函式支持者可能會不同意),這取決於編譯器。對於 FFmpeg 來說,每一點額外的效能提升都有用,這就是為什麼我們直接使用組合程式碼。還有一種觀點認為,內聯函式由於使用了"[匈牙利命名法](https://zh.wikipedia.org/wiki/匈牙利命名法)"而難以閱讀。 +在網路上,你會經常看到人們使用*內聯函式 (intrinsics)*,這些C語言風格的函式映射到組合指令,以便更快地開發。在 FFmpeg 中,我們不使用內聯函式,而是手寫組合程式碼。這是一個有爭議的部分,但內聯函式通常比手寫組合慢約10-15%(內聯函式支持者可能會不同意),這取決於編譯器。對於 FFmpeg 來說,每一點額外的效能提升都有用,這就是為什麼我們直接使用組合程式碼。還有一種觀點認為,內聯函式由於使用了「[匈牙利命名法](https://zh.wikipedia.org/wiki/匈牙利命名法)」而難以閱讀。 出於歷史原因,你可能還會在 FFmpeg 的一些地方看到*內聯組合 (inline assembly)*(即不使用內聯函式),或者在 Linux 核心等專案中因為非常特殊的應用場景而看到內聯組合。這是指組合程式碼不寫在單獨的檔案中,在像 FFmpeg 這樣的專案中,主流觀點認為這種程式碼難以閱讀,缺乏廣泛的編譯器支援,並且難以維護。 -最後,你會看到很多自稱專家的人在網路上說這些都是沒必要的,編譯器可以為你完成所有這些"向量化"工作。至少以學習的目的,請忽視他們:例如,[dav1d專案](https://www.videolan.org/projects/dav1d.html)的最近測試顯示,這種自動向量化帶來了約2倍的速度提升,而手寫版本可以達到8倍。 +最後,你會看到很多自稱專家的人在網路上說這些都是沒必要的,編譯器可以為你完成所有這些「向量化」工作。至少以學習的目的,請忽視他們:例如,[dav1d專案](https://www.videolan.org/projects/dav1d.html)的最近測試顯示,這種自動向量化帶來了約2倍的速度提升,而手寫版本可以達到8倍。 **組合語言的風格** @@ -51,7 +51,7 @@ FFmpeg中的絕大多數組合程式碼都是所謂的*SIMD,單指令多資料 暫存器是 CPU 內用於處理資料的區域。CPU 並不會直接對記憶體進行操作,而是先將資料載入到暫存器中進行處理,然後再寫回記憶體。在組合語言中,通常不能直接在兩個記憶體位置之間複製資料,而必須先通過暫存器中轉。 **通用暫存器 (General Purpose Register)** -第一種暫存器被稱為通用暫存器(GPR)。之所以稱為'通用',是因為它們既可以儲存資料(最大支援 64 位元值),也可以儲存記憶體位址(指標)。GPR 中的值可以進行各種操作,如加法、乘法、位移等。 +第一種暫存器被稱為通用暫存器(GPR)。之所以稱為「通用」,是因為它們既可以儲存資料(最大支援 64 位元值),也可以儲存記憶體位址(指標)。GPR 中的值可以進行各種操作,如加法、乘法、位移等。 在大多數組合書籍中,會有整章內容專門講解 GPR 的細節和歷史背景等。這是因為在作業系統程式設計、逆向工程等領域,GPR 是非常重要的。然而,在 FFmpeg 中編寫的組合程式碼裡,GPR 更像是鷹架,大多數情況下不需要它們的複雜性,並且被抽象出來。 @@ -108,7 +108,7 @@ dec r0q imul r0q, 5 ``` -在第一行中,*立即值 (immediate value)* 3(直接儲存在組合程式碼中的值,而不是從記憶體中獲取的值)被儲存到暫存器 r0 中,作為一個四字(quadword)值。注意,在 Intel 風格中,來源運算元(提供資料的值或位置,位於右側)被傳送到目標運算元(接收資料的位置,位於左側),類似於 memcpy 的行為。你也可以理解為'r0q = 3',因為順序是相同的。r0 的 'q' 後綴表示該暫存器作為四字 (**q**uadword) 來用。'inc' 操作將值增加,使 r0q 包含 4,'dec' 操作將值減少回 3,'imul' 將值乘以 5。因此,最終 r0q 包含 15。 +在第一行中,*立即值 (immediate value)* 3(直接儲存在組合程式碼中的值,而不是從記憶體中獲取的值)被儲存到暫存器 r0 中,作為一個四字(quadword)值。注意,在 Intel 風格中,來源運算元(提供資料的值或位置,位於右側)被傳送到目標運算元(接收資料的位置,位於左側),類似於 memcpy 的行為。你也可以理解為「r0q = 3」,因為順序是相同的。r0 的 「q」 後綴表示該暫存器作為四字 (**q**uadword) 來用。「inc」 操作將值增加,使 r0q 包含 4,「dec」 操作將值減少回 3,「imul」 將值乘以 5。因此,最終 r0q 包含 15。 請注意,組譯器 (assembler) 將人類可讀的指令(如 mov 和 inc)組譯成機器碼,這些被稱為*助記符 (mnemonics)*。你可能會在網路上和書籍中看到助記符以大寫字母表示,如 MOV 和 INC,但它們與小寫版本完全相同。在 FFmpeg 中,我們使用小寫助記符,並將大寫保留給巨集 (macro) 定義。 @@ -140,7 +140,7 @@ cglobal add_values, 2, 2, 2, src, src2 %include "x86inc.asm" ``` -這是一個由x264、FFmpeg 和 dav1d 社群開發的"標頭檔",提供幫助函式、預定義名稱和巨集(如下面的cglobal)以簡化組合編寫。 +這是一個由x264、FFmpeg 和 dav1d 社群開發的「標頭檔」,提供幫助函式、預定義名稱和巨集(如下面的cglobal)以簡化組合編寫。 ```assembly SECTION .text @@ -153,7 +153,7 @@ SECTION .text INIT_XMM sse2 ``` -第一行是註解(在組合中,分號 ';' 的作用類似於 C 語言中的 '//'),用於展示該函式的 C 語言參數形式。第二行用於初始化函式,使其能夠使用 XMM 暫存器,並指定使用 sse2 指令集。這是因為 paddb 是一個 sse2 指令。我們將在下一課中更詳細地介紹 sse2。 +第一行是註解(在組合中,分號 「;」 的作用類似於 C 語言中的 「//」),用於展示該函式的 C 語言參數形式。第二行用於初始化函式,使其能夠使用 XMM 暫存器,並指定使用 sse2 指令集。這是因為 paddb 是一個 sse2 指令。我們將在下一課中更詳細地介紹 sse2。 ```assembly cglobal add_values, 2, 2, 2, src, src2 @@ -175,7 +175,7 @@ cglobal add_values, 2, 2, 2, src, src2 movu m1, [src2q] ``` -movu 是 movdqu(move double quad unaligned 移動雙四字未對齊)的簡寫。關於對齊的內容我們會在後續課程中講解,但目前可以將 movu 視為從 [srcq] 進行 128 位元資料傳輸。對於 mov 指令,方括號表示對 [srcq] 指向的位址進行解參照,相當於 *C 語言中的 \*src*,這就是所謂的'載入'(load)。需要注意的是,q 後綴表示指標的大小(即在 C 語言中在 64 位元系統上 sizeof(*src) == 8,x86 組合在 32 位元系統上則會自動調整為 32 位元),但實際載入的資料仍然是 128 位元的。 +movu 是 movdqu(move double quad unaligned 移動雙四字未對齊)的簡寫。關於對齊的內容我們會在後續課程中講解,但目前可以將 movu 視為從 [srcq] 進行 128 位元資料傳輸。對於 mov 指令,方括號表示對 [srcq] 指向的位址進行解參照,相當於 *C 語言中的 \*src*,這就是所謂的「載入」(load)。需要注意的是,q 後綴表示指標的大小(即在 C 語言中在 64 位元系統上 sizeof(*src) == 8,x86 組合在 32 位元系統上則會自動調整為 32 位元),但實際載入的資料仍然是 128 位元的。 需要注意的是,我們並不使用向量暫存器的完整名稱(例如 xmm0),而是使用抽象形式的 m0。在後續課程中,你會看到這種方式的優勢 —— 它允許你編寫一次程式碼,並在不同大小的 SIMD 暫存器上通用適用。 @@ -183,7 +183,7 @@ movu 是 movdqu(move double quad unaligned 移動雙四字未對齊)的簡 paddb m0, m1 ``` -paddb(可以在腦海中讀作 *p-add-b*)會對每個暫存器中的每個位元組進行相加,如下所示。"p" 前綴代表 "packed"(打包),用於區分向量指令和純量指令。"b" 後綴表示這是 按位元組(bytewise) 進行的加法運算。 +paddb(可以在腦海中讀作 *p-add-b*)會對每個暫存器中的每個位元組進行相加,如下所示。「p」 前綴代表 「packed」(打包),用於區分向量指令和純量指令。「b」 後綴表示這是 按位元組(bytewise) 進行的加法運算。 | a | b | c | d | e | f | g | h | i | j | k | l | m | n | o | p | | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | From 5d372f24fe34a21db0bd7f10aa047fe75da3b1ee Mon Sep 17 00:00:00 2001 From: Zachary Wang Date: Wed, 2 Apr 2025 21:51:49 +1100 Subject: [PATCH 07/11] update wording --- lesson_01/index_zh-hans.md | 2 +- lesson_01/index_zh-hant.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lesson_01/index_zh-hans.md b/lesson_01/index_zh-hans.md index 3e8e0a4..96b7d3d 100644 --- a/lesson_01/index_zh-hans.md +++ b/lesson_01/index_zh-hans.md @@ -217,4 +217,4 @@ RET 在后续的作业中,你会看到,我们会为汇编函数创建函数指针,并在可用的地方使用它们。 -[下一课](../lesson_02/index_zh-hans.md) +[下一节课](../lesson_02/index_zh-hans.md) diff --git a/lesson_01/index_zh-hant.md b/lesson_01/index_zh-hant.md index dfa3056..8f4fa25 100644 --- a/lesson_01/index_zh-hant.md +++ b/lesson_01/index_zh-hant.md @@ -212,4 +212,4 @@ RET 在後續的作業中,你會看到,我們會為組合函式創建函式指標,並在可用的地方使用它們。 -[下一課](../lesson_02/index_zh-hant.md) +[下一節課](../lesson_02/index_zh-hant.md) From c526fec99ed5fccb16c34cff944280050b012747 Mon Sep 17 00:00:00 2001 From: Zachary Wang Date: Wed, 2 Apr 2025 21:53:46 +1100 Subject: [PATCH 08/11] update wording --- lesson_01/index_zh-hans.md | 2 +- lesson_01/index_zh-hant.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lesson_01/index_zh-hans.md b/lesson_01/index_zh-hans.md index 96b7d3d..3e8e0a4 100644 --- a/lesson_01/index_zh-hans.md +++ b/lesson_01/index_zh-hans.md @@ -217,4 +217,4 @@ RET 在后续的作业中,你会看到,我们会为汇编函数创建函数指针,并在可用的地方使用它们。 -[下一节课](../lesson_02/index_zh-hans.md) +[下一课](../lesson_02/index_zh-hans.md) diff --git a/lesson_01/index_zh-hant.md b/lesson_01/index_zh-hant.md index 8f4fa25..dfa3056 100644 --- a/lesson_01/index_zh-hant.md +++ b/lesson_01/index_zh-hant.md @@ -212,4 +212,4 @@ RET 在後續的作業中,你會看到,我們會為組合函式創建函式指標,並在可用的地方使用它們。 -[下一節課](../lesson_02/index_zh-hant.md) +[下一課](../lesson_02/index_zh-hant.md) From 45dbd5be57e768dbc13a60c029bb2947fed69ed8 Mon Sep 17 00:00:00 2001 From: Zachary Wang Date: Wed, 2 Apr 2025 21:56:43 +1100 Subject: [PATCH 09/11] remove redundant stuff --- lesson_01/index_zh-hans.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lesson_01/index_zh-hans.md b/lesson_01/index_zh-hans.md index 3e8e0a4..434665c 100644 --- a/lesson_01/index_zh-hans.md +++ b/lesson_01/index_zh-hans.md @@ -29,7 +29,7 @@ FFmpeg中的绝大多数汇编代码都是所谓的*SIMD,单指令多数据 (S 出于历史原因,你可能还会在 FFmpeg 的一些地方看到*内联汇编 (inline assembly)*(即不使用内联函数),或者在 Linux 内核等项目中因为非常特殊的应用场景而看到内联汇编。这是指汇编代码不写在单独的文件中,在像 FFmpeg 这样的项目中,主流观点认为这种代码难以阅读,缺乏广泛的编译器支持,并且难以维护。 -最后,你会看到很多自称专家的人在网上说这些都是没必要的,编译器可以为你完成所有这些“向量化”工作。至少以学习的目的,请忽视他们:例如,[dav1d项目](https://www.videolan.org/projects/dav1d.html)的最近测试显示,这种自动向量化带来了约2倍的速度提升,而手写版本可以达到8倍。 +最后,你会看到很多自称专家的人在网上说这些都是没必要的,编译器可以为你完成所有这些 “向量化” 工作。至少以学习的目的,请忽视他们:例如,[dav1d项目](https://www.videolan.org/projects/dav1d.html)的最近测试显示,这种自动向量化带来了约2倍的速度提升,而手写版本可以达到8倍。 **汇编语言的风格** @@ -58,10 +58,6 @@ FFmpeg中的绝大多数汇编代码都是所谓的*SIMD,单指令多数据 (S **向量寄存器 (Vector registers)** 向量 (SIMD) 寄存器,顾名思义,包含多个数据元素。不同类型的向量寄存器包括: -* mm registers - MMX registers, 64-bit sized, historic and not used much any more -* xmm registers - XMM registers, 128-bit sized, widely available -* ymm registers - YMM registers, 256-bit sized, some complications when using these -* zmm registers - ZMM registers, 512-bit sized, limited availability * mm寄存器 - MMX寄存器,64位大小,历史悠久,目前已不常使用 * xmm寄存器 - XMM寄存器,128位大小,广泛可用 * ymm寄存器 - YMM寄存器,256位大小,使用时有一些复杂性 From bbd22e3d3d824eed459622824d67bd70f9c10faf Mon Sep 17 00:00:00 2001 From: Zachary Wang Date: Thu, 3 Apr 2025 22:17:29 +1100 Subject: [PATCH 10/11] lesson 2 for Chinese (s & t) and polish some wording in lesson 1 --- lesson_01/index_zh-hans.md | 30 +++---- lesson_01/index_zh-hant.md | 28 +++---- lesson_02/index_zh-hans.md | 168 +++++++++++++++++++++++++++++++++++++ lesson_02/index_zh-hant.md | 168 +++++++++++++++++++++++++++++++++++++ 4 files changed, 365 insertions(+), 29 deletions(-) create mode 100644 lesson_02/index_zh-hans.md create mode 100644 lesson_02/index_zh-hant.md diff --git a/lesson_01/index_zh-hans.md b/lesson_01/index_zh-hans.md index 434665c..508f398 100644 --- a/lesson_01/index_zh-hans.md +++ b/lesson_01/index_zh-hans.md @@ -2,34 +2,34 @@ **介绍** -欢迎来到FFmpeg汇编语言学校。你已经向着编程中最有趣、最具挑战性和最有回报的旅程迈出了第一步。这些课程将为你奠定FFmpeg中汇编语言的编写基础,并让你了解计算机中实际发生的情况。 +欢迎来到 FFmpeg 汇编语言学校。你已经向着编程中最有趣、最具挑战性和最有回报的旅程迈出了第一步。这些课程将为你奠定在 FFmpeg 中编写汇编语言的基础,并让你了解计算机中实际发生的情况。 **所需知识** -* C语言知识,特别是指针。如果你不了解C语言,建议学习[《C程序设计语言》](https://zh.wikipedia.org/wiki/C程序设计语言_(书))一书 +* C 语言知识,特别是指针。如果你不了解 C 语言,建议学习[《C程序设计语言》](https://zh.wikipedia.org/wiki/C程序设计语言_(书))一书 * 高中数学(标量与向量、加法、乘法等) **什么是汇编语言 (Assembly language)?** -汇编语言 (Assembly language) 是一种编程语言,你编写的代码直接对应于CPU处理的指令。人类可读的汇编语言,顾名思义,被*汇编 (assembled)* 成CPU能够理解的二进制数据,称为*机器码 (machine code)*。你可能会看到汇编语言代码被简称为 “assembly” 或 “asm” 。 +汇编语言 (Assembly language) 是一种编程语言,你编写的代码直接对应于CPU处理的指令。人类可读的汇编语言,顾名思义,被*汇编 (assembled)* 成CPU能够理解的二进制数据,称为*机器码 (machine code)*。你可能会看到汇编语言代码被简称为 "assembly" 或 "asm" 。 -FFmpeg中的绝大多数汇编代码都是所谓的*SIMD,单指令多数据 (Single Instruction Multiple Data)*。SIMD有时也被称为向量编程 (vector programming)。这意味着特定指令能同时对多个数据元素进行操作。大多数编程语言一次只处理一个数据元素,称为标量编程 (scalar programming)。 +FFmpeg中的绝大多数汇编代码都是所谓的 *SIMD,单指令多数据 (Single Instruction Multiple Data)*。SIMD有时也被称为向量编程 (vector programming)。这意味着特定指令能同时对多个数据元素进行操作。大多数编程语言一次只处理一个数据元素,称为标量编程 (scalar programming)。 -你可能已经猜到,SIMD非常适合处理图像、视频和音频,因为这些数据在内存中按顺序排列。CPU中有专门的指令帮助我们处理顺序数据。 +你可能已经猜到,SIMD 非常适合处理图像、视频和音频,因为这些数据在内存中按顺序排列。CPU中有专门的指令帮助我们处理这种顺序数据。 -在FFmpeg中,你会看到 “汇编函数 (assembly function)” 、 “SIMD” 和 “向量(化)” 这些术语被互换使用。它们都指的是同一件事:通过手写汇编语言函数一次性地处理多个数据元素。一些项目也可能将这些称为 “汇编内核 (assembly kernels)” 。 +在 FFmpeg 中,你会看到 "汇编函数 (assembly function)" 、 "SIMD" 和 "向量(化)" 这些术语被互换使用。它们都指的是同一件事:通过手写汇编语言函数一次性地处理多个数据元素。一些项目也可能将这些称为 "汇编内核 (assembly kernels)" 。 -所有这些听起来可能比较复杂,但谨记,在FFmpeg项目中,高中生也编写了汇编代码。与所有事物一样,学习是50%术语和50%实际学习。 +所有这些听起来可能比较复杂,但请记住,在FFmpeg项目中,高中生也能编写汇编代码。与所有学习一样,掌握汇编语言是50%理解术语和50%实际动手。 **为什么我们用汇编语言编写?** 为了使多媒体处理得更快。编写汇编代码通常能带来10倍或以上的速度提升,这对于实时播放视频而不卡顿尤为重要。它还能节省能源并延长电池寿命。值得注意的是,视频编码和解码是全球最常用的功能之一,无论是普通用户还是大型企业的数据中心都用到它们。因此,即使是微小的改进,也能迅速积累出显著的效果。 -在网上,你会经常看到人们使用*内联函数 (intrinsics)*,这些C语言风格的函数映射到汇编指令,以便更快地开发。在 FFmpeg 中,我们不使用内联函数,而是手写汇编代码。这是一个有争议的部分,但内联函数通常比手写汇编慢约10-15%(内联函数支持者可能会不同意),这取决于编译器。对于 FFmpeg 来说,每一点额外的性能提升都有用,这就是为什么我们直接使用汇编代码。还有一种观点认为,内联函数由于使用了 “[匈牙利命名法](https://zh.wikipedia.org/wiki/匈牙利命名法)” 而难以阅读。 +在网上,你会经常看到人们使用*内联函数 (intrinsics)*,这些C语言风格的函数映射到汇编指令,以便更快地开发。在 FFmpeg 中,我们不使用内联函数,而是手写汇编代码。这是一个有争议的部分,但内联函数通常比手写汇编慢约10-15%(内联函数支持者可能会不同意),这取决于编译器。对于 FFmpeg 来说,每一点额外的性能提升都很重要,这就是为什么我们直接使用汇编代码。还有一种观点认为,内联函数由于使用了 "[匈牙利命名法](https://zh.wikipedia.org/wiki/匈牙利命名法)" 而难以阅读。 出于历史原因,你可能还会在 FFmpeg 的一些地方看到*内联汇编 (inline assembly)*(即不使用内联函数),或者在 Linux 内核等项目中因为非常特殊的应用场景而看到内联汇编。这是指汇编代码不写在单独的文件中,在像 FFmpeg 这样的项目中,主流观点认为这种代码难以阅读,缺乏广泛的编译器支持,并且难以维护。 -最后,你会看到很多自称专家的人在网上说这些都是没必要的,编译器可以为你完成所有这些 “向量化” 工作。至少以学习的目的,请忽视他们:例如,[dav1d项目](https://www.videolan.org/projects/dav1d.html)的最近测试显示,这种自动向量化带来了约2倍的速度提升,而手写版本可以达到8倍。 +最后,你会看到很多自称专家的人在网上说这些都是没必要的,编译器可以为你完成所有这些 "向量化" 工作。至少以学习的目的,请忽视他们:例如,[dav1d项目](https://www.videolan.org/projects/dav1d.html)的最近测试显示,这种自动向量化带来了约2倍的速度提升,而手写版本可以达到8倍。 **汇编语言的风格** @@ -51,7 +51,7 @@ FFmpeg中的绝大多数汇编代码都是所谓的*SIMD,单指令多数据 (S 寄存器是 CPU 内用于处理数据的区域。CPU 并不会直接对内存进行操作,而是先将数据加载到寄存器中进行处理,然后再写回内存。在汇编语言中,通常不能直接在两个内存位置之间复制数据,而必须先通过寄存器中转。 **通用寄存器 (General Purpose Register)** -第一种寄存器被称为通用寄存器(GPR)。之所以称为‘通用’,是因为它们既可以存储数据(最大支持 64 位值),也可以存储内存地址(指针)。GPR 中的值可以进行各种操作,如加法、乘法、位移等。 +第一种寄存器被称为通用寄存器(GPR)。之所以称为'通用',是因为它们既可以存储数据(最大支持 64 位值),也可以存储内存地址(指针)。GPR 中的值可以进行各种操作,如加法、乘法、位移等。 在大多数汇编书籍中,会有整章内容专门讲解 GPR 的细节和历史背景等。这是因为在操作系统编程、逆向工程等领域,GPR 是非常重要的。然而,在 FFmpeg 中编写的汇编代码里,GPR 更像是脚手架,大多数情况下不需要它们的复杂性,并且被抽象出来。 @@ -109,7 +109,7 @@ dec r0q imul r0q, 5 ``` -在第一行中,*立即数 (immediate value)* 3(直接存储在汇编代码中的值,而不是从内存中获取的值)被存储到寄存器 r0 中,作为一个四字(quadword)值。注意,在 Intel 风格中,源操作数(提供数据的值或位置,位于右侧)被传送到目标操作数(接收数据的位置,位于左侧),类似于 memcpy 的行为。你也可以理解为“r0q = 3”,因为顺序是相同的。r0 的 “q” 后缀表示该寄存器作为四字 (**q**uadword) 来用。“inc” 操作将值增加,使 r0q 包含 4,“dec” 操作将值减少回 3,“imul” 将值乘以 5。因此,最终 r0q 包含 15。 +在第一行中,*立即数 (immediate value)* 3(直接存储在汇编代码中的值,而不是从内存中获取的值)被存储到寄存器 r0 中,作为一个四字(quadword)值。注意,在 Intel 风格中,源操作数(提供数据的值或位置,位于右侧)被传送到目标操作数(接收数据的位置,位于左侧),类似于 memcpy 的行为。你也可以理解为"r0q = 3",因为顺序是相同的。r0 的 "q" 后缀表示该寄存器作为四字 (**q**uadword) 来用。"inc" 操作将值增加,使 r0q 包含 4,"dec" 操作将值减少回 3,"imul" 将值乘以 5。因此,最终 r0q 包含 15。 请注意,汇编器 (assembler) 将人类可读的指令(如 mov 和 inc)汇编成机器代码,这些被称为*助记符 (mnemonics)*。你可能会在网上和书籍中看到助记符以大写字母表示,如 MOV 和 INC,但它们与小写版本完全相同。在 FFmpeg 中,我们使用小写助记符,并将大写保留给宏 (macro) 定义。 @@ -141,7 +141,7 @@ cglobal add_values, 2, 2, 2, src, src2 %include "x86inc.asm" ``` -这是一个由x264、FFmpeg 和 dav1d 社区开发的“头文件”,提供帮助函数、预定义名称和宏(如下面的cglobal)以简化汇编编写。 +这是一个由x264、FFmpeg 和 dav1d 社区开发的"头文件",提供帮助函数、预定义名称和宏(如下面的cglobal)以简化汇编编写。 ```assembly SECTION .text @@ -154,7 +154,7 @@ SECTION .text INIT_XMM sse2 ``` -第一行是注释(在汇编中,分号 “;” 的作用类似于 C 语言中的 “//”),用于展示该函数的 C 语言参数形式。第二行用于初始化函数,使其能够使用 XMM 寄存器,并指定使用 sse2 指令集。这是因为 paddb 是一个 sse2 指令。我们将在下一课中更详细地介绍 sse2。 +第一行是注释(在汇编中,分号 ";" 的作用类似于 C 语言中的 "//"),用于展示该函数的 C 语言参数形式。第二行用于初始化函数,使其能够使用 XMM 寄存器,并指定使用 sse2 指令集。这是因为 paddb 是一个 sse2 指令。我们将在下一课中更详细地介绍 sse2。 ```assembly cglobal add_values, 2, 2, 2, src, src2 @@ -176,7 +176,7 @@ cglobal add_values, 2, 2, 2, src, src2 movu m1, [src2q] ``` -movu 是 movdqu(move double quad unaligned 移动双四字未对齐)的简写。关于对齐的内容我们会在后续课程中讲解,但目前可以将 movu 视为从 [srcq] 进行 128 位数据传输。对于 mov 指令,方括号表示对 [srcq] 指向的地址进行解引用,相当于 *C 语言中的 \*src*,这就是所谓的“加载”(load)。需要注意的是,q 后缀表示指针的大小(即在 C 语言中在 64 位系统上 sizeof(*src) == 8,x86 汇编在 32 位系统上则会自动调整为 32 位),但实际加载的数据仍然是 128 位的。 +movu 是 movdqu(move double quad unaligned 移动双四字未对齐)的简写。关于对齐的内容我们会在后续课程中讲解,但目前可以将 movu 视为从 [srcq] 进行 128 位数据传输。对于 mov 指令,方括号表示对 [srcq] 指向的地址进行解引用,相当于 *C 语言中的 \*src*,这就是所谓的"加载"(load)。需要注意的是,q 后缀表示指针的大小(即在 C 语言中在 64 位系统上 `sizeof(*src) == 8`,x86 汇编在 32 位系统上则会自动调整为 32 位),但实际加载的数据仍然是 128 位的。 需要注意的是,我们并不使用向量寄存器的完整名称(例如 xmm0),而是使用抽象形式的 m0。在后续课程中,你会看到这种方式的优势 —— 它允许你编写一次代码,并在不同大小的 SIMD 寄存器上通用适用。 @@ -184,7 +184,7 @@ movu 是 movdqu(move double quad unaligned 移动双四字未对齐)的简 paddb m0, m1 ``` -paddb(可以在脑海中读作 *p-add-b*)会对每个寄存器中的每个字节进行相加,如下所示。“p” 前缀代表 “packed”(打包),用于区分向量指令和标量指令。“b” 后缀表示这是 按字节(bytewise) 进行的加法运算。 +paddb(可以在脑海中读作 *p-add-b*)会对每个寄存器中的每个字节进行相加,如下所示。"p" 前缀代表 "packed"(打包),用于区分向量指令和标量指令。"b" 后缀表示这是按字节(bytewise)进行的加法运算。 | a | b | c | d | e | f | g | h | i | j | k | l | m | n | o | p | | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | diff --git a/lesson_01/index_zh-hant.md b/lesson_01/index_zh-hant.md index dfa3056..caa8614 100644 --- a/lesson_01/index_zh-hant.md +++ b/lesson_01/index_zh-hant.md @@ -2,38 +2,38 @@ **介紹** -歡迎來到FFmpeg組合語言學校。你已經朝著程式設計中最有趣、最具挑戰性和最有回報的旅程邁出了第一步。這些課程將為你奠定FFmpeg中組合語言的編寫基礎,並讓你了解電腦中實際發生的情況。 +歡迎來到 FFmpeg 組合語言學校。你已經朝著程式設計中最有趣、最具挑戰性和最有回報的旅程邁出了第一步。這些課程將為你奠定在 FFmpeg 中編寫組合語言的基礎,並讓你了解電腦中實際發生的情況。 **所需知識** -* C語言知識,特別是指標。如果你不了解C語言,建議學習[《C程式設計語言》](https://zh.wikipedia.org/wiki/C程序设计语言_(书))一書 +* C 語言知識,特別是指標。如果你不了解 C 語言,建議學習[《C程式設計語言》](https://zh.wikipedia.org/wiki/C程序设计语言_(书))一書 * 高中數學(純量與向量、加法、乘法等) **什麼是組合語言 (Assembly language)?** -組合語言 (Assembly language) 是一種程式設計語言,你編寫的程式碼直接對應於CPU處理的指令。人類可讀的組合語言,顧名思義,被*組譯 (assembled)* 成CPU能夠理解的二進位資料,稱為*機器碼 (machine code)*。你可能會看到組合語言程式碼被簡稱為「assembly」或「asm」。 +組合語言 (Assembly language) 是一種程式設計語言,你編寫的程式碼直接對應於 CPU 處理的指令。人類可讀的組合語言,顧名思義,被*組譯 (assembled)* 成CPU能夠理解的二進位資料,稱為*機器碼 (machine code)*。你可能會看到組合語言程式碼被簡稱為「assembly」或「asm」。 -FFmpeg中的絕大多數組合程式碼都是所謂的*SIMD,單指令多資料 (Single Instruction Multiple Data)*。SIMD有時也被稱為向量程式設計 (vector programming)。這意味著特定指令能同時對多個資料元素進行操作。大多數程式設計語言一次只處理一個資料元素,稱為純量程式設計 (scalar programming)。 +FFmpeg中的絕大多數組合程式碼都是所謂的 *SIMD,單指令多資料 (Single Instruction Multiple Data)*。SIMD有時也被稱為向量程式設計 (vector programming)。這意味著特定指令能同時對多個資料元素進行操作。大多數程式設計語言一次只處理一個資料元素,稱為純量程式設計 (scalar programming)。 -你可能已經猜到,SIMD非常適合處理影像、視訊和音訊,因為這些資料在記憶體中按順序排列。CPU中有專門的指令幫助我們處理順序資料。 +你可能已經猜到,SIMD 非常適合處理影像、視訊和音訊,因為這些資料在記憶體中按順序排列。CPU中有專門的指令幫助我們處理這種連續資料。 在FFmpeg中,你會看到「組合函式 (assembly function)」、「SIMD」和「向量(化)」這些術語被互換使用。它們都指的是同一件事:通過手寫組合語言函式一次性地處理多個資料元素。一些專案也可能將這些稱為「組合核心 (assembly kernels)」。 -所有這些聽起來可能比較複雜,但謹記,在FFmpeg專案中,高中生也編寫了組合程式碼。與所有事物一樣,學習是50%術語和50%實際學習。 +所有這些聽起來可能比較複雜,但請記住,在FFmpeg專案中,高中生也能編寫組合程式碼。與所有學習一樣,掌握組合語言是50%理解術語和50%實際動手練習。 **為什麼我們用組合語言編寫?** -為了使多媒體處理得更快。編寫組合程式碼通常能帶來10倍或以上的速度提升,這對於即時播放視訊而不卡頓尤為重要。它還能節省能源並延長電池壽命。值得注意的是,視訊編碼和解碼是全球最常用的功能之一,無論是普通使用者還是大型企業的資料中心都用到它們。因此,即使是微小的改進,也能迅速積累出顯著的效果。 +為了使多媒體處理得更快。編寫組合程式碼通常能帶來10倍或以上的速度提升,這對於即時播放視訊而不卡頓尤為重要。它還能節省能源並延長電池壽命。值得注意的是,視訊編碼和解碼是全球最常用的功能之一,無論是一般使用者還是大型企業的資料中心都用到它們。因此,即使是微小的改進,也能迅速累積出顯著的效果。 -在網路上,你會經常看到人們使用*內聯函式 (intrinsics)*,這些C語言風格的函式映射到組合指令,以便更快地開發。在 FFmpeg 中,我們不使用內聯函式,而是手寫組合程式碼。這是一個有爭議的部分,但內聯函式通常比手寫組合慢約10-15%(內聯函式支持者可能會不同意),這取決於編譯器。對於 FFmpeg 來說,每一點額外的效能提升都有用,這就是為什麼我們直接使用組合程式碼。還有一種觀點認為,內聯函式由於使用了「[匈牙利命名法](https://zh.wikipedia.org/wiki/匈牙利命名法)」而難以閱讀。 +在網路上,你會經常看到人們使用*內聯函式 (intrinsics)*,這些C語言風格的函式映射到組合指令,以便更快地開發。在 FFmpeg 中,我們不使用內聯函式,而是手寫組合程式碼。這是一個有爭議的部分,但內聯函式通常比手寫組合慢約10-15%(內聯函式支持者可能會不同意),這取決於編譯器。對於 FFmpeg 來說,每一點額外的效能提升都很重要,這就是為什麼我們直接使用組合程式碼。還有一種觀點認為,內聯函式由於使用了「[匈牙利命名法](https://zh.wikipedia.org/wiki/匈牙利命名法)」而難以閱讀。 出於歷史原因,你可能還會在 FFmpeg 的一些地方看到*內聯組合 (inline assembly)*(即不使用內聯函式),或者在 Linux 核心等專案中因為非常特殊的應用場景而看到內聯組合。這是指組合程式碼不寫在單獨的檔案中,在像 FFmpeg 這樣的專案中,主流觀點認為這種程式碼難以閱讀,缺乏廣泛的編譯器支援,並且難以維護。 -最後,你會看到很多自稱專家的人在網路上說這些都是沒必要的,編譯器可以為你完成所有這些「向量化」工作。至少以學習的目的,請忽視他們:例如,[dav1d專案](https://www.videolan.org/projects/dav1d.html)的最近測試顯示,這種自動向量化帶來了約2倍的速度提升,而手寫版本可以達到8倍。 +最後,你會看到很多自稱專家的人在網路上說這些都是沒必要的,編譯器可以為你完成所有這些「向量化」工作。至少以學習的目的來說,請忽視他們:例如,[dav1d專案](https://www.videolan.org/projects/dav1d.html)的最近測試顯示,這種自動向量化帶來了約2倍的速度提升,而手寫版本可以達到8倍。 **組合語言的風格** -本課程將專注於 x86 64 位組合語言,它也被稱為 amd64,儘管它同樣適用於 Intel 處理器。此外,不同的 CPU 也有其他類型的組合語言,例如 ARM 和 RISC-V,未來這些課程可能會擴展到涵蓋這些架構。 +本課程將專注於 x86 64 位元組合語言,它也被稱為 amd64,儘管它同樣適用於 Intel 處理器。此外,不同的 CPU 也有其他類型的組合語言,例如 ARM 和 RISC-V,未來這些課程可能會擴展到涵蓋這些架構。 網路上常見的 x86 組合風格有兩種:AT&T 和 Intel。AT&T 語法較早,但相較於 Intel 語法更難閱讀。因此,我們將使用 Intel 風格。 @@ -99,7 +99,7 @@ FFmpeg中的絕大多數組合程式碼都是所謂的*SIMD,單指令多資料 **一個簡單的純量組合 (scalar asm) 片段** -讓我們看一段簡單(且非常人工構造)的純量組合程式碼(每條指令一次只對單個資料項進行操作的組合程式碼),看看發生了什麼: +讓我們看一段簡單(且相當人工建構)的純量組合程式碼(每條指令一次只對單個資料項進行操作的組合程式碼),看看發生了什麼: ```assembly mov r0q, 3 @@ -175,7 +175,7 @@ cglobal add_values, 2, 2, 2, src, src2 movu m1, [src2q] ``` -movu 是 movdqu(move double quad unaligned 移動雙四字未對齊)的簡寫。關於對齊的內容我們會在後續課程中講解,但目前可以將 movu 視為從 [srcq] 進行 128 位元資料傳輸。對於 mov 指令,方括號表示對 [srcq] 指向的位址進行解參照,相當於 *C 語言中的 \*src*,這就是所謂的「載入」(load)。需要注意的是,q 後綴表示指標的大小(即在 C 語言中在 64 位元系統上 sizeof(*src) == 8,x86 組合在 32 位元系統上則會自動調整為 32 位元),但實際載入的資料仍然是 128 位元的。 +movu 是 movdqu(move double quad unaligned 移動雙四字未對齊)的簡寫。關於對齊的內容我們會在後續課程中講解,但目前可以將 movu 視為從 [srcq] 進行 128 位元資料傳輸。對於 mov 指令,方括號表示對 [srcq] 指向的位址進行解參照,相當於 *C 語言中的 \*src*,這就是所謂的「載入」(load)。需要注意的是,q 後綴表示指標的大小(即在 C 語言中在 64 位元系統上 `sizeof(*src) == 8`,x86 組合在 32 位元系統上則會自動調整為 32 位元),但實際載入的資料仍然是 128 位元的。 需要注意的是,我們並不使用向量暫存器的完整名稱(例如 xmm0),而是使用抽象形式的 m0。在後續課程中,你會看到這種方式的優勢 —— 它允許你編寫一次程式碼,並在不同大小的 SIMD 暫存器上通用適用。 @@ -183,7 +183,7 @@ movu 是 movdqu(move double quad unaligned 移動雙四字未對齊)的簡 paddb m0, m1 ``` -paddb(可以在腦海中讀作 *p-add-b*)會對每個暫存器中的每個位元組進行相加,如下所示。「p」 前綴代表 「packed」(打包),用於區分向量指令和純量指令。「b」 後綴表示這是 按位元組(bytewise) 進行的加法運算。 +paddb(可以在腦海中讀作 *p-add-b*)會對每個暫存器中的每個位元組進行相加,如下所示。「p」 前綴代表 「packed」(打包),用於區分向量指令和純量指令。「b」 後綴表示這是按位元組(bytewise)進行的加法運算。 | a | b | c | d | e | f | g | h | i | j | k | l | m | n | o | p | | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | @@ -210,6 +210,6 @@ RET 這是一個表示函式返回的巨集 (macro)。在 FFmpeg 中,幾乎所有組合函式都會直接修改參數中的資料,而不是返回一個值。 -在後續的作業中,你會看到,我們會為組合函式創建函式指標,並在可用的地方使用它們。 +在後續的作業中,你會看到,我們會為組合函式建立函式指標,並在可用的地方使用它們。 [下一課](../lesson_02/index_zh-hant.md) diff --git a/lesson_02/index_zh-hans.md b/lesson_02/index_zh-hans.md new file mode 100644 index 0000000..d165f1f --- /dev/null +++ b/lesson_02/index_zh-hans.md @@ -0,0 +1,168 @@ +**FFmpeg 汇编语言第二课** + +现在你已经写过了第一个汇编函数,我们开始介绍分支(branches)和循环(loops)。 + +我们先介绍标签(labels)与跳转(jumps)的概念。在下方构造的例子里,jmp(跳转)指令将代码执行流程移动到 ".loop:" 指令下方。".loop:" 就是一个*标签*,标签前缀的点(.)表示它是一个*本地标签*,这样就可以在多个函数中重复使用同一个标签名。当然,这个例子展示的是一个无限循环,但我们稍后会将其扩展到更现实的场景。 + +```assembly +mov r0q, 3 +.loop: + dec r0q + jmp .loop +``` + +在开始实际循环之前,我们必须先介绍 *FLAGS* (标志)寄存器。我们不会过多讨论 *FLAGS* (标志)的复杂性(同样因为 GPR 操作主要是脚手架),但有几个标志(flags),如零标志(Zero-Flag)、符号标志(Sign-Flag)和溢出标志(Overflow-Flag),这些标志是根据大多数非移动(non-mov)指令对标量数据(如算术运算和移位)的输出设置的。 + +下面的示例中,循环计数器倒数到 0,jg(**j**ump if **g**reater than zero,大于 0 则跳转)是循环条件。dec r0q 指令会根据该指令执行后 r0q 的值设置 FLAGS,然后你可以根据这些 FLAGS 进行跳转。 + +```assembly +mov r0q, 3 +.loop: + ; do something + dec r0q + jg .loop ; 大于 0 则跳转 +``` + +它等同于如下 C 代码: + +```c +int i = 3; +do +{ + // do something + i--; +} while(i > 0) +``` + +这段 C 代码有点不太自然。一般来说 C 语言里循环功能是这么写的: + +```c +int i; +for(i = 0; i < 3; i++) { + // do something +} +``` + +这段大致等同于(没有简单的方法来完全匹配这个 ```for``` 循环): + +```assembly +xor r0q, r0q +.loop: + ; do something + inc r0q + cmp r0q, 3 + jl .loop ; 如果 (r0q - 3) < 0 则跳转, 即 (r0q < 3) +``` + +这段代码有几点值得注意。首先是 ```xor r0q, r0q```,这是一种常见的将寄存器清零的方法,在某些系统上比 ```mov r0q, 0``` 更快,简而言之,这是因为不需要进行实际的加载操作。这也可以用于 SIMD 寄存器,通过 ```pxor m0, m0``` 来将整个寄存器清零。另一个需要注意的是 cmp (compare,对比)的使用。cmp 实际上是从第一个寄存器中减去第二个寄存器的值(但不存储结果)并设置 **FLAGS**,但根据注释所示,它可以与跳转指令一起理解,(jl = **j**ump if **l**ess than zero,如果小于零则跳转)以在 ```r0q < 3``` 时进行跳转。 + +请注意,在这个代码片段中有一条额外的指令(cmp)。一般来说,指令越少意味着代码运行越快,这就是为什么之前的代码片段更受青睐。正如你将在后续课程中看到的,还有更多技巧可以用来避免这种额外指令,并利用算术或其他操作来设置 **FLAGS** 寄存器。需要注意的是,我们编写汇编代码时并不是为了完全匹配 C 语言的循环结构,而是为了在汇编中让循环尽可能地高效。 + +以下是一些你将会用到的常见跳转助记符(jump mnemonics)(*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 | + +**常量(Constants)** + +让我们看几个展示如何使用常量的例子: + +```assembly +SECTION_RODATA + +constants_1: db 1,2,3,4 +constants_2: times 2 dw 4,3,2,1 +``` + +* SECTION_RODATA 指定这是一个只读数据段(read-only data section)。(这是一个宏,因为不同操作系统使用不同的声明方式输出文件格式) +* constants_1:标签 constants_1 通过 ```db```(声明字节,declare byte)定义 - 相当于 uint8_t constants_1[4] = {1, 2, 3, 4}; +* constants_2:这里使用 ```times 2``` 宏来重复声明的字(words)- 相当于 uint16_t constants_2[8] = {4, 3, 2, 1, 4, 3, 2, 1}; + +这些标签会被汇编器转换为内存地址,然后可以用于加载操作(但由于是只读的,不能用于存储操作)。某些指令可以直接将内存地址作为操作数(operand),而无需显式地先加载到寄存器中(这种做法既有优点也有缺点)。 + +**偏移量(Offsets)** + +偏移量是内存中连续元素之间的距离(以字节为单位)。偏移量由数据结构中**每个元素的大小**决定。 + +现在我们已经能够编写循环,是时候获取数据了。但这与 C 语言相比有一些区别。让我们看看以下例子中的 C 语言循环: + +```c +uint32_t data[3]; +int i; +for(i = 0; i < 3; i++) { + data[i]; +} +``` + +C 编译器会预先计算出数组元素之间的 4 字节偏移量。但在手写汇编代码时,你需要自己计算这些偏移量。 + +让我们看看内存地址计算的语法。这适用于所有类型的内存地址: + +```assembly +[base + scale*index + disp] +``` + +* base - 这是一个通用寄存器(GPR)(通常是 C 函数的参数的指针) +* scale - 这可以是 1、2、4 或 8。默认值为 1 +* index - 这是一个通用寄存器(GPR)(通常是循环计数器) +* disp - 这是一个整数(最大 32 位)。位移(Displacement)是数据的偏移量 + +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] + + ; do some things + + add srcq, mmsize +dec r1q +jg .loop + +RET +``` + +请注意在 ```movu m1, [srcq+2*r1q+3+mmsize]``` 中,汇编器会预先计算出正确的位移常量。在下一课中,我们将介绍一个技巧,可以避免在循环中同时使用 add 和 dec 指令,而是用单个 add 指令来替代它们。 + +**LEA** + +现在你已经理解了偏移量,你可以使用 lea(加载有效地址,Load Effective Address)指令。它能让你用一条指令完成乘法和加法运算,这比使用多条指令要快得多。当然,lea 对可以乘以什么数和加上什么值有一些限制,但这并不妨碍 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 ; shift arithmetic left(左移运算) 3 = * 8 +add r3q, 5 +add r0q, r3q +``` + +你会看到 lea 经常被用来在循环前设置地址或执行上述计算。当然需要注意的是,你不能用 lea 执行所有类型的乘法和加法运算,但乘以 1、2、4、8 和添加固定偏移量是很常见的用法。 + +在作业中,你需要在循环中加载常量并将这些值添加到 SIMD 向量中。 + +[下一课](../lesson_03/index_zh-hans.md) diff --git a/lesson_02/index_zh-hant.md b/lesson_02/index_zh-hant.md new file mode 100644 index 0000000..4347f1d --- /dev/null +++ b/lesson_02/index_zh-hant.md @@ -0,0 +1,168 @@ +**FFmpeg 組合語言第二課** + +現在你已經寫過了第一個組合函式,我們開始介紹分支(branches)和迴圈(loops)。 + +我們先介紹標籤(labels)與跳轉(jumps)的概念。在下方建構的例子裡,jmp(跳轉)指令將程式碼執行流程移動到 ".loop:" 指令下方。".loop:" 就是一個*標籤*,標籤字首的點(.)表示它是一個*本地標籤*,這樣就可以在多個函式中重複使用同一個標籤名稱。當然,這個例子展示的是一個無限迴圈,但我們稍後會將其擴展到更實際的場景。 + +```assembly +mov r0q, 3 +.loop: + dec r0q + jmp .loop +``` + +在開始實際迴圈之前,我們必須先介紹 *FLAGS* (旗標)暫存器。我們不會過多討論 *FLAGS* (旗標)的複雜性(同樣因為 GPR 操作主要是鷹架),但有幾個旗標(flags),如零旗標(Zero-Flag)、符號旗標(Sign-Flag)和溢位旗標(Overflow-Flag),這些旗標是根據大多數非移動(non-mov)指令對標量資料(如算術運算和位移)的輸出設定的。 + +下面的範例中,迴圈計數器倒數到 0,jg(**j**ump if **g**reater than zero,大於 0 則跳轉)是迴圈條件。dec r0q 指令會根據該指令執行後 r0q 的值設定 FLAGS,然後你可以根據這些 FLAGS 進行跳轉。 + +```assembly +mov r0q, 3 +.loop: + ; do something + dec r0q + jg .loop ; jump if greater than zero +``` + +它等同於如下 C 程式碼: + +```c +int i = 3; +do +{ + // do something + i--; +} while(i > 0) +``` + +這段 C 程式碼有點不太自然。一般來說 C 語言裡迴圈功能是這麼寫的: + +```c +int i; +for(i = 0; i < 3; i++) { + // do something +} +``` + +這段大致等同於(沒有簡單的方法來完全匹配這個 ```for``` 迴圈): + +```assembly +xor r0q, r0q +.loop: + ; do something + inc r0q + cmp r0q, 3 + jl .loop ; jump if (r0q - 3) < 0, i.e (r0q < 3) +``` + +這段程式碼有幾點值得注意。首先是 ```xor r0q, r0q```,這是一種常見的將暫存器清零的方法,在某些系統上比 ```mov r0q, 0``` 更快,簡而言之,這是因為不需要進行實際的載入操作。這也可以用於 SIMD 暫存器,通過 ```pxor m0, m0``` 來將整個暫存器清零。另一個需要注意的是 cmp (compare,比較)的使用。cmp 實際上是從第一個暫存器中減去第二個暫存器的值(但不儲存結果)並設定 **FLAGS**,但根據註解所示,它可以與跳轉指令一起理解,(jl = **j**ump if **l**ess than zero,如果小於零則跳轉)以在 ```r0q < 3``` 時進行跳轉。 + +請注意,在這個程式碼片段中有一條額外的指令(cmp)。一般來說,指令越少意味著程式碼執行越快,這就是為什麼之前的程式碼片段更受青睞。正如你將在後續課程中看到的,還有更多技巧可以用來避免這種額外指令,並利用算術或其他操作來設定 **FLAGS** 暫存器。需要注意的是,我們編寫組合程式碼時並不是為了完全匹配 C 語言的迴圈結構,而是為了在組合語言中讓迴圈盡可能地高效。 + +以下是一些你將會用到的常見跳轉助記符(jump mnemonics)(*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 | + +**常數(Constants)** + +讓我們看幾個展示如何使用常數的例子: + +```assembly +SECTION_RODATA + +constants_1: db 1,2,3,4 +constants_2: times 2 dw 4,3,2,1 +``` + +* SECTION_RODATA 指定這是一個唯讀資料段(read-only data section)。(這是一個巨集,因為不同作業系統使用不同的宣告方式輸出檔案格式) +* constants_1:標籤 constants_1 通過 ```db```(宣告位元組,declare byte)定義 - 相當於 uint8_t constants_1[4] = {1, 2, 3, 4}; +* constants_2:這裡使用 ```times 2``` 巨集來重複宣告的字(words)- 相當於 uint16_t constants_2[8] = {4, 3, 2, 1, 4, 3, 2, 1}; + +這些標籤會被組譯器轉換為記憶體位址,然後可以用於載入操作(但由於是唯讀的,不能用於儲存操作)。某些指令可以直接將記憶體位址作為運算元(operand),而無需顯式地先載入到暫存器中(這種做法既有優點也有缺點)。 + +**偏移量(Offsets)** + +偏移量是記憶體中連續元素之間的距離(以位元組為單位)。偏移量由資料結構中**每個元素的大小**決定。 + +現在我們已經能夠編寫迴圈,是時候獲取資料了。但這與 C 語言相比有一些區別。讓我們看看以下例子中的 C 語言迴圈: + +```c +uint32_t data[3]; +int i; +for(i = 0; i < 3; i++) { + data[i]; +} +``` + +C 編譯器會預先計算出陣列元素之間的 4 位元組偏移量。但在手寫組合程式碼時,你需要自己計算這些偏移量。 + +讓我們看看記憶體位址計算的語法。這適用於所有類型的記憶體位址: + +```assembly +[base + scale*index + disp] +``` + +* base - 這是一個通用暫存器(GPR)(通常是 C 函式的參數的指標) +* scale - 這可以是 1、2、4 或 8。預設值為 1 +* index - 這是一個通用暫存器(GPR)(通常是迴圈計數器) +* disp - 這是一個整數(最大 32 位元)。位移(Displacement)是資料的偏移量 + +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] + + ; do some things + + add srcq, mmsize +dec r1q +jg .loop + +RET +``` + +請注意在 ```movu m1, [srcq+2*r1q+3+mmsize]``` 中,組譯器會預先計算出正確的位移常數。在下一課中,我們將介紹一個技巧,可以避免在迴圈中同時使用 add 和 dec 指令,而是用單個 add 指令來替代它們。 + +**LEA** + +現在你已經理解了偏移量,你可以使用 lea(載入有效位址,Load Effective Address)指令。它能讓你用一條指令完成乘法和加法運算,這比使用多條指令要快得多。當然,lea 對可以乘以什麼數和加上什麼值有一些限制,但這並不妨礙 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 ; shift arithmetic left(左移運算) 3 = * 8 +add r3q, 5 +add r0q, r3q +``` + +你會看到 lea 經常被用來在迴圈前設定位址或執行上述計算。當然需要注意的是,你不能用 lea 執行所有類型的乘法和加法運算,但乘以 1、2、4、8 和添加固定偏移量是很常見的用法。 + +在作業中,你需要在迴圈中載入常數並將這些值添加到 SIMD 向量中。 + +[下一課](../lesson_03/index_zh-hans.md) From de830f965afd84dbda5b54968a1b55419266ebd8 Mon Sep 17 00:00:00 2001 From: Zachary Wang Date: Thu, 3 Apr 2025 23:15:00 +1100 Subject: [PATCH 11/11] Chinese (s & t) version of lesson 3 --- lesson_03/index_zh-hans.md | 207 +++++++++++++++++++++++++++++++++++++ lesson_03/index_zh-hant.md | 205 ++++++++++++++++++++++++++++++++++++ 2 files changed, 412 insertions(+) create mode 100644 lesson_03/index_zh-hans.md create mode 100644 lesson_03/index_zh-hant.md diff --git a/lesson_03/index_zh-hans.md b/lesson_03/index_zh-hans.md new file mode 100644 index 0000000..fc058a6 --- /dev/null +++ b/lesson_03/index_zh-hans.md @@ -0,0 +1,207 @@ +**FFmpeg 汇编语言第三课** + +让我们解释一些更多的术语,并为你提供一个简短的历史课程。 + +**指令集(Instruction Sets)** + +你可能已经注意到在上一课中我们谈到了 SSE2,它是一组 SIMD 指令。当新一代 CPU 发布时,它可能会配备新的指令,有时还会有更大的寄存器尺寸。x86 指令集的历史非常复杂,所以这里是一个简化的历史(还有更多的子类别): + +* MMX - 1997年推出,Intel处理器中的首个SIMD,64位寄存器,历史悠久 +* SSE (Streaming SIMD Extensions) - 1999年推出,128位寄存器 +* SSE2 - 2000年推出,许多新指令 +* SSE3 - 2004年推出,首个水平(horizontal)指令 +* SSSE3 (Supplemental SSE3) - 2006年推出,新指令,但最重要的是 pshufb 洗牌(shuffle)指令,可以说是视频处理中最重要的指令 +* SSE4 - 2008年推出,许多新指令,包括打包最小值和最大值 +* AVX - 2011年推出,256位寄存器(仅浮点)和新的三操作数语法 +* AVX2 - 2013年推出,整数指令的256位寄存器 +* AVX512 - 2017年推出,512位寄存器,新的操作掩码特性。当时在 FFmpeg 中使用有限,因为使用新指令时CPU频率会降低。具有完整的512位洗牌(排列)功能,使用 vpermb +* AVX512ICL - 2019年推出,不再有时钟频率降低问题 +* AVX10 - 即将推出 + +值得注意的是,指令集可以从 CPU 中移除,也可以添加。例如,AVX512 在第12代 Intel CPU 中被[移除](https://www.igorslab.de/en/intel-deactivated-avx-512-on-alder-lake-but-fully-questionable-interpretation-of-efficiency-news-editorial/),而引起了争议。因此,FFmpeg 会进行运行时 CPU 检测(runtime CPU detection)。FFmpeg 会检测正在运行的 CPU 的能力。 + +正如你在作业中看到的,函数指针默认为 C 语言风格,但会被特定指令集变体替换。这意味着检测只需进行一次,之后无需再次进行。这与许多专有软件形成对比,后者硬编码特定指令集,以至于即使是功能完善的计算机也因此被淘汰。这也允许在运行时开启/关闭优化函数。这是开源的一大好处。 + +像 FFmpeg 这样的程序正运行在全球数十亿设备上,其中一些可能非常老旧。FFmpeg 技术上支持仅支持 SSE 的机器,这些机器已有25年历史!幸好,x86inc.asm 能够告诉你是否使用了特定指令集中不可用的指令。 + +为了让你了解真实世界的能力,以下是截至2024年11月 [Steam 调查](https://store.steampowered.com/hwsurvey/Steam-Hardware-Software-Survey-Welcome-to-Steam) 的指令集可用性(这显然偏向于游戏玩家): + +| 指令集 | 可用性 | +| :---- | :---- | +| 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) 中有广泛的测试基础设施,用于测试 CPU/操作系统/编译器的各种变体。每一个提交的代码都会在数百台机器上运行,以确保没有问题发生。 + +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 汇编的一部分挑战是找到适合你需求的正确指令。在某些情况下,指令可以以非原本设计的方式使用。 + +**指针偏移技巧** + +让我们回到第一课中原来的函数,但给 C 函数添加一个宽度参数。 + +我们对宽度变量使用 ptrdiff_t 而不是 int,以确保64位参数的高32位为零。如果我们在函数签名中直接传递 int 类型的宽度,并尝试将其用作指针算术的四字(即:使用 `widthq`),那么寄存器的高32位可能会填充任意值。我们可以通过使用`movsxd`(另见 x86inc.asm 中的宏 `movsxdifnidn`)对宽度进行符号扩充来修复这个问题,但这是一种更简单的方法。 + +下面的函数包含指针偏移技巧: + +```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 指令。它还允许在多个加载和存储中使用指针偏移,以及在需要时使用指针偏移的倍数(请记住这一点,用于作业)。 + +**对齐(Alignment)** + +在我们所有的例子中,我们一直使用 movu 来避免对齐(alignment)这个话题。如果数据是对齐的,即内存地址可以被 SIMD 寄存器大小整除,许多 CPU 可以更快地加载和存储数据。在可能的情况下,我们尝试在 FFmpeg 中使用 mova 进行对齐加载和存储。 + +在 FFmpeg 中,av_malloc 能够在堆(heap)上提供对齐的内存,而 DECLARE_ALIGNED C 预处理器指令可以在栈(stack)上提供对齐的内存。如果使用未对齐的地址调用 mova,将导致段错误(segmentation fault),软件会崩溃。确保对齐值与SIMD寄存器大小相对应也很重要,即 xmm 为16,ymm 为32,zmm 为64。 + +以下是如何将 RODATA 部分的开头对齐到64字节: + +```assembly +SECTION_RODATA 64 +``` + +请注意,这只是对齐 RODATA 的开头。可能需要填充字节来确保下一个标签保持在64字节边界上。 + +**范围扩充(Range expansion)** + +我们到现在为止还避免讨论的另一个话题是溢出(overflowing)。例如,当字节值在加法或乘法等操作后超过255时,就会发生这种情况。我们可能想执行一个操作,其中需要一个比字节更大的中间值(例如字),或者我们可能希望将数据保留在那个更大的中间值的大小中。 + +对于无符号字节,这就是 punpcklbw(packed unpack high bytes to words,打包解压低字节到字)和 punpckhbw(packed unpack high bytes to words,打包解压高字节到字)发挥作用的地方。 + +让我们看看 punpcklbw 如何工作。Intel 手册中 SSE2 版本的语法如下: + + +| PUNPCKLBW xmm1, xmm2/m128 | +| :---- | + + +这意味着其源(右侧)可以是 xmm 寄存器或内存地址(m128 表示具有标准 [base + scale*index + disp] 语法的内存地址),而目标是xmm寄存器。 + +上面的 officedaytime.com 网站有一个很好的图表展示了发生的情况: + +![图片描述](image1.png) + +你可以看到字节是从每个寄存器的下半部分交错的。但这与范围扩充有什么关系呢?如果 src 寄存器全为零,这会将 dst 中的字节与零交错。这就是所谓的*零扩充*,因为字节是无符号的。punpckhbw 可以用于对高字节执行相同的操作。 + +这段代码展示这是如何做到的: + +```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``` 和 ```m1``` 现在包含已被零扩充为字的原始字节。在下一课中,你将看到 AVX 中的三操作数(three-operand )指令如何使第二个 movu 变得不必要。 + + +**符号扩充(Sign extension)** + +有符号数据稍微复杂一些。要扩充有符号整数的范围,我们需要使用一个称为 [符号扩充](https://zh.wikipedia.org/wiki/%E7%AC%A6%E5%8F%B7%E6%89%A9%E5%85%85) 的过程。这会用符号位填充 MSB。例如:int8_t 中的-2是 0b11111110。要将其符号扩充到 int16_t,MSB 的1被重复以生成 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. + +```pcmpgtb```(packed compare greater than byte,打包比较大于字节)可用于符号扩充。通过执行比较(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 +``` + +如你所见,与无符号情况相比多了一条额外的指令。 + +**打包(Packing)** + +packuswb(pack unsigned word to byte,打包无符号字到字节)和 packsswb 都允许你从字转到字节。它允许你将包含字的两个 SIMD 寄存器交错到一个包含字节的 SIMD 寄存器中。注意,如果值超出字节范围,它们将被饱和(即限制在最大值)。 + +**洗牌(Shuffles)** + +洗牌,也称为置换,可以说是视频处理中最重要的指令,而 pshufb(packed shuffle bytes,打包洗牌字节),在 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 位场(bitfield)(二进制补码),因此MSB(0x80)被设置。 + +[image1]: \ No newline at end of file diff --git a/lesson_03/index_zh-hant.md b/lesson_03/index_zh-hant.md new file mode 100644 index 0000000..7acdbd7 --- /dev/null +++ b/lesson_03/index_zh-hant.md @@ -0,0 +1,205 @@ +**FFmpeg 組合語言第三課** + +讓我們解釋一些更多的術語,並為你提供一個簡短的歷史課程。 + +**指令集(Instruction Sets)** + +你可能已經注意到在上一課中我們談到了 SSE2,它是一組 SIMD 指令。當新一代 CPU 發布時,它可能會配備新的指令,有時還會有更大的暫存器尺寸。x86 指令集的歷史非常複雜,所以這裡是一個簡化的歷史(還有更多的子類別): + +* MMX - 1997年推出,Intel處理器中的首個SIMD,64位元暫存器,歷史悠久 +* SSE (Streaming SIMD Extensions) - 1999年推出,128位元暫存器 +* SSE2 - 2000年推出,許多新指令 +* SSE3 - 2004年推出,首個水平(horizontal)指令 +* SSSE3 (Supplemental SSE3) - 2006年推出,新指令,但最重要的是 pshufb 洗牌(shuffle)指令,可以說是視訊處理中最重要的指令 +* SSE4 - 2008年推出,許多新指令,包括打包最小值和最大值 +* AVX - 2011年推出,256位元暫存器(僅浮點)和新的三運算元語法 +* AVX2 - 2013年推出,整數指令的256位元暫存器 +* AVX512 - 2017年推出,512位元暫存器,新的操作遮罩特性。當時在 FFmpeg 中使用有限,因為使用新指令時CPU頻率會降低。具有完整的512位元洗牌(排列)功能,使用 vpermb +* AVX512ICL - 2019年推出,不再有時脈頻率降低問題 +* AVX10 - 即將推出 + +值得注意的是,指令集可以從 CPU 中移除,也可以添加。例如,AVX512 在第12代 Intel CPU 中被[移除](https://www.igorslab.de/en/intel-deactivated-avx-512-on-alder-lake-but-fully-questionable-interpretation-of-efficiency-news-editorial/),而引起了爭議。因此,FFmpeg 會進行執行時 CPU 檢測(runtime CPU detection)。FFmpeg 會檢測正在執行的 CPU 的能力。 + +正如你在作業中看到的,函式指標預設為 C 語言風格,但會被特定指令集變體替換。這意味著檢測只需進行一次,之後無需再次進行。這與許多專有軟體形成對比,後者硬編碼特定指令集,以至於即使是功能完善的電腦也因此被淘汰。這也允許在執行時開啟/關閉最佳化函式。這是開源的一大好處。 + +像 FFmpeg 這樣的程式正執行在全球數十億裝置上,其中一些可能非常老舊。FFmpeg 技術上支援僅支援 SSE 的機器,這些機器已有25年歷史!幸好,x86inc.asm 能夠告訴你是否使用了特定指令集中不可用的指令。 + +為了讓你了解真實世界的能力,以下是截至2024年11月 [Steam 調查](https://store.steampowered.com/hwsurvey/Steam-Hardware-Software-Survey-Welcome-to-Steam) 的指令集可用性(這顯然偏向於遊戲玩家): + +| 指令集 | 可用性 | +| :---- | :---- | +| 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) 中有廣泛的測試基礎設施,用於測試 CPU/作業系統/編譯器的各種變體。每一個提交的程式碼都會在數百台機器上執行,以確保沒有問題發生。 + +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 組合的一部分挑戰是找到適合你需求的正確指令。在某些情況下,指令可以以非原本設計的方式使用。 + +**指標偏移技巧** + +讓我們回到第一課中原來的函式,但給 C 函式添加一個寬度參數。 + +我們對寬度變數使用 ptrdiff_t 而不是 int,以確保64位元參數的高32位元為零。如果我們在函式簽名中直接傳遞 int 類型的寬度,並嘗試將其用作指標算術的四字(即:使用 `widthq`),那麼暫存器的高32位元可能會填充任意值。我們可以通過使用`movsxd`(另見 x86inc.asm 中的巨集 `movsxdifnidn`)對寬度進行符號擴充來修復這個問題,但這是一種更簡單的方法。 + +下面的函式包含指標偏移技巧: + +```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 指令。它還允許在多個載入和儲存中使用指標偏移,以及在需要時使用指標偏移的倍數(請記住這一點,用於作業)。 + +**對齊(Alignment)** + +在我們所有的例子中,我們一直使用 movu 來避免對齊(alignment)這個話題。如果資料是對齊的,即記憶體位址可以被 SIMD 暫存器大小整除,許多 CPU 可以更快地載入和儲存資料。在可能的情況下,我們嘗試在 FFmpeg 中使用 mova 進行對齊載入和儲存。 + +在 FFmpeg 中,av_malloc 能夠在堆疊(heap)上提供對齊的記憶體,而 DECLARE_ALIGNED C 前處理器指令可以在堆疊(stack)上提供對齊的記憶體。如果使用未對齊的位址呼叫 mova,將導致段錯誤(segmentation fault),軟體會當機。確保對齊值與SIMD暫存器大小相對應也很重要,即 xmm 為16,ymm 為32,zmm 為64。 + +以下是如何將 RODATA 部分的開頭對齊到64位元組: + +```assembly +SECTION_RODATA 64 +``` + +請注意,這只是對齊 RODATA 的開頭。可能需要填充位元組來確保下一個標籤保持在64位元組邊界上。 + +**範圍擴充(Range expansion)** + +我們到現在為止還避免討論的另一個話題是溢位(overflowing)。例如,當位元組值在加法或乘法等操作後超過255時,就會發生這種情況。我們可能想執行一個操作,其中需要一個比位元組更大的中間值(例如字),或者我們可能希望將資料保留在那個更大的中間值的大小中。 + +對於無符號位元組,這就是 punpcklbw(packed unpack high bytes to words,打包解壓低位元組到字)和 punpckhbw(packed unpack high bytes to words,打包解壓高位元組到字)發揮作用的地方。 + +讓我們看看 punpcklbw 如何工作。Intel 手冊中 SSE2 版本的語法如下: + + +| PUNPCKLBW xmm1, xmm2/m128 | +| :---- | + + +這意味著其來源(右側)可以是 xmm 暫存器或記憶體位址(m128 表示具有標準 [base + scale*index + disp] 語法的記憶體位址),而目標是xmm暫存器。 + +上面的 officedaytime.com 網站有一個很好的圖表展示了發生的情況: + +![圖片描述](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 中的三運算元(three-operand)指令如何使第二個 movu 變得不必要。 + + +**符號擴充(Sign extension)** + +有符號資料稍微複雜一些。要擴充有符號整數的範圍,我們需要使用一個稱為[符號擴充](https://zh.wikipedia.org/wiki/%E7%AC%A6%E5%8F%B7%E6%89%A9%E5%85%85)的過程。這會用符號位填充 MSB。例如:int8_t 中的-2是 0b11111110。要將其符號擴充到 int16_t,MSB 的1被重複以生成 0b1111111111111110。 + +```pcmpgtb```(packed compare greater than byte,打包比較大於位元組)可用於符號擴充。通過執行比較(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 +``` + +如你所見,與無符號情況相比多了一條額外的指令。 + +**打包(Packing)** + +packuswb(pack unsigned word to byte,打包無符號字到位元組)和 packsswb 都允許你從字轉到位元組。它允許你將包含字的兩個 SIMD 暫存器交錯到一個包含位元組的 SIMD 暫存器中。注意,如果值超出位元組範圍,它們將被飽和(即限制在最大值)。 + +**洗牌(Shuffles)** + +洗牌,也稱為置換,可以說是視訊處理中最重要的指令,而 pshufb(packed shuffle bytes,打包洗牌位元組),在 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 ; 根據 m1 洗牌 m0 +``` + +注意,-1被用作洗牌索引以清零輸出位元組,便於閱讀:位元組形式的-1是 0b11111111 位元場(bitfield)(二進位補碼),因此MSB(0x80)被設置。 + +[image1]: \ No newline at end of file