为什么Clang在AArch32架构下不为__builtin_popcountll使用vcnt指令?
问题描述
我们先看一段简单的测试代码:
unsigned f(unsigned long long x) { return __builtin_popcountll(x); }
使用clang --target=arm-none-linux-eabi -mfpu=neon -mfloat-abi=softfp -mcpu=cortex-a15 -Os编译该代码时⁎,编译器会生成多条指令,对x的高低字并行实现经典popcount算法后再相加得到结果。
我查阅架构手册后认为,类似下方代码生成的NEON代码就算没有性能提升,至少在代码体积上也更有优势:
#include <arm_neon.h> unsigned f(unsigned long long x) { uint8x8_t v = vcnt_u8(vcreate_u8(x)); return vget_lane_u64(vpaddl_u32(vpaddl_u16(vpaddl_u8(v))), 0); }
请问Clang†为何不采用这种实现?是我给出的编译选项有误吗?就算在Cortex-A15上,ARM到NEON再到ARM的寄存器转移开销也大到不值得这么做吗?还是因为目前绝大多数移动设备都采用AArch64架构,Clang对AArch32的代码生成维护投入不足?
⁎ 此处使用的Clang编译选项可能存在错误或冗余,可按需调整。
† 我测试发现GCC也不会采用该实现,只会输出对__popcountdi2的调用,这也可能是我调用方式有误。
回答
主要有四个核心原因:
- 寄存器转移开销抵消计算收益:Cortex-A15上通用寄存器到NEON寄存器的
VMOV指令本身就有2个周期的延迟,若NEON单元此前处于低功耗idle状态,还会产生额外的唤醒开销。纯通用寄存器实现的并行popcount在Cortex-A15上仅需5~6个周期即可完成,而NEON实现除了转移开销,还要执行vcnt、三次vpaddl再把结果移回通用寄存器,总延迟反而高于纯通用寄存器实现。 - 调用约定限制无法避免转移开销:你使用的
softfp浮点ABI要求所有NEON/浮点参数、返回值都必须通过通用寄存器传递,只要调用NEON指令就必然产生两次跨寄存器文件的转移,完全没有优化空间。如果切换为hardfpABI这个问题会稍有缓解,但softfp是多数嵌入式AArch32场景的默认配置,编译器需要保证该ABI下的性能不会出现退化。 - 实现要兼容全系列架构:编译器内置函数的实现需要覆盖所有支持NEON的ARMv7-A架构CPU,不能仅适配Cortex-A15。比如更早的Cortex-A8上,通用寄存器和NEON寄存器之间的转移延迟高达10个周期,用NEON实现单元素popcount性能会下降数倍,编译器不会为了新架构的小收益牺牲旧架构的兼容性。
- NEON popcount仅在批量场景有收益:NEON的优势是单指令多数据并行计算,只有在批量处理连续多组数据时,才能把跨寄存器转移的开销平摊到每个数据上,体现出性能优势。单64位元素的popcount属于极短路径计算,完全发挥不出NEON的并行优势,编译器的成本收益模型不会选择这种实现。
你的编译选项没有明显错误,如果需要强制使用NEON实现可以尝试切换-mfloat-abi=hard并开启-Ofast优化,但绝大多数版本的Clang和GCC都不会自动做这个转换,因为内置函数的实现模板中本身就没有为单元素popcount提供NEON版本的实现。
内容的提问来源于stack exchange,提问作者Alex Shpilkin
相关产品推荐
相关产品推荐

