请求排查:仅CPU代码性能随可执行文件名变化的问题
问题重现
在Apple Silicon Mac(测试过2020款M1 MacBook Air和16英寸M3 Max MacBook Pro,均运行macOS Sonoma,使用Apple clang 15.0)上运行以下shell脚本:
#!/bin/bash rm -rf NTRU git clone https://github.com/vector-polymul-ntru-ntrup/NTRU.git cd NTRU/ntruhps2048677/aarch64_tmvp cat << EOF > speed_polymul.c #include <stddef.h> #include <stdint.h> #include <stdlib.h> #include <stdio.h> #include "api.h" #include "params.h" #include "poly.h" #include "tmvp.h" #include "batch_multiplication.h" #define NTESTS 1000 uint64_t time0, time1; uint64_t cycles[NTESTS]; #ifdef __APPLE__ #include "m1cycles.h" #define __AVERAGE__ #define SETUP_COUNTER() {(void)cycles; setup_rdtsc();} #define CYCLE_TYPE "%lld" #define GET_TIME rdtsc() #else #include "hal.h" #undef __AVERAGE__ #define __MEDIAN__ #define SETUP_COUNTER() {} #define CYCLE_TYPE "%ld" #define GET_TIME hal_get_time() static int cmp_uint64(const void *a, const void *b){ return ((*((const uint64_t*)a)) - ((*((const uint64_t*)b)))); } #endif #ifdef __AVERAGE__ #define LOOP_INIT(__clock0, __clock1) { __clock0 = GET_TIME; } #define LOOP_TAIL(__f_string, records, __clock0, __clock1) { __clock1 = GET_TIME; printf(__f_string, (__clock1 - __clock0) / NTESTS); } #define BODY_INIT(__clock0, __clock1) {} #define BODY_TAIL(records, __clock0, __clock1) {} #elif defined(__MEDIAN__) #define LOOP_INIT(__clock0, __clock1) {} #define LOOP_TAIL(__f_string, records, __clock0, __clock1) { qsort(records, sizeof(uint64_t), NTESTS, cmp_uint64); printf(__f_string, records[NTESTS >> 1]); } #define BODY_INIT(__clock0, __clock1) { __clock0 = GET_TIME; } #define BODY_TAIL(records, __clock0, __clock1) { __clock1 = GET_TIME; records[i] = __clock1 - __clock0; } #endif #define WRAP_FUNC(__f_string, records, __clock0, __clock1, func){ LOOP_INIT(__clock0, __clock1); for(size_t i = 0; i < NTESTS; i++){ BODY_INIT(__clock0, __clock1); func; BODY_TAIL(records, __clock0, __clock1); } LOOP_TAIL(__f_string, records, __clock0, __clock1); } poly src1, src2; poly des; int main(int argc, char *argv[]) { int iters = 1; SETUP_COUNTER(); if (argc > 1) { iters = atoi(argv[1]); } if (iters == 1) { WRAP_FUNC("polymul (Z_q[x] / (x^677 - 1)): " CYCLE_TYPE " \n", cycles, time0, time1, poly_Rq_mul(&(des), &(src1), &(src2))); } return 0; } EOF make -j16 speed_polymul cp speed_polymul abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ echo Type your password for sudo now sudo echo Password OK sudo ./speed_polymul > /dev/null # First run is slower (possibly has to do with verifying code signature) sudo ./abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ > /dev/null # First run is slower (possibly has to do with verifying code signature) echo Running speed_polymul 5 times sudo ./speed_polymul sudo ./speed_polymul sudo ./speed_polymul sudo ./speed_polymul sudo ./speed_polymul echo Running abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ 5 times sudo ./abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ sudo ./abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ sudo ./abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ sudo ./abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ sudo ./abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
注:需要sudo权限,因Apple Silicon CPU的周期计数器仅root可访问
该脚本执行以下操作:
- 克隆包含ARMv8优化加密代码及CPU周期计数基准测试代码的仓库;
- 修改基准测试代码以触发问题:仅保留单个函数、将main()部分局部变量移至全局作用域、添加命令行参数逻辑(未实际使用但需存在以触发问题);
- 编译生成speed_polymul可执行文件;
- 创建该文件的字节级副本,命名为abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ;
- 分别运行两个可执行文件各5次。
测试结果
输出显示两个可执行文件的性能存在约5%的明显差异,且完全可复现:
Running speed_polymul 5 times polymul (Z_q[x] / (x^677 - 1)): 6881 polymul (Z_q[x] / (x^677 - 1)): 6916 polymul (Z_q[x] / (x^677 - 1)): 6864 polymul (Z_q[x] / (x^677 - 1)): 6883 polymul (Z_q[x] / (x^677 - 1)): 6881 Running abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ 5 times polymul (Z_q[x] / (x^677 - 1)): 7179 polymul (Z_q[x] / (x^677 - 1)): 7186 polymul (Z_q[x] / (x^677 - 1)): 7177 polymul (Z_q[x] / (x^677 - 1)): 7195 polymul (Z_q[x] / (x^677 - 1)): 7179
该代码为纯CPU代码,无任何I/O操作,且即使增加迭代次数至10万次,差异仍存在。
背景
我有一个生成多个不同文件名可执行文件的项目,用于基准测试相同代码的不同输入,但因该问题,相同代码仅因文件名不同产生不同周期计数。且排除了加密代码非恒定时间的可能,因字节级副本也存在差异。
问题
- 该性能差异的可能原因是什么?
- 如何消除该差异,使结果不受文件名影响?
假设
推测差异与main()中argv[0]指向的缓冲区大小有关,可能触发变量对齐问题,但尚未明确具体机制。已确认argv[0]地址会变化,其他变量地址模256后无差异,但不清楚为何影响基准测试函数或周期计数代码。
回答
1. 性能差异的可能原因
核心原因是macOS的动态链接器dyld加载可执行文件时,会根据文件名调整进程内存布局,进而影响代码或数据的缓存命中/对齐状态:
- dyld初始化进程时,会将
argv[0]对应的字符串存入进程内存,不同长度的文件名会占用不同内存空间,导致后续全局变量、栈帧甚至代码段的加载偏移出现细微变化。 - Apple Silicon的ARMv8架构对内存对齐和缓存行布局极为敏感,哪怕几个字节的偏移,都可能让关键数据或代码跨越缓存行边界,触发额外的缓存miss,最终体现为5%左右的性能差异。
- 另外,macOS的代码签名验证和ASLR(地址空间布局随机化)机制,虽已做字节级副本,但文件名作为进程标识的一部分,可能影响dyld的内存加载决策,间接改变内存布局。
2. 消除差异的解决方案
方案一:统一argv[0]内容
在main函数开头强制覆盖argv[0]为固定长度的字符串,消除文件名带来的内存布局差异:
int main(int argc, char *argv[]) { static char fixed_argv0[] = "fixed_test_name"; argv[0] = fixed_argv0; // 原有代码逻辑 }
方案二:将全局变量改为局部变量
当前全局变量src1、src2、des的地址受进程初始内存布局影响较大,改为main函数内的局部变量后,栈帧布局更稳定,且栈内存对齐通常不受文件名这类外部因素干扰:
int main(int argc, char *argv[]) { poly src1, src2; poly des; // 原有代码逻辑 }
方案三:禁用ASLR(仅测试场景)
运行可执行文件时,通过环境变量禁用位置无关可执行文件的ASLR特性,强制代码和数据加载到固定地址:
sudo DYLD_NO_PIE=1 ./speed_polymul sudo DYLD_NO_PIE=1 ./abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
此方法能彻底消除内存布局随机化影响,但会降低安全性,仅适合基准测试。
方案四:优化基准测试流程
增加预热迭代次数,对结果多次采样后取平均或中位数;同时通过powermetrics工具锁定CPU到最高频率,减少系统调度带来的干扰。
内容的提问来源于stack exchange,提问作者swineone

