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