比 C 快 n 倍,Arm 版·Luna 的博客

这是一篇关于在 M1 Pro 上使用不同方法优化代码性能的博客文章,主要内容如下:

  • 基本方法:C 语言实现的基本算法,通过循环遍历字节数组并根据字符进行计数操作,与 Owen 的 310MB/s 相比,作者得到 305MB/s 的速度,对应的 A64 汇编与 x64 指令几乎等效,但 Clang 使用 movadd 寄存器而不是立即数 add 让作者困惑。
  • 表驱动方法:通过将分支替换为表查找来优化紧密循环,性能提升 9 倍至 2.712GB/s,但与 Owen 的 4GB/s 相比仍有差距。使用 -falign-loops=64 指令后性能大致保持在 2.695GB/s,分析发现函数 90.8%的时间用于从输入加载字节,为提高速度需一次加载多个字节。
  • 作弊事件 #1 - 显式长度:输入地址空间后接未映射区域,一次加载多个字节可能导致段错误,可直接传入输入长度而不依赖空终止符来处理“剩余字节”(“slack”),实现新的实现方式后性能提升 15 倍至 4.610GB/s。
  • 按块处理输入:将输入指针从 uint8_t * 转换为 uint64_t * 以一次加载 8 个字节,处理完后再处理剩余字节(“slack”),性能提升至 6.358GB/s,Clang 消除了 memcpy 和循环,生成了高效但冗长的汇编代码。
  • 使用 128 位整数减少加载次数:AArch64 提供 32 个 128 位寄存器,可用于优化代码,将 table_8 中的数据类型修改为 __uint128_t 后性能提升至 7.090GB/s,尽管 128 位寄存器未实际使用,但 Clang 能有效优化代码。
  • 新方法:Neon:介绍 Arm 的 Neon 指令集,可用于单指令多数据操作,通过 Neon 实现算法,性能提升至 22.519GB/s,比原始方法快 73 倍,但进一步分析发现函数 81.6%的时间用于 vaddlvq_s8 指令,可通过减少向量减少操作来进一步优化。
  • 减少减少操作次数:不每次循环都减少向量,而是使用向量累加器变量在末尾减少,性能再次提升至 33.952GB/s,比原始方法快 111 倍,但 Thomas Ip 的代码达到 48.522GB/s,作者对其代码进行重构和优化后达到 21.171GB/s,通过调整循环迭代次数可再次提升性能至 49.806GB/s,比原始方法快 163 倍,尝试应用 least-significant bit 策略后性能达到 87.017GB/s 或 87.138GB/s。
  • 总结:优化代码时应先尝试让编译器自动向量化代码,使用 SIMD intrinsics 前要检查生成的汇编代码,必要时可使用原始汇编代码,同时要注意编译器的优化行为和性能计数器的准确性。
阅读 23
0 条评论