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

Cortex-M4汇编代码运行慢于预期的原因排查求助

问题:Cortex-M4汇编代码性能预估与实测偏差及C版本更优原因

背景信息

我正在为STM32F4DISCOVERY开发板的STM32F407VG(Cortex-M4内核)编写对性能极度敏感的汇编代码,目标是压榨每一个时钟周期。我使用Cortex-M4内置的DWT周期计数器做基准测试,某输入规模下代码实际运行1494个周期。代码从Flash运行,CPU降频至24MHz以实现Flash零等待状态访问(ART加速器已禁用),两次连续读取DWT计数器仅产生1个周期的基准测试开销。

代码仅从Flash读取5个32位常量字(可能因同时读取指令和数据引发总线矩阵冲突),其余数据访问均在RAM中完成。我已确保所有分支目标32位对齐,并手动为部分指令添加.W后缀,仅余两条32位指令为16位而非32位对齐——其中一条在当前输入规模下不会执行,另一条是函数的最终POP指令,显然不在循环中运行。注:多数指令采用32位编码,平均指令长度为3.74字节。

我制作了电子表格统计每条指令的执行次数(循环内指令统计迭代次数),甚至考虑了分支是否命中(这会影响指令周期数)。参考《Cortex-M4技术参考手册(TRM)》获取指令周期数,且始终采用最保守估算:涉及流水线刷新开销的指令按最大3周期计算;所有加载和存储操作均按最坏情况计算,尽管TRM第3.3.2节提到诸多可减少周期数的特殊情况。电子表格包含两次读取DWT计数器之间所有指令的开销。

令我意外的是,电子表格预估代码应运行1268个周期,而实际为1494个周期,比理论最坏情况慢18%。即使完全展开负责约3/4执行时间的主循环,代码仍需1429个周期,而电子表格预估展开版本仅需1186个周期。

有趣的是,同算法的完全展开、精心调优的C版本仅需1309个周期。该C版本共有1013条指令,而汇编展开版本仅930条指令。两者均包含当前测试输入未触发的分支代码,但这部分代码对性能无显著影响。此外,C代码的平均指令长度仅略小,为3.59字节。

核心问题

  • 我的汇编代码实际性能与预估存在显著偏差的原因是什么?
  • 为何指令数更多、指令长度相近的C版本运行更快?

最小复现示例

主汇编函数

// #define UNROLL

    .cpu cortex-m4
    .arch armv7e-m
    .fpu softvfp
    .syntax unified
    .thumb

