为何Clang对filter_A和filter_B生成的短路求值代码不同?
函数短路求值差异的原因分析
原始C代码
struct Customer {CityName city; // city name FavcName favorite; // favorite category of goods int age; // age int gender; // 1 = M; 2 = F; 3 = genderfluid; bool married; // true = married int reg; // registration date (unixtime) int lastdeal; // last deal date (unixtime) float total; }; // total spended (UAH) bool __cdecl filter_A( char city[24], char favorite[24], int age, int gender, bool married, int reg, int lastdeal, float total) { return strcmp(favorite,"Tools") == 0 && age <= 25; } bool __cdecl filter_B(Customer c) { return strcmp(c.favorite,"Tools") == 0 && c.age <= 25; }
Clang -O1优化后的LLVM IR代码(已修正名称修饰)
define dso_local noundef zeroext i1 @filter_A(ptr %0, ptr %1, i32 %2, i32 %3, i1 %4, i32 %5, i32 %6, float %7) { %9 = tail call i32 @strcmp(ptr %1, ptr @"Tools") %10 = icmp eq i32 %9, 0 %11 = icmp slt i32 %2, 26 %12 = and i1 %11, %10 ret i1 %12 } define dso_local noundef zeroext i1 @filter_B(ptr %0) { %2 = getelementptr inbounds %struct.Customer, ptr %0, i64 0, i32 1 %3 = tail call i32 @strcmp(ptr %2, ptr @"Tools") %4 = icmp eq i32 %3, 0 br i1 %4, label %5, label %9 5: ; preds = %1 %6 = getelementptr inbounds %struct.Customer, ptr %0, i64 0, i32 2 %7 = load i32, ptr %6, align 4, !tbaa !4 %8 = icmp slt i32 %7, 26 br label %9 9: ; preds = %5, %1 %10 = phi i1 [ false, %1 ], [ %8, %5 ] ret i1 %10 }
差异原因分析
两个函数逻辑表达式一致,但编译后出现短路求值差异,核心在于条件判断中操作数的获取成本与风险不同:
- 对于
filter_A,age是直接传入的整型参数,获取它不需要额外内存加载操作,无任何副作用且成本极低。Clang在-O1优化下会直接计算两个条件,再用逻辑与合并结果——多算这一步没有额外开销,还能避免分支跳转带来的性能损耗。 - 对于
filter_B,c.age是结构体成员,需要先通过指针偏移定位内存地址,再执行load操作读取数据。如果strcmp的结果已经为false(用户不关注Tools品类),完全没必要执行这次内存加载:既节省了一次内存访问的开销,也能避免潜在的内存访问风险(比如结构体指针非法的情况,编译器会做保守优化)。此时分支跳转的开销远小于不必要的内存加载,所以Clang选择了短路求值策略,先判断strcmp结果,再决定是否加载age。
简单来说:filter_A里的age是现成的,计算无额外成本;filter_B里的age需要读内存,能省则省,因此编译器采用了不同的优化方案。
内容的提问来源于stack exchange,提问作者Валентин Разносилин
相关产品推荐
相关产品推荐

