GCC为何生成减1后与2比较的汇编?2的幂次cmp指令更快吗?
问题背景
我在编写将屏幕清空为指定颜色的代码,对应的C++实现如下:
void clear_screen(unsigned int color, void *memory, int height, int width) { unsigned int *pixel = (unsigned int *)memory; for (auto y = 0; y < height; y++) for (auto x = 0; x < width; x++) *pixel++ = color; }
我使用g++编译、objconv工具生成了对应的汇编代码,以下是生成结果,我也为部分我理解的指令添加了注释:
renderer_clear_screen: push r13 push r12 push rbp push rdi push rsi push rbx mov r11d, ecx ; 将color存入r11d mov ebx, r8d ; 将height存入ebx mov rcx, rdx ; 内存指针存入rcx test r8d, r8d ; jle _cls_return ; 宽或高为0时直接返回 test r9d, r9d ; (比如窗口最小化的情况) jle _cls_return ; mov r8d, r9d mov esi, r9d ; esi = width mov edi, r9d ; edi = width xor r10d, r10d ; r10d = 0,外层循环计数器 shr esi, 2 movd xmm1, r11d ; 将color低32位存入xmm1 lea r12d, [r9-1] ; r12d = width - 1 shl rsi, 4 mov ebp, r8d shl rdi, 2 pshufd xmm0, xmm1, 0 ; 将xmm1中的32位颜色值复制4份填满xmm0 shl rbp, 2 ALIGN 8 ?_001: cmp r12d, 2 jbe ?_006 ; 若width - 1 <= 2,跳转到标量处理分支 mov rax, rcx lea rdx, [rcx+rsi] ALIGN 8 ?_002: movups oword [rax], xmm0 ; 一次写16字节(4个像素) add rax, 16 cmp rdx, rax jnz ?_002 ; 循环写完当前行所有整16字节块 lea rdx, [rcx+rbp] mov eax, r8d cmp r9d, r8d jz ?_004 ?_003: lea r13d, [rax+1H] mov dword [rdx], r11d ; 写1个剩余像素 cmp r13d, r9d jge ?_004 add eax, 2 mov dword [rdx+4H], r11d ; 写第2个剩余像素 cmp r9d, eax jle ?_004 mov dword [rdx+8H], r11d ; 写第3个剩余像素 ?_004: add r10d, 1 add rcx, rdi ; 指针移动到下一行起始位置 cmp ebx, r10d jnz ?_001 ; 外层循环,处理下一行 _cls_return: pop rbx pop rsi pop rdi pop rbp pop r12 pop r13 ; 恢复所有保存的寄存器 ret ; 返回 ?_006: mov rdx, rcx xor eax, eax jmp ?_003 ; 直接进入逐像素写逻辑
疑问
可以看到在?_001标签处,编译器将width - 1与2做比较,该逻辑等价于直接将width与3比较。我的疑问是:开启-O3优化等级时,编译器为什么选择和2比较,还额外消耗一条lea指令将width - 1存入r12d?
我目前猜测的原因是汇编中cmp指令对比2的幂次时执行速度更快,还是说这只是编译器的实现特性?
解答
这和「cmp对比2的幂次执行速度更快」没有任何关系,纯粹是GCC开启O3优化做循环向量化拆分时的通用代码生成逻辑,不存在你以为的额外性能损耗:
- 这条
lea r12d, [r9-1]不是额外冗余指令。编译器生成16字节XMM向量批量写逻辑时,本来就要计算「行宽无法被向量长度整除时的剩余像素边界」,width-1是后续标量收尾分支本来就要用到的值,提前算好存入被调用者保存的r12寄存器,是为了避免外层y循环每轮迭代都重复计算这个值,反而是优化手段。 - 比较
width-1 <= 2和width <=3虽然逻辑等价,但前者是编译器做循环拆分的通用判断模板:这里XMM寄存器一次可以写入4个4字节像素,只要行宽小于4,就直接跳转到逐像素处理的标量分支,完全不走SIMD批量写循环。用n-1和阈值比较是编译器判断数值上界的惯用生成模式,不需要额外调整标志位判断逻辑,和直接比较3相比,分支预测效率、指令执行速度没有任何区别。 - 你可以观察后面的
?_003标量处理分支,本身就是按写1个、写2个、写3个的顺序做分支跳转,刚好覆盖行宽为1、2、3的所有情况,不需要额外处理边界,这个判断和后续收尾逻辑是完全配套的,不是孤立生成的代码。 - x86架构下
cmp指令对所有普通立即数的比较,延迟和吞吐量完全一致,不存在比较2比比较3更快的说法。
内容的提问来源于stack exchange,提问作者avighnac
相关产品推荐
相关产品推荐