.macro MACRO r_0, r_1, r_2, d
    ldr       lr, [r0, #\d]
    and     \r_0,  \r_0, \r_1, ror #11
    and     \r_0,  \r_0, \r_1, ror #11
    and       lr,  \r_0,   lr, ror #11
    and       lr,  \r_0,   lr, ror #11
    and     \r_2,  \r_2,   lr, ror #11
    and     \r_2,  \r_2,   lr, ror #11
    and     \r_1,  \r_2, \r_1, ror #11
    and     \r_1,  \r_2, \r_1, ror #11
    str       lr, [r0, #\d]
.endm

    .text
    .p2align 2
    .global  f
f:
    push    {r4-r11,lr}
    ldmia    r0, {r1-r12}

    .p2align 2

#ifndef UNROLL
    mov     lr,   #25
    push.w  {lr}

loop:
#else
.rept 25
#endif
    MACRO          r1,  r2,  r3, 48
    MACRO          r4,  r5,  r6, 52
    MACRO          r7,  r8,  r9, 56
    MACRO         r10, r11, r12, 60
#ifndef UNROLL
    ldr     lr, [sp]
    subs    lr,   lr, #1
    str     lr, [sp]
    bne     loop

    add.w   sp,   sp, #4
#else
.endr
#endif

    stmia    r0, {r1-r12}
    pop     {r4-r11,pc}

主代码(需STM32F4 HAL)

#include "stm32f4xx_hal.h"

void SysTick_Handler(void) {
    HAL_IncTick();
}

void SystemClock_Config(void) {
    RCC_OscInitTypeDef RCC_OscInitStruct;
    RCC_ClkInitTypeDef RCC_ClkInitStruct;

    // Enable Power Control clock
    __HAL_RCC_PWR_CLK_ENABLE();

    // The voltage scaling allows optimizing the power consumption when the device is
    // clocked below the maximum system frequency, to update the voltage scaling value
    // regarding system frequency refer to product datasheet.
    __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE2);

    // Enable HSE Oscillator and activate PLL with HSE as source
    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
    RCC_OscInitStruct.HSEState = RCC_HSE_ON;      // External 8 MHz xtal on OSC_IN/OSC_OUT
    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;  // 8 MHz / 8 * 192 / 8 = 24 MHz
    RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
    RCC_OscInitStruct.PLL.PLLM = 8;              // VCO input clock = 1 MHz / PLLM = 1 MHz
    RCC_OscInitStruct.PLL.PLLN = 192;            // VCO output clock = VCO input clock * PLLN = 192 MHz
    RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV8;  // PLLCLK = VCO output clock / PLLP = 24 MHz
    RCC_OscInitStruct.PLL.PLLQ = 4;              // USB clock = VCO output clock / PLLQ = 48 MHz
    if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
        while (1)
            ;
    }

    // Select PLL as system clock source and configure the HCLK, PCLK1 and PCLK2 clocks dividers
    RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
    RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;  // 24 MHz
    RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;         // 24 MHz
    RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;          // 24 MHz
    RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;          // 24 MHz
    if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK) {
        while (1)
            ;
    }
}

void print_cycles(uint32_t cycles) {
    uint32_t q = 1000, t;

    for (int i = 0; i < 4; i++) {
        t = (cycles / q) % 10;
        ITM_SendChar('0' + t);
        q /= 10;
    }

    ITM_SendChar('\n');
}

void f(uint32_t *);

int main(void) {
    uint32_t x[16];

    SystemClock_Config();

    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;

    uint32_t before, after;

    while (1) {
        __disable_irq();
        before = DWT->CYCCNT;

        f(x);

        after = DWT->CYCCNT;
        __enable_irq();

        print_cycles(after - before);

        HAL_Delay(1000);
    }
}

运行说明

  • 将上述代码导入含STM32F4 HAL的项目,需添加全局宏定义HSE_VALUE=8000000(HAL默认25MHz晶振,开发板实际为8MHz)。
  • 通过注释/取消注释代码开头的#define UNROLL可切换展开/非展开版本。

DWT计数器调用点验证

对main()函数执行arm-none-eabi-objdump查看调用点:

80009da:       4668            mov     r0, sp
        before = DWT->CYCCNT;
 80009dc:       6865            ldr     r5, [r4, #4]
        f(x);
 80009de:       f7ff fbd3       bl      8000188 <f>

        after = DWT->CYCCNT;
 80009e2:       6860            ldr     r0, [r4, #4]

可见两次读取DWT计数器之间仅存在跳转至f()函数的bl指令。

实测与预估数据

  • 非展开版本实测运行1536个周期,展开版本为1356个周期。

非展开版本预估电子表格(未计入1个周期DWT开销)

指令循环迭代次数宏重复次数单宏内指令数周期数总周期数
bl (来自main)11144
push (12个寄存器)1111313
ldmia (12个寄存器)1111313
mov11111
push (1个寄存器)11122
ldr25412200
and25481800
str25412200
ldr11122
subs11111
str11122
bne (命中)2411496
bne (未命中)11111
stmia (12个寄存器)1111313
pop (11个寄存器 + pc)1111616
1364
  • 非展开版本误差:1536/(1364 + 1) - 1 = 12.5%(+1为DWT计数器开销)。
  • 展开版本预估:移除循环初始化及分支相关指令,总计减少105个周期,预估执行时间为1259个周期。
  • 展开版本误差:1356/(1259 + 1) - 1 = 7.6%。

内容的提问来源于stack exchange,提问作者swineone

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 22:57:00