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

请求排查:仅CPU代码性能随可执行文件名变化的问题

Apple Silicon上同名二进制因文件名不同出现性能差异问题

问题重现

在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万次,差异仍存在。

背景

我有一个生成多个不同文件名可执行文件的项目,用于基准测试相同代码的不同输入,但因该问题,相同代码仅因文件名不同产生不同周期计数。且排除了加密代码非恒定时间的可能,因字节级副本也存在差异。

问题

  1. 该性能差异的可能原因是什么?
  2. 如何消除该差异,使结果不受文件名影响?

假设

推测差异与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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 15:29:53