You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为什么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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.25 13:24:03