ASCII、Unicode、UTF - 8 与汇编代码优化解析
1. ASCII、Unicode 与 UTF - 8
在计算机处理字符数据时,编码是一个核心问题。早期,ASCII 编码在字符表示中占据主导地位,它简单直接,并且在一定程度上与更高级的国际标准 Unicode 兼容。
1.1 Unicode
在 Unicode 出现之前,为字符串添加额外字符的解决方案是临时的,缺乏全局规划。不同的代码页(code page)被用于映射一组数字到屏幕上的显示字符,但这种方法存在局限性。例如,当需要混合使用不同代码页,或者不同组织使用不同代码页时,就会出现问题。如果要写一篇用日语汉字讨论梵语及其向希伯来语发展的论文,代码页的局限性就会凸显出来。
同时,扩展字符集也面临挑战,因为现有的大量文档和程序通常使用 ASCII 编码。将它们迁移到新的标准会很困难。
Unicode 联盟提出的解决方案是将字符列表与字符的表示方式分离。Unicode 为任何语言的每个字符分配一个 32 位的数字,这提供了超过 40 亿种可能性,几乎不会用完。当前的 Unicode 13.0 版本有 143,859 个字符,这比 8 位或 16 位能表示的字符数量多得多,但只是 32 位所能表示范围的一小部分。
Unicode 字符通常用 U + XXXX 表示,XXXX 是十六进制值,这被称为字符的代码点(code point)。对于大多数语言,大多数值位于前 16 位,因此 Unicode 代码点通常用 16 位十六进制值表示,但也可以使用长达 32 位的十六进制值。对于常见的 ASCII 字符,代码点和 ASCII 码相同。
1.2 Unicode 编码与 UTF - 8
虽然每个字符都有唯一的代码点,但在计算机上的表示方式并不唯一,这些表示方式被称为字符编码。UTF - 32 是一种简单的 Unicode 编码,每个字符为 32 位宽,值就是 Unicode 值。但它几乎与其他编码不兼容。
因此,通常选择 UTF - 8 编码。UTF - 8 使用不同长度的字符,所以如果要查找字符串中的第 13 个字符,不能直接进行索引查找,而需要逐个字符处理,同时确定每个字符的长度。不过,其优点是,如果使用不支持 UTF - 8 的旧程序,只使用标准英语字符时,看起来就像 ASCII 编码。
UTF - 8 编码不同长度字符的方式是使用第一个字节的第一位来指定字符是否为多字节字符。在传统 ASCII 中,第一位总是 0,所以不会与 ASCII 产生冲突。具体编码规则如下:
- 代码点 U + 0000 到 U + 007F:编码为 1 字节,0xxxxxxx(7 位)
- 代码点 U + 0080 到 U + 07FF:编码为 2 字节,110xxxxx 10xxxxxx(11 位)
- 代码点 U + 0800 到 U + FFFF:编码为 3 字节,1110xxxx 10xxxxxx 10xxxxxx(16 位)
- 代码点 U + 10000 到 U + 10FFFF:编码为 4 字节,11110xxx 10xxxxxx 10xxxxxx 10xxxxxx(21 位)
例如,希腊小写字母 β 的代码点是 U + 03B2,二进制为 1110110010,长度为 10 位。在 UTF - 8 中,它被编码为 2 字节,允许 11 位,因此值扩展为 01110110010,编码为 11001110 10110010,十六进制为 0xCEB2。
如果要查找字符串中的第 n 个字符,可以使用每个字符的初始位来确定字符的长度。
1.3 UTF - 8 的一些特殊情况
在 UTF - 8 中,一些文档以字节顺序标记(BOM)开头,它基于另一种 Unicode 编码 UTF - 16。转换为 UTF - 8 时,这个值是 0xEFBBBF。如果这个值出现在 UTF - 8 文件的开头,可以基本忽略。
另一个重要问题是 Unicode 字符 U + FFFD,即“Unicode 替换字符”。当进行文本处理的系统遇到无效字符时,有时会用这个字符替换它们。在 UTF - 8 中,它编码为 0xEFBFBD。如果二进制文件或数据流被期望处理文本的系统处理,输出中大量出现 0xEFBFBD 字节就表明可能发生了这种情况。
1.4 ASCII 表
ASCII 非常重要,以下是 ASCII 字符的标准表,使用时,找到感兴趣的字符,将顶行的数字与左列的数字相加即可得到对应的 ASCII 码。
| +0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 |
|---|---|---|---|---|---|---|---|
| 0 | NULL | SOH | STX | ETX | EOT | ENQ | ACK |
| 8 | BS | HTAB | LF | VTAB | FF | CR | SO |
| 16 | DLE | DC1 | DC2 | DC3 | DC4 | NAK | SYN |
| 24 | CAN | EM | SUB | ESC | FS | GS | RS |
| 32 | ! | “ | # | $ | % | & | ’ |
| 40 | ( | ) | * | + | , | - | . |
| 48 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
| 56 | 8 | 9 | : | ; | < | = | > |
| 64 | @ | A | B | C | D | E | F |
| 72 | H | I | J | K | L | M | N |
| 80 | P | Q | R | S | T | U | V |
| 88 | X | Y | Z | [ | \ | ] | ^ |
| 96 | ‘ | a | b | c | d | e | f |
| 104 | h | i | j | k | l | m | n |
| 112 | p | q | r | s | t | u | v |
| 120 | x | y | z | { | | | } | DEL |
不过,ASCII 也并非完全标准。例如,计算机对行尾的编码方式不同。在 Windows 上,标准是用两个字符表示行尾,回车符(CR)将光标水平移到行首,换行符(LF)将光标垂直移到下一行;在基于 Unix 的机器上,使用单个 LF 表示行尾,同时完成水平和垂直移动;在非常旧的 Macintosh 上,则使用单个回车符。此外,水平制表符应代表多少个空格也是一个有争议的问题。
2. 汇编代码优化
优化汇编语言代码需要深入了解 CPU 的指令处理架构。现代处理器非常复杂,目标是充分利用每纳秒的运算时间。以下是一些常见的优化概念。
2.1 对齐(Alignment)
处理器通常以块为单位处理数据,即使处理单个值,在底层,处理器和计算机的其他部分也以更大的尺寸进行操作。
从代码角度看,内存中的每个字节都有独立的地址,可以从内存位置 17、18 或 19 轻松加载四字(quadword)。但对于实际的硬件 RAM 并非如此。在 x86 - 64 系统中,数据通常以四字块分组(与处理器的字长匹配)。如果尝试从内存加载四字,但该四字的地址不能被 8(四字的字节数)整除,处理器实际上需要从两个不同的物理 RAM 位置加载数据并拼接结果。
因此,需要关注数据地址,使要加载值的地址与数据在 RAM 中的物理存储方式匹配。可以使用 .balign 指令控制数据对齐。例如,要确保 .data 段中的值按 8 字节边界对齐,只需在标记地址并添加数据之前使用 .balign 8。
2.2 数据缓存(Data Caching)
当访问主内存时,处理器会在数据总线上发出请求,然后等待请求完成。但通常,使用过一次的内存很快会再次被使用,因此处理器会将值存储在一个或多个位于处理器上的小缓存中。当再次请求该内存时,如果在缓存中找到(缓存命中),就无需等待内存总线来满足内存需求。
数据以块的形式从内存传输到缓存,这些块称为缓存行(cache line)或缓存块(cache block)。大多数 x86 - 64 处理器的缓存行为 64 字节。当请求内存时,处理器通常会假设需要更多数据,因此会请求超过所需的数据,加载一个完整的缓存块到缓存中。这些缓存块按 64 字节对齐。
如果要处理内存中的多个值,将这些值存储在彼此靠近的位置有助于提高处理器速度。例如,如果处理一个大的结构体,更可能一起访问的部分应存储在一起,这将提高缓存性能。还可以使用 .balign 64 指令将部分数据对齐到最近的缓存块。
2.3 流水线(Pipelining)
处理器提高速度的一种方式是通过指令流水线。现代 CPU 指令很复杂,需要处理器执行多种操作,如指令提取、指令解码、内存访问、寄存器访问、算术运算、浮点运算以及将运算结果写入寄存器或内存。
现代处理器将指令分成多个阶段,这样可以同时执行多条指令。例如,当一条指令正在解码时,另一条指令正在进行内存提取。目标是让处理器的各个部分都忙于处理不同的指令。
处理器需要检测指令是否可以流水线执行。如果一条指令不能与另一条指令流水线执行,就会导致流水线停顿(pipeline stall)。例如,如果第一条指令最后写入 %rax,而第二条指令需要从 %rax 读取,处理器就不能对这两条指令进行流水线处理,因为第二条指令依赖于第一条指令的完成。作为程序员,无需担心这些,处理器会确保代码的正确执行。但在编程时考虑流水线,可以帮助处理器更优化地执行代码,目标是让处理器的各个部分都保持忙碌,尽量减少等待前一条指令结果的时间。
一些超标量处理器(superscalar processor)更进一步,可以同时启动两条指令,甚至可以乱序执行(前提是不影响代码的含义)。
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始]):::startend --> B(指令 1 提取):::process
B --> C(指令 1 解码):::process
C --> D(指令 1 内存访问):::process
D --> E(指令 1 执行):::process
E --> F(指令 1 写回):::process
G(指令 2 提取):::process --> H(指令 2 解码):::process
H --> I(指令 2 内存访问):::process
I --> J(指令 2 执行):::process
J --> K(指令 2 写回):::process
B --> G
C --> H
D --> I
E --> J
F --> K
F --> L([结束]):::startend
K --> L
这个流程图展示了两条指令的流水线执行过程,两条指令的不同阶段可以并行进行,提高了处理器的执行效率。
2.4 指令缓存与分支预测
代码也存储在内存中,处理器可以缓存代码所在的内存,从而更快地加载下一条指令。处理器还擅长预测程序的未来走向,因此可以预取即将执行的指令,避免在执行时等待内存。
但当遇到分支(如 jmp、call 或条件跳转)时,情况会发生变化。在分支处,代码不一定会执行下一条指令。对于条件分支,处理器甚至无法提前确定会选择哪个分支;对于间接跳转,甚至分支的目标地址也可能无法提前知道。这会影响缓存和流水线,因为两者都需要处理器知道下一条指令是什么。
分支预测允许处理器猜测条件分支代码可能选择的分支。预测不一定正确,但正确的预测会更快。否则,处理器会缓存错误的指令并开始流水线处理(称为推测执行)。当分支不按处理器的预测执行时,流水线会停顿,需要清除流水线并从内存读取实际指令。
分支预测可以通过多种方式实现,从简单的(总是以相同方式猜测)到更复杂的(跟踪不同分支被选择或未被选择的频率)。x86 指令集甚至允许向处理器提示分支更可能被选择还是不被选择,但现代处理器不再使用这些提示。
在进行间接跳转时,处理器需要更难的预测,即预测跳转的目标地址。现代处理器在一定程度上可以做到这一点,但需要在跳转前足够长的时间将值放入寄存器,以便处理器提前预测目标地址并做好相应准备。
有时,将分支目标按缓存行对齐会有帮助,这样在分支被预测时,代码可以更有效地移动到指令缓存中。可以在 .text 段使用 .balign 指令使分支目标正确对齐。当 .balign 在数据段时,用零填充数据;在 .text 段时,用 nop(或等效)指令填充指令。
2.5 选择指令和寄存器
指令选择也很重要。不同的指令在字节大小上不同,这会影响缓存。此外,不同的指令对流水线的影响也不同。例如,SSE 指令 movaps 和 movapd 实际上执行相同的操作,但 movaps 编码使用的字节数更少,因此更适合放入指令缓存。虽然 enter 指令专门用于设置栈帧,但实际上它比手动设置栈帧慢,尽管它需要更多的指令。
寄存器选择同样重要。有时,使用新寄存器存储结果比重用现有寄存器更好,因为这样可以实现更好的流水线处理。
优化汇编语言代码是一个复杂的过程,需要考虑多个因素。通过了解这些常见的优化概念,可以更好地编写高效的汇编代码。如果对优化汇编语言感兴趣,可以进一步了解处理器的内部架构和各种指令的详细操作。
ASCII、Unicode、UTF - 8 与汇编代码优化解析
2. 汇编代码优化(续)
2.6 优化操作步骤总结
为了更好地进行汇编代码优化,下面总结一些具体的操作步骤:
1.
对齐优化
- 确定数据类型和处理器架构,对于 x86 - 64 系统,数据通常以四字块分组,关注 8 字节对齐。
- 在
.data
段使用
.balign 8
指令确保数据按 8 字节边界对齐,示例代码如下:
.data
.balign 8
my_variable: .quad 123 ; 按 8 字节对齐的四字变量
- 对于分支目标,在 `.text` 段使用 `.balign 64` 指令按 64 字节缓存行对齐,示例代码如下:
.text
.balign 64
my_label:
; 分支目标代码
-
数据缓存优化
- 将经常一起访问的数据存储在相邻位置,例如结构体中相关成员的排列。
// C 语言示例,结构体成员按访问频率排列
struct my_struct {
int frequently_used;
int less_frequently_used;
};
- 使用 `.balign 64` 指令将数据对齐到 64 字节缓存块,提高缓存命中率。
.data
.balign 64
my_array: .int 1, 2, 3, 4 ; 按 64 字节对齐的数组
-
流水线优化
- 避免指令间的依赖关系,例如避免前一条指令写入寄存器,后一条指令立即读取该寄存器。
; 不好的示例,存在依赖关系
mov %rax, %rbx ; 写入 %rbx
add %rbx, %rcx ; 从 %rbx 读取
; 好的示例,减少依赖
mov %rax, %rdx ; 临时存储到 %rdx
add %rdx, %rcx ; 使用 %rdx
- 编写代码时尽量让处理器的各个部分保持忙碌,充分利用流水线。
-
指令缓存与分支预测优化
- 对于条件分支,尽量减少分支的不确定性,例如通过代码逻辑让分支结果更可预测。
- 对于间接跳转,提前将跳转目标地址放入寄存器,让处理器有足够时间进行预测。
; 提前设置跳转目标地址
mov $target_address, %rax
jmp *%rax ; 间接跳转
- 使用 `.balign` 指令将分支目标按缓存行对齐,提高指令缓存效率。
-
指令和寄存器选择优化
-
选择字节长度更短的指令,例如优先使用
movaps而非movapd。
-
选择字节长度更短的指令,例如优先使用
; 使用 movaps 指令
movaps %xmm0, %xmm1
- 合理选择寄存器,避免不必要的寄存器重用,提高流水线性能。
3. 总结
本文主要介绍了字符编码和汇编代码优化的相关知识。
在字符编码方面:
- Unicode 解决了传统代码页在处理多语言字符时的局限性,通过为每个字符分配 32 位代码点,实现了更广泛的字符表示。
- UTF - 8 作为一种常用的 Unicode 编码,具有与 ASCII 兼容的优点,同时能处理不同长度的字符,但查找字符时需要特殊处理。
- ASCII 虽然是基础的字符编码,但在不同系统中存在行尾编码和制表符表示等差异。
在汇编代码优化方面:
|优化方面|要点|
| ---- | ---- |
|对齐|关注数据地址与硬件存储方式匹配,使用
.balign
指令进行对齐|
|数据缓存|将相关数据存储在一起,使用
.balign 64
指令提高缓存命中率|
|流水线|减少指令依赖,让处理器各部分保持忙碌|
|指令缓存与分支预测|减少分支不确定性,提前设置跳转目标,使用
.balign
指令对齐分支目标|
|指令和寄存器选择|选择字节长度短的指令,合理选择寄存器|
通过掌握这些知识和优化方法,可以更好地处理字符编码问题,编写高效的汇编代码。在实际应用中,需要根据具体的场景和需求,综合运用这些优化技巧,以达到最佳的性能效果。
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始优化]):::startend --> B{选择优化方向}:::decision
B -->|对齐优化| C(确定对齐方式):::process
C --> D(使用.balign 指令):::process
D --> E(检查对齐效果):::process
B -->|数据缓存优化| F(整理数据布局):::process
F --> G(使用.balign 64 指令):::process
G --> H(测试缓存命中率):::process
B -->|流水线优化| I(减少指令依赖):::process
I --> J(保持处理器忙碌):::process
J --> K(分析流水线性能):::process
B -->|指令缓存与分支预测优化| L(减少分支不确定性):::process
L --> M(提前设置跳转目标):::process
M --> N(使用.balign 指令对齐分支目标):::process
N --> O(评估预测准确率):::process
B -->|指令和寄存器选择优化| P(选择合适指令):::process
P --> Q(合理选择寄存器):::process
Q --> R(测试代码性能):::process
E --> S([结束优化]):::startend
H --> S
K --> S
O --> S
R --> S
这个流程图展示了汇编代码优化的整体流程,根据不同的优化方向进行相应的操作,最终评估优化效果并结束优化过程。通过这些优化方法和流程,可以不断提高汇编代码的性能和效率。
超级会员免费看
2697

被折叠的 条评论
为什么被折叠?



