swi*_*one 10 performance assembly arm cortex-m
我正在为 Cortex-M4 编写一些汇编代码,特别是 STM32F4DISCOVERY 套件中的 STM32F407VG。
该代码对性能极其敏感,因此我希望从中挤出最后一个周期。我对其进行了基准测试(使用 Cortex-M4 中提供的 DWT 周期计数器),对于特定大小的输入,它以 1494 个周期运行。代码从闪存运行,CPU 降频至 24 MHz,以确保对闪存进行真正的零等待状态访问(禁用 ART 加速器)。对 DWT 周期计数器的两次连续读取进行基准测试会产生一个周期,因此这是与基准测试相关的唯一开销。
该代码仅从闪存读取 5 个常量 32 位字(如果从闪存读取指令和数据,可能会导致总线矩阵争用);所有其他数据存储器访问都是从 RAM 进行或到 RAM 进行的。我确保所有分支目标都是 32 位对齐的,并手动.W向某些指令添加后缀以消除所有指令,除了两个 16 位但不是 32 位对齐的 32 位指令——其中之一没有即使运行这个输入大小,第二条是POP函数的最终指令,它显然不是在循环中运行。请注意,大多数指令使用 32 位编码:实际上,平均指令长度为 3.74 字节。
我还制作了一个电子表格,记录了代码中的每一条指令,它们在循环内运行了多少次,甚至还记录了每个分支是否被采用,因为这会影响给定指令需要的周期数。我阅读了Cortex-M4 技术参考手册(TRM) 来获取每条指令的周期计数,并始终使用最保守的估计:当一条指令取决于管道刷新的成本时,我假设它最多需要 3 个周期;此外,我假设了所有加载和存储的最坏情况,尽管 TRM 第 3.3.2 节中讨论的许多特殊情况实际上可能会减少这些计数。我的电子表格包含 DWT 周期计数器两次读取之间每条指令的成本。
因此,我非常惊讶地发现我的电子表格预测代码应该在 1268 个周期内运行(回想一下实际性能是 1494 个周期)。我无法解释为什么代码的运行速度比根据指令时序假定的最坏情况慢 18%。即使完全展开代码的主循环(应负责大约 3/4 的执行时间)也只能将其减少到 1429 个周期,并且快速调整电子表格表明此展开的版本应在 1186 个周期内运行。
有趣的是,同一算法的完全展开、仔细调整的 C 版本运行了 1309 个周期。它总共有 1013 条指令,而我的汇编代码的完全展开版本有 930 条指令。在这两种情况下,都有一些代码处理未由用于基准测试的特定输入执行的情况,但就这些未使用的代码而言,C 版本和汇编版本之间应该没有显着差异。最后,C 代码的平均指令长度并没有明显变小:3.59 个周期。
那么:什么可能导致我的汇编代码中的预测性能和实际性能之间存在这种不小的差异?尽管有更多数量的指令和类似(稍小一些,但相差不大)的 16 位和 32 位指令混合,C 版本可以做什么才能运行得更快?
根据要求,这是一个适当匿名的最小可重现示例。因为我隔离了一段代码,所以对于非展开版本,预测和实际测量之间的误差下降到 12.5%(对于展开版本甚至更小:7.6%),但我仍然认为这个有点高,尤其是考虑到核心的简单性和最坏情况时序的使用,非展开版本。
一、主要组装功能:
// #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}
Run Code Online (Sandbox Code Playgroud)
这是主要代码(需要 STM32F4 HAL,通过 SWO 输出数据,可以使用 ST-Link 实用程序或 st-trace 实用程序从此处通过命令行读取数据st-trace -c24):
#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);
}
}
Run Code Online (Sandbox Code Playgroud)
我相信这足以转储到包含 STM32F4 HAL 的项目中并运行代码。该项目需要添加一个全局#definefor HSE_VALUE=8000000,因为 HAL 假定使用 25 MHz 晶体,而不是实际安装在板上的 8 MHz 晶体。
#define UNROLL通过在代码开头注释/取消注释,可以在展开版本和非展开版本之间进行选择。
运行arm-none-eabi-objdump该main()函数并查看调用站点:
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]
Run Code Online (Sandbox Code Playgroud)
因此,DWT 周期计数器的两次读取之间的唯一指令是bl分支到f()汇编函数的指令。
非展开版本运行 1536 个周期,而展开版本运行 1356 个周期。
这是我的非展开版本的电子表格(不考虑已测量的读取 DWT 周期计数器的 1 周期开销):
| 操作说明 | 循环迭代 | 宏重复 | 数数 | 周期盘点 | 总循环次数 |
|---|---|---|---|---|---|
| bl(来自主程序) | 1 | 1 | 1 | 4 | 4 |
| 推(12条) | 1 | 1 | 1 | 13 | 13 |
| ldmia(12 条规则) | 1 | 1 | 1 | 13 | 13 |
| 移动 | 1 | 1 | 1 | 1 | 1 |
| 推(1 条) | 1 | 1 | 1 | 2 | 2 |
| LDR | 25 | 4 | 1 | 2 | 200 |
| 和 | 25 | 4 | 8 | 1 | 800 |
| 斯特 | 25 | 4 | 1 | 2 | 200 |
| LDR | 1 | 1 | 1 | 2 | 2 |
| 潜艇 | 1 | 1 | 1 | 1 | 1 |
| 斯特 | 1 | 1 | 1 | 2 | 2 |
| bne(已拍摄) | 24 | 1 | 1 | 4 | 96 |
| bne(未采取) | 1 | 1 | 1 | 1 | 1 |
| stmia(12 项规定) | 1 | 1 | 1 | 13 | 13 |
| 流行音乐(11 条规则 + 个人电脑) | 1 | 1 | 1 | 16 | 16 |
| 第1364章 |
最后一列只是表中第 2 到第 5 列的乘积,最后一行是“总计”列中所有值的总和。这是预测的执行时间。
因此,对于非展开版本:1536/(1364 + 1) - 1 = 12.5% 误差(+ 1 项是为了考虑 DWT 循环计数器开销)。
对于展开版本,必须从上表中删除一些指令:循环设置(mov和push (1 reg))以及循环增量和分支(ldr、subs、str和bne,均采用和不采用)。计算得出为 105 个周期,因此预测性能将为 1259 个周期。
对于展开版本,我们的误差为 1356/(1259 + 1) - 1 = 7.6%。
您正在根据文档中的指令时序对总体时序做出假设。该处理器已经很长时间没有驱动性能了。
您在测试中进行了内存访问。2a) 您在测试中进行了对齐和未对齐的内存访问
很确定 ART 已打开,我已尝试多次将其关闭。也许这是一个 cortex-m7,我至少可以通过它关闭或其他什么,不记得了。需要从 sram 而不是 flash 运行。
零等待状态并不意味着零等待状态闪存通常是每个时钟几个时钟(如果不是更多的话)(零额外等待状态)。在 STM32 部件上很难甚至不可能确定。ti和其他没有这种闪存缓存(ART)性能的人更容易看到。
和其他东西。
我不知道与正常拇指指令相关的 nops 和强制拇指2 扩展是什么意思。这些nop在哪里?
顺便说一句,工作非常出色,我不会以任何方式否认这一点。只是想添加一些额外的信息,我无法判断您是否计时,因为您的测试肯定涉及系统计时问题并且超出了指令计时。
所以ARM ARM和ARM TRM适用于cortex-m4。
从代码存储空间 0x00000000 到 0x1FFFFFFC 的指令读取是通过 32 位 AHB-Lite 总线执行的。
所有提取都是字范围的。每个字获取的指令数量取决于正在运行的代码以及代码在内存中的对齐方式。
指令要么是一个半字,要么是两个半字,总共 16 或 32 位,我们可以使用该信息来导致性能下降(特别是如果您强制所有指令都是拇指 2 扩展)
我可以在这个答案中提供完整的 100% 来源,因为我在测试中没有使用任何库。处理器足够慢,闪存上有“零等待状态”,在晶体上运行 8Mhz 只是为了让打印结果的 uart 更准确,否则内部时钟很好。NUCLEO-F411RE 因此应该与他们为 F4 发现购买的相同 m4 核心。我在这里的某个地方有一些最初的 f4 发现,还有一些廉价的克隆,但核要容易得多,而且就放在附近。
大多数时候,当然在这种情况下,您不需要弄乱 DWT 周期计数,因为 systick 给出相同的答案,某些实现(其他供应商,如果有)可能会将系统时钟划分为 systick(如果有 systick) )(也可能不是 dwt)但不是在这种情况下,我得到相同的结果,并且 systick 稍微容易一些,所以......
ldr r2,[r0]
loop:
subs r1,#1
bne loop
ldr r3,[r0]
subs r0,r2,r3
bx lr
Run Code Online (Sandbox Code Playgroud)
从计时器寄存器中的简单循环开始(在本例中为 systick,如果 dwt 周期计数则交换 r2、r3),以测量被测循环周围的情况。
hexstring(STK_MASK&TEST(STK_CVR,0x1000));
hexstring(STK_MASK&TEST(STK_CVR,0x1000));
800011e: 6802 ldr r2, [r0, #0]
08000120 <loop>:
8000120: f1b1 0101 subs.w r1, r1, #1
8000124: f47f affc bne.w 8000120 <loop>
8000128: 6803 ldr r3, [r0, #0]
800012a: 1ad0 subs r0, r2, r3
800012c: 4770 bx lr
800012e: bf00 nop
00003001
00003001
Run Code Online (Sandbox Code Playgroud)
当使用thumb2扩展时,循环本身是对齐的(在8个字的边界上)。
800011e: 6802 ldr r2, [r0, #0]
08000120 <loop>:
8000120: 3901 subs r1, #1
8000122: d1fd bne.n 8000120 <loop>
8000124: 6803 ldr r3, [r0, #0]
8000126: 1ad0 subs r0, r2, r3
8000128: 4770 bx lr
800012a: bf00 nop
00003001
00003001
Run Code Online (Sandbox Code Playgroud)
大拇指指示,此时没关系:
8000120: 6802 ldr r2, [r0, #0]
08000122 <loop>:
8000122: 3901 subs r1, #1
8000124: d1fd bne.n 8000122 <loop>
8000126: 6803 ldr r3, [r0, #0]
8000128: 1ad0 subs r0, r2, r3
800012a: 4770 bx lr
00003001
00003001
Run Code Online (Sandbox Code Playgroud)
通过半字更改对齐方式,拇指指令,不会改变结果
8000120: 6802 ldr r2, [r0, #0]
08000122 <loop>:
8000122: f1b1 0101 subs.w r1, r1, #1
8000126: f47f affc bne.w 8000122 <loop>
800012a: 6803 ldr r3, [r0, #0]
800012c: 1ad0 subs r0, r2, r3
800012e: 4770 bx lr
00004000
00004000
Run Code Online (Sandbox Code Playgroud)
拇指2扩展未对齐,我们看到额外的提取,或者假设它是额外的提取。
自从STM32问世以来,我一直无法关闭ART。闪存 acr 中的预取位不会影响这里的结果。让我们从 sram 和 flash 中运行。
800011e: 6802 ldr r2, [r0, #0]
08000120 <loop>:
8000120: f1b1 0101 subs.w r1, r1, #1
8000124: f47f affc bne.w 8000120 <loop>
8000128: 6803 ldr r3, [r0, #0]
800012a: 1ad0 subs r0, r2, r3
800012c: 4770 bx lr
00003001 flash
00003001
00005FFF sram
00005FFF
Run Code Online (Sandbox Code Playgroud)
拇指2延伸,对齐。
8000120: 6802 ldr r2, [r0, #0]
08000122 <loop>:
8000122: f1b1 0101 subs.w r1, r1, #1
8000126: f47f affc bne.w 8000122 <loop>
800012a: 6803 ldr r3, [r0, #0]
800012c: 1ad0 subs r0, r2, r3
800012e: 4770 bx lr
00004000 flash
00004000
00007FFD sram
00007FFD
Run Code Online (Sandbox Code Playgroud)
拇指2扩展,未对齐,我们看到假设是额外的提取。
8000120: 6802 ldr r2, [r0, #0]
08000122 <loop>:
8000122: 3901 subs r1, #1
8000124: d1fd bne.n 8000122 <loop>
8000126: 6803 ldr r3, [r0, #0]
8000128: 1ad0 subs r0, r2, r3
800012a: 4770 bx lr
00003001
00003001
00005FFD
00005FFD
Run Code Online (Sandbox Code Playgroud)
拇指,未对齐
800011e: 6802 ldr r2, [r0, #0]
08000120 <loop>:
8000120: 3901 subs r1, #1
8000122: d1fd bne.n 8000120 <loop>
8000124: 6803 ldr r3, [r0, #0]
8000126: 1ad0 subs r0, r2, r3
8000128: 4770 bx lr
00003001
00003001
00004001
00004001
Run Code Online (Sandbox Code Playgroud)
拇指对齐,这很有趣。我们稍后会看到
subs 1
bne taken 4
bne not taken 1
subs 0x1000 0x1000 0x1000
bne taken 0x0FFF 0x1FFE up to 0x3FFC
bne not taken 0x0001 0x0001 0x0001
========== =======
0x2FFF 0x4FFD
Run Code Online (Sandbox Code Playgroud)
你的测试中有很多我认为不需要的东西,并且你混合了对齐和未对齐的负载和存储,我将它们分开,我进行了你的测试的一部分......
800021c: b570 push {r4, r5, r6, lr}
800021e: 6802 ldr r2, [r0, #0]
08000220 <loop2>:
8000220: ea04 24f5 and.w r4, r4, r5, ror #11
8000224: ea04 24f5 and.w r4, r4, r5, ror #11
8000228: ea04 2efe and.w lr, r4, lr, ror #11
800022c: ea04 2efe and.w lr, r4, lr, ror #11
8000230: ea06 26fe and.w r6, r6, lr, ror #11
8000234: ea06 26fe and.w r6, r6, lr, ror #11
8000238: ea06 25f5 and.w r5, r6, r5, ror #11
800023c: ea06 25f5 and.w r5, r6, r5, ror #11
8000240: 3901 subs r1, #1
8000242: d1ed bne.n 8000220 <loop2>
8000244: 6803 ldr r3, [r0, #0]
8000246: 1ad0 subs r0, r2, r3
8000248: e8bd 4070 ldmia.w sp!, {r4, r5, r6, lr}
800024c: 4770 bx lr
0000B001
0000B001
00013FFE
00013FFE
Run Code Online (Sandbox Code Playgroud)
你的测试都是拇指2扩展(好吧,三个注册并且毫无疑问旋转)。对齐。
800021c: b570 push {r4, r5, r6, lr}
800021e: 6802 ldr r2, [r0, #0]
08000220 <loop2>:
8000220: ea04 24f5 and.w r4, r4, r5, ror #11
8000224: ea04 24f5 and.w r4, r4, r5, ror #11
8000228: ea04 2efe and.w lr, r4, lr, ror #11
800022c: ea04 2efe and.w lr, r4, lr, ror #11
8000230: ea06 26fe and.w r6, r6, lr, ror #11
8000234: ea06 26fe and.w r6, r6, lr, ror #11
8000238: ea06 25f5 and.w r5, r6, r5, ror #11
800023c: ea06 25f5 and.w r5, r6, r5, ror #11
8000240: 3901 subs r1, #1
8000242: d1ed bne.n 8000220 <loop2>
8000244: 6803 ldr r3, [r0, #0]
8000246: 1ad0 subs r0, r2, r3
8000248: e8bd 4070 ldmia.w sp!, {r4, r5, r6, lr}
800024c: 4770 bx lr
800024e: bf00 nop
0000C001
0000C001
00015FFD
00015FFD
Run Code Online (Sandbox Code Playgroud)
未对齐,因此我们看不到每条指令的额外读取(假设事实就是如此),而整个循环只看到一次。这进一步强化了由于对齐而导致的额外获取。
8000220: b570 push {r4, r5, r6, lr}
8000222: 6802 ldr r2, [r0, #0]
08000224 <loop2>:
8000224: ea04 24f5 and.w r4, r4, r5, ror #11
8000228: ea04 24f5 and.w r4, r4, r5, ror #11
800022c: ea04 2efe and.w lr, r4, lr, ror #11
8000230: ea04 2efe and.w lr, r4, lr, ror #11
8000234: ea06 26fe and.w r6, r6, lr, ror #11
8000238: ea06 26fe and.w r6, r6, lr, ror #11
800023c: ea06 25f5 and.w r5, r6, r5, ror #11
8000240: ea06 25f5 and.w r5, r6, r5, ror #11
8000244: 3901 subs r1, #1
8000246: d1ed bne.n 8000224 <loop2>
8000248: 6803 ldr r3, [r0, #0]
800024a: 1ad0 subs r0, r2, r3
800024c: e8bd 4070 ldmia.w sp!, {r4, r5, r6, lr}
8000250: 4770 bx lr
8000252: bf00 nop
0000B001
0000B001
00013FFE
00013FFE
Run Code Online (Sandbox Code Playgroud)
我继续将其再移动一个半字,使其仅 1 字对齐,而不是 8 字对齐。也许ART会受到影响,但预计sram不会改变。两者都没有受到影响(在像全尺寸手臂这样的更大处理器上,这会产生不同的结果,因为一次提取大约 4 或 8 个字,并且您有很多对齐加上分支预测敏感点,这会导致多个不同的性能数字相同的机器代码)。
你有一些加载和存储,除非我读错了代码,否则你有一个 16 个字的数组,但没有初始化它们。但还是用了它们。这不是浮点也不是乘法/除法,因此不要期望根据数据内容节省任何时钟。我猜你没有超出堆栈/这个数组,正如我可能在这个答案的顶部提到的那样......
8000318: b430 push {r4, r5}
800031a: f04f 5500 mov.w r5, #536870912 ; 0x20000000
800031e: 6802 ldr r2, [r0, #0]
08000320 <loop3>:
8000320: 686c ldr r4, [r5, #4]
8000322: 606c str r4, [r5, #4]
8000324: 3901 subs r1, #1
8000326: d1fb bne.n 8000320 <loop3>
8000328: 6803 ldr r3, [r0, #0]
800032a: 1ad0 subs r0, r2, r3
800032c: bc30 pop {r4, r5}
800032e: 4770 bx lr
00005001
00005001
00008FFE
00008FFE
Run Code Online (Sandbox Code Playgroud)
漂亮、漂亮、对齐。这是我们的基线。
8000318: b430 push {r4, r5}
800031a: f04f 5500 mov.w r5, #536870912 ; 0x20000000
800031e: 6802 ldr r2, [r0, #0]
08000320 <loop3>:
8000320: f8d5 4005 ldr.w r4, [r5, #5]
8000324: f8c5 4005 str.w r4, [r5, #5]
8000328: 3901 subs r1, #1
800032a: d1f9 bne.n 8000320 <loop3>
800032c: 6803 ldr r3, [r0, #0]
800032e: 1ad0 subs r0, r2, r3
8000330: bc30 pop {r4, r5}
8000332: 4770 bx lr
0000A001
0000A001
0000FFFF
0000FFFF
Run Code Online (Sandbox Code Playgroud)
如果核心甚至支持它,如果陷阱被禁用等等,则取消一个字节的对齐。如预期需要更长的时间。相当长,可以从这些测试中开始感受到 sram 周期有多长。
8000318: b430 push {r4, r5}
800031a: f04f 5500 mov.w r5, #536870912 ; 0x20000000
800031e: 6802 ldr r2, [r0, #0]
08000320 <loop3>:
8000320: f8d5 4006 ldr.w r4, [r5, #6]
8000324: f8c5 4006 str.w r4, [r5, #6]
8000328: 3901 subs r1, #1
800032a: d1f9 bne.n 8000320 <loop3>
800032c: 6803 ldr r3, [r0, #0]
800032e: 1ad0 subs r0, r2, r3
8000330: bc30 pop {r4, r5}
8000332: 4770 bx lr
00008001
00008001
0000DFFF
0000DFFF
Run Code Online (Sandbox Code Playgroud)
半字对齐但未字对齐,字循环。这很有趣,这是没有预料到的。必须检查文档。
该处理器提供三个主要总线接口,实现 AMBA 3 AHB-Lite 协议的变体
对代码存储空间(0x00000000 至 0x1FFFFFFF)的数据和调试访问是通过 32 位 AHB-Lite 总线执行的。
所以它是来自 ARM 端的 32 位,但芯片供应商可以做任何他们想做的事情,所以也许他们的 sram 是由 16 位宽的块构建的,谁知道呢。
8000318: b430 push {r4, r5}
800031a: f04f 5500 mov.w r5, #536870912 ; 0x20000000
800031e: 6802 ldr r2, [r0, #0]
08000320 <loop3>:
8000320: f8d5 4007 ldr.w r4, [r5, #7]
8000324: f8c5 4007 str.w r4, [r5, #7]
8000328: 3901 subs r1, #1
800032a: d1f9 bne.n 8000320 <loop3>
800032c: 6803 ldr r3, [r0, #0]
800032e: 1ad0 subs r0, r2, r3
8000330: bc30 pop {r4, r5}
8000332: 4770 bx lr
0000A001
0000A001
0000FFFF
0000FFFF
Run Code Online (Sandbox Code Playgroud)
现在正如预期的那样,这种对齐也比正确对齐要糟糕得多。
MACRO r1, r2, r3, 48 aligned
MACRO r4, r5, r6, 52 unaligned
MACRO r7, r8, r9, 56 unaligned
MACRO r10, r11, r12, 60 aligned
Run Code Online (Sandbox Code Playgroud)
这些未对齐的访问将创建额外的时钟。除其他可能的事情外。
8000318: b430 push {r4, r5}
800031a: f04f 5500 mov.w r5, #536870912 ; 0x20000000
800031e: 6802 ldr r2, [r0, #0]
08000320 <loop3>:
8000320: f3af 8000 nop.w
8000324: f3af 8000 nop.w
8000328: 3901 subs r1, #1
800032a: d1f9 bne.n 8000320 <loop3>
800032c: 6803 ldr r3, [r0, #0]
800032e: 1ad0 subs r0, r2, r3
8000330: bc30 pop {r4, r5}
8000332: 4770 bx lr
00005000
00005000
00007FFF
00007FFF
Run Code Online (Sandbox Code Playgroud)
与
00005001
00005001
00008FFE
00008FFE
Run Code Online (Sandbox Code Playgroud)
nops 而不是 ldr/str。不一定能帮助我们测量 ldr/str 指令。但我并不认为它始终是固定的 2 条指令。
现在,显然编译后的代码将尽可能利用拇指指令。创建拇指和拇指2的混合,最好是拇指。因此,对于相同数量的指令,它将是或可以是更少的读取。展开当然可以节省你的循环次数乘以一定数量的时钟(哦,对了,我尝试了 BPIALL,但没有看到任何效果,我认为在 -m7 中你可以搞乱分支预测,如果 m4 或 m3 中甚至有的话,等等)(绝对可以在全尺寸的手臂和其他处理器中看到它,结合对齐再次使相同机器代码的不同性能测量值加倍)(最终结果基准测试是BS,并且无法计算指令数并计算出最后几个的时钟几十年左右),这样您就可以节省那些额外的循环分支时钟。即使有额外的指令,没有分支的线性代码通常也是最快的。
我不会完全重复你所写的实验。我想我已经提供了一些可供咀嚼的信息,当然我认为你的 ldr/str 时机是错误的。我不认为在所有情况下每条指令都是 2 个时钟。(您还对内存推送/弹出循环计数器,可能导致额外的未计数时钟或每个循环很少)。我还认为 ART 处于打开状态并且无法关闭,因此您会得到一些缓慢的闪存以及它们的预取缓存内容,这些内容为核心提供数据,这使得此类测量更加难以控制/理解。虽然 ti 和 nxp 可能购买了 m4 的不同版本(我有一段时间没有查看 arm 是否发布了多个版本),并且总是有供应商定制。我记得 ti 没有像 st 那样的魔法闪存缓存。它可能实现了实际的数据缓存,这使得在相同机器代码上再次乘以性能测量结果变得更加有趣。但与您的期望相比,您可能会感受到不同系统中 m4 的作用。我认为部分问题在于期望,部分与许多平台有关,我们几十年来一直无法从指令中计算时钟,并且系统本身在处理器之上的性能方面发挥着重要作用。MCU 便宜且足够快,并且不一定是高性能机器(我们的台式机也不是)现代总线的本质,它不是每个东西一个周期,与管道相结合,单独获取通常会造成无法估量的混乱。在其他人插话之前,我同意,到目前为止,在这些 cortex-m 平台上,特定的二进制构建,没有中断等干扰,如果不更改任何变量,二进制的性能是一致的。但是您可以使用看似与任何事情无关的内容重新编译该程序,甚至可能位于与它所影响的代码无关的文件中,并且会看到与下一个版本的显着性能差异。
仅未对齐的 ldr/strs 就可以轻松解释 200 个时钟计数差异。
最重要的是,处理器只是系统的一部分,我们没有(完全)受到处理器的限制,因此它的时序并不决定性能(不能再使用/依赖指令时序文档)。我认为,因此存在一些预期问题,并且有一些额外的时钟在这里和那里潜入,一到两位数的性能预期百分比来自系统问题而不是处理器问题。
使用thumb 和thumb2 扩展的C 编译器即使相同数量的指令执行速度可能会也可能不会更快,但埋藏在管道中或停止管道的读取操作确实较少。与每次读取强制执行一条指令相比。
根据您的评论,使用 SYSCFG_MEMRMP (感谢您在这个寄存器上教育我)。
特定测试
00003001 flash
00003001 flash
00004001 sram
00004001 sram
00003001 sram through icode bus
00003001 sram through icode bus
Run Code Online (Sandbox Code Playgroud)
所以它有效,感谢您的信息。不会再经历整个答案,但很高兴知道未来。
我把这个问题放在一边一段时间,经过几个小时的重新审视后,我实际上能够击败问题中显示的最坏情况时序预测(强调它们是最坏情况,所以它是他们能被打败并不意外)。有两个完全不同的问题在起作用,我将一次处理一个问题。
首先,正如对现有答案的评论中所见,我发现的一个技巧是将堆栈映射到 CCMRAM。然而,这对我来说从来没有意义,除非通过 STM32F407 的总线矩阵引入了延迟,但我没有发现任何证据证明这一点。
事实证明我的预感是正确的:不涉及 CCMRAM 也可以达到全速。关键是STM32F407参考手册第2节图1 :
此外,请注意Cortex-M4 技术参考手册第 2.3.1 节(“总线接口”)中的以下注释:
系统界面
对地址范围 0x20000000 至 0xDFFFFFFF 和 0xE0100000 至 0xFFFFFFFF 的指令读取、数据和调试访问是通过 32 位 AHB-Lite 总线执行的。
对于同时访问 32 位 AHB-Lite 总线,仲裁顺序按优先级递减为:
- 数据访问。
- 指令和向量读取。
- 调试。
系统总线接口包含处理未对齐访问、FPB 重新映射访问、位带访问和流水线指令获取的控制逻辑。
流水线指令获取
为了在系统总线上提供干净的时序接口,需要注册对该总线的指令和向量获取请求。
这会导致额外的延迟周期,因为从系统总线获取指令需要两个周期。这也意味着不可能从系统总线连续读取指令。
(我自己强调最后一段。)因此请注意,通过系统总线(上图中的S总线)进行访问需要一个额外的周期。另请注意,通过查看总线矩阵,内核的 D 总线和 SRAM2 之间没有连接,只有 SRAM1。根据 STM32F407 的参考手册,SRAM2 对应于 0x2001C000-0x2001FFFF 范围内的地址,即常规非 CCM RAM 的 128 KB 块中的最后 16 KB。
现在将其与在链接器脚本中初始化堆栈指针的常用技术结合起来(相关部分引用自链接器脚本,据我所知,该链接器脚本直接来自 ST):
MEMORY
{
CCMRAM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
}
/* Highest address of the user mode stack */
_estack = ORIGIN(RAM) + LENGTH(RAM); /* end of "RAM" Ram type memory */
Run Code Online (Sandbox Code Playgroud)
正如所写,这确保堆栈指针将从 0x20020000 开始,因此堆栈的前 16 KB 将直接落入 SRAM2,速度较慢。虽然这通常是避免堆栈溢出的合理策略(从最低 RAM 地址开始静态分配变量,同时将堆栈指针设置为最高 RAM 地址,在两者之间创建最大可能的间隙),但它会导致严重的性能影响。
事实上,只需将堆栈指针重新定位到 SRAM1,我就能够将问题中 MRE 的执行时间从 1536 个周期减少到 1407 个周期。
这的含义超出了我问题中的玩具示例;这应该会影响基于 ST 提供的默认链接器脚本的每个 STM32F407 固件。当考虑到总线矩阵中 Cortex-M4 D 总线和 SRAM2 之间缺乏连接以及堆栈指针的默认选择时,ST 在这里所做的事情近乎犯罪/严重疏忽。考虑到所有已发货的 STM32F407 单元(以及可能受此问题影响的许多其他 MCU),因此在全球范围内造成的性能损失/能源浪费是不可想象的。ST你真丢脸!
在 Cortex-M4 技术参考手册的第 3.3.3 节(“加载/存储时序”)中,对加载和存储指令的配对进行了一系列考虑。引用第一个断言:
STR Rx,[Ry,#imm]总是一个周期。这是因为地址生成是在初始周期中进行的,并且数据存储是在执行下一条指令的同时进行的。如果存储到写入缓冲区,并且写入缓冲区已满或未启用,则下一条指令将延迟,直到存储完成。如果存储到写入缓冲区,例如存储到代码段,并且该事务停止,则只有在完成之前执行另一个加载或存储操作时才会感受到对时序的影响。
请注意,我的 MRE 中的宏以加载开始,以存储结束(并且该存储使用上面引用的精确寻址模式);鉴于这些宏是按顺序实例化的,一个实例末尾的存储后面是下一个实例开头的加载。
我的理解是,这个写缓冲区默认是启用的:参见STM32 Cortex-M4编程手册的第4.4.1节(“辅助控制寄存器(ACTLR)”),位1(“DISDEFWBUF”),并注意复位状态该寄存器的全部为 0 位——在位 1 的情况下,行为是“启用写缓冲区使用”。另外,我认为存储缓冲区会在几个周期后清除,肯定比一个存储和下一个存储之间的 10 个以上周期(从宏的下一个实例化开始)要快。
不管怎样,我决定尝试将存储指令移到代码流的前面,以便它不与宏的下一个实例化的加载相邻。也就是说,我将问题中的 MRE 中的宏重写为以下内容:
.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
str lr, [r0, #\d]
and \r_1, \r_2, \r_1, ror #11
and \r_1, \r_2, \r_1, ror #11
.endm
Run Code Online (Sandbox Code Playgroud)
此版本将周期计数从 1407 个(应用上述问题 #1 的修复后)减少到 1307 个。这正好是 100 个周期,而且我认为上述更改消除了 100 个 后跟 的实例并不是STR巧合LDR。最重要的是,到目前为止,我已经打破了问题表中 1364 个周期的原始预测,因此至少我已经达到(并且确实改进了)最坏的情况。另一方面,考虑到上面关于如何STR Rx,[Ry,#imm]总是需要一个周期的引用,也许更好的估计是 1264 个周期,因此仍然有 43 个周期的差异需要解释。如果有人可以进一步改进预测或编码以达到这个推测的 1264 个周期界限,我将非常有兴趣知道。
最后,这个SO问题及其答案可能包含相关信息。在接下来的几天里我会重读几次,看看它是否提供了进一步的见解。