优化面向区间内输入的std::clamp:保留cmov而非分支是否有意义?
C++17 std::clamp 整数场景下的优化疑问
C++17的std::clamp是一个模板函数,用于确保输入值不小于给定最小值且不大于给定最大值:若输入值在区间内则返回输入值,否则对应返回最小值或最大值。
本次优化的前提假设
- 类型参数为32位或64位整数
- 输入值大概率已经处于区间内,即大概率直接返回输入值
- 输入值通常是调用前不久计算所得,最小值和最大值通常提前已知
- 忽略引用相关逻辑:引用会增加优化复杂度,且在整数场景下无实际使用价值
基础实现的编译结果
无论是标准库实现还是朴素手写实现,gcc和clang生成的汇编代码都没有适配上述「输入大概率在区间内」的假设,以下是两个基础实现:
#include <algorithm> int clamp1(int v, int minv, int maxv) { return std::clamp(v, minv, maxv); } int clamp2(int v, int minv, int maxv) { if (maxv < v) { return maxv ; } if (v < minv) { return minv; } return v; }
上述代码编译后都会生成两条cmov指令,对应汇编如下:
mov eax, esi cmp edi, esi cmovge eax, edi cmp edx, edi cmovl eax, edi
加入分支预测提示后的编译差异
尝试使用__builtin_expect告知编译器优先适配区间内场景(gcc目前似乎会忽略C++20的[[likely]]注解),实现如下:
int clamp3(int v, int minv, int maxv) { if (__builtin_expect(maxv < v, 0)) { return maxv; } if (__builtin_expect(v < minv, 0)) { return minv; } return v; }
此时gcc和clang的编译结果出现差异:gcc仍然生成两条cmov指令完全避免分支,clang则只生成1条条件跳转而非预期的2条,clang生成的汇编如下(注解为手动添加):
clamp3(int, int, int): mov eax, edx ; result = maxv cmp edi, esi ; v, minv cmovge esi, edi ; if (v >= minv) minv = v cmp edx, edi ; maxv, v jl .LBB0_2 ; if (maxv < v) goto LBB0_2 mov eax, esi ; result = minv (无截断时该值已被更新为v) .LBB0_2: ret
待解答的核心问题
- 预期每次都走相同路径的条件跳转是否存在明显劣势,导致gcc选择避免使用分支?
- clang生成的单条件跳转版本,是否比双跳转版本性能更优?
Intel官方优化手册的相关参考
2021年6月版《Intel® 64 and IA-32 架构优化参考手册》第3-5页建议不要随意使用cmov:
汇编/编译器编码规则2(影响程度中,通用性中高) 尽可能使用SETCC和CMOV指令消除不可预测的条件分支,但不要对可预测分支做该转换。也不要用这些指令消除所有不可预测条件分支:这类指令需要执行条件分支的两条路径,会产生执行开销。此外将条件分支转换为SETCC或CMOV,本质是用数据依赖替换控制流依赖,会限制乱序执行引擎的能力。调优时请注意,所有Intel 64和IA-32处理器通常都有极高的分支预测准确率,持续预测错误的分支非常罕见。仅当计算时间的增量小于分支预测错误的预期开销时,才可以使用这类指令。
内容的提问来源于stack exchange,提问作者Alex Guteniev
相关产品推荐
相关产品推荐

