C语言switch表格跳转的汇编实现
最近在看 https://github.com/skywind3000/FastMemcpy/ 这个超快的memcpy实现,里面有段代码让我想到了一个困惑很久的问题,就是据说switch如果分支很多的话,那么会实现表格跳转实现来生成代码,但是我始终不太清楚这个汇编实现是什么样子的。今天看了一下这段汇编代码,好像也不是特别复杂。
先说一下这个memcpy逻辑:对于拷贝字节数小于等于128的话,会使用switch实现来做判断。相关代码在这个地方 https://github.com/skywind3000/FastMemcpy/blob/8fea5f666be174c6548d0ae4010e81b0a742c853/FastMemcpy.h#L87
static INLINE void *memcpy_tiny(void *dst, const void *src, size_t size) { unsigned char *dd = ((unsigned char*)dst) + size; const unsigned char *ss = ((const unsigned char*)src) + size; switch (size) { case 64: memcpy_sse2_64(dd - 64, ss - 64); case 0: break; ... } static void* memcpy_fast(void *destination, const void *source, size_t size) { unsigned char *dst = (unsigned char*)destination; const unsigned char *src = (const unsigned char*)source; static size_t cachesize = 0x200000; // L2-cache size size_t padding; // small memory copy if (size <= 128) { return memcpy_tiny(dst, src, size); } ... }
我分别在Linux和MacOSX(实际是clang)上使用 `gcc -O3 FastMemcpy.c -S` 查看生成的汇编代码。因为这个函数是inline的,并且放在了 memcpy_fast 函数最开头,所以很容易就可以找到对应的汇编代码部分。下面是Linux上的汇编代码以及相关注释,直接使用 `jmp *.L5(,%rdx,8)` 这样的跳转指令,其中rdx是拷贝字节数
memcpy_fast:
.LFB549:
.file 1 "FastMemcpy.h"
.loc 1 581 0
.cfi_startproc
.LVL0:
.loc 1 588 0
cmpq $128, %rdx # rdx是拷贝长度,首先和128对比
.loc 1 581 0
movq %rdi, %rax # rdi是目标地址,放到了rax里面
.loc 1 588 0
ja .L2 # 进入>128情况下的代码
.LVL1:
.LBB5571:
.LBB5572:
.loc 1 88 0
leaq (%rdi,%rdx), %rcx # rcx = rdi + rdx,就是目标结束地址
.LVL2:
.loc 1 89 0
addq %rdx, %rsi # rsi = rsi + rdx,就是源结束地址
.LVL3:
.loc 1 91 0
jmp *.L5(,%rdx,8) # .L5就是跳转表格起始地址,根据字节数(rdx)跳转到对应的例程,每个地址占用8个字节
.section .rodata
.align 8
.align 4
.L5:
.quad .L3 # rdx = 0的情况
.quad .L4 # rdx = 1的情况
...
.L4: # 这里只贴出rdx=1的情况
.LBE7296:
.LBE7295:
.LBE7280:
.LBE7279:
.loc 1 100 0
movzbl -1(%rsi), %edx # rsi是源结束地址,取最后一个字节扩展到edx上
.LVL1567:
movb %dl, -1(%rcx) # 取ebx最低1字节dl,拷贝到rcx-1上,rcx是结束地址
ret
MacOSX上的汇编代码稍微复杂一些,跳转的时候多了3条指令,但是占用表格空间减少了128 * 4 = 512字节。最终也是 `jmpq *%rax` 这样的跳转指令。
_memcpy_fast: ## @memcpy_fast
Lfunc_begin3:
.loc 5 581 0 ## ./FastMemcpy.h:581:0
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
Ltmp54:
cmpq $128, %rdx # rdx是拷贝长度,和128进行标记
Ltmp55:
ja LBB3_129 # 如果>128的话,那么跳转到对应地址
Ltmp56:
leaq -1(%rdx), %rax # rax = rdx - 1
cmpq $127, %rax
ja LBB3_144 # 如果rax > 127的话,那么跳转到这里,但是我觉得好像不行,因为rdx <=128, rax最大值是127
Ltmp57:
addq %rdx, %rdi # rdi = rdi + rdx, 表示目标结束地址
Ltmp58:
addq %rdx, %rsi # rsi = rsi + rdx, 表示源结束地址
Ltmp59:
leaq LJTI3_1(%rip), %rcx # LJTI3_1是表格地址地址,rcx就是表示绝对地址
movslq (%rcx,%rax,4), %rax # rax = rcx + 4 * rax. 注意这个rax不是绝对地址,而是相对rcx偏移上的值
addq %rcx, %rax # rax = rax + rcx 此时rax才是绝对地址
jmpq *%rax # 进行地址跳转
...
.set L3_1_set_4, LBB3_4-LJTI3_1 # 偏移量
.set L3_1_set_6, LBB3_6-LJTI3_1
.set L3_1_set_8, LBB3_8-LJTI3_1
.set L3_1_set_10, LBB3_10-LJTI3_1
.set L3_1_set_12, LBB3_12-LJTI3_1
...
LJTI3_1: # 表格起始地址
.long L3_1_set_4 # 实际地址相对表格起始地址偏移,每个偏移量占用4个字节
.long L3_1_set_6
.long L3_1_set_8
....
LBB3_4: # 进行1字节copy的代码
movb -1(%rsi), %al
movb %al, -1(%rdi)
popq %rbp
retq