TRE*_*lec 5 c microcontroller arm stm32 cortex-m
我注意到一个无法解释的行为:函数的执行时间似乎取决于它在闪存 ROM 中的位置。我使用的是 STM32F746NGH 微控制器(基于 ARM-cortex M7)和 STM32CubeIDE(适用于 ARM 的 GCC 编译器)。
这是我的测试:
我初始化了 SysTick 计数器以触发固定周期 T = 1ms 的中断。在中断处理程序中,我在两个线程之间切换(像 RTOS):让我们将它们命名为 Thread1 和 Thread2。
每个线程只是简单地递增一个变量。
这是两个线程的代码:
uint32_t ctr1, ctr2;
void thread1(void)
{
while(1)
{
ctr1++;
}
}
void thread2(void)
{
while(1)
{
ctr2++;
}
}
Run Code Online (Sandbox Code Playgroud)
在监视这些变量时,我注意到 ctr2 的增量比 ctr1 快得多。
使用此代码:线程1的地址是0x08000418,线程2的地址是0x0800042C。
然后,我尝试将另一个函数放在 thread1 之前的内存中:我们将其命名为 thread0。
所以我的新代码是:
uint32_t ctr0, ctr1, ctr2;
void thread0(void)
{
while(1)
{
ctr0++;
}
}
void thread1(void)
{
while(1)
{
ctr1++;
}
}
void thread2(void)
{
while(1)
{
ctr2++;
}
}
Run Code Online (Sandbox Code Playgroud)
使用此新代码: thread0 的地址为 0x08000418(thread1 与先前代码的位置),thread1 的地址为 0x0800042C(thread2 与先前代码的位置),thread2 的地址为 0x08000440。
我可以看到 ctr1 和 ctr2 以相同的速率递增,而 ctr0 的递增速度比这两个慢得多。
最后,我尝试了 20 个不同的线程。每个线程都会递增一个变量(类似于上面共享的代码)。我观察到变量以两种不同的速率递增:speed1 和 speed2;速度 1 低于速度 2。
线 | 地址 | 速度 |
---|---|---|
线程0 | 0x08000418 | 速度1 |
线程1 | 0x0800042C | 速度2 |
线程2 | 0x08000440 | 速度2 |
线程3 | 0x08000454 | 速度1 |
线程4 | 0x08000468 | 速度2 |
线程5 | 0x0800047C | 速度2 |
线程6 | 0x08000490 | 速度2 |
线程7 | 0x080004A4 | 速度2 |
线程8 | 0x080004B8 | 速度1 |
线程9 | 0x080004CC | 速度2 |
线程10 | 0x080004E0 | 速度2 |
线程11 | 0x080004F4 | 速度1 |
线程12 | 0x08000508 | 速度2 |
线程13 | 0x0800051C | 速度2 |
线程14 | 0x08000530 | 速度2 |
线程15 | 0x08000544 | 速度2 |
线程16 | 0x08000558 | 速度1 |
线程17 | 0x0800056C | 速度2 |
线程18 | 0x08000580 | 速度2 |
线程19 | 0x08000594 | 速度1 |
我还在程序集中检查了所有线程都有相似的代码(相同的代码大小、相同的指令和相同的指令数量);所以与代码本身无关。每个线程有 10 条指令,因此代码大小为 20 字节(每条指令为 2 字节宽)。它对应于每个线程的内存地址之间的增量(20 = 0x14)。
这是一个线程的代码(正如前面所说,其他线程也有类似的代码):
task0:
08000418: push {r7}
0800041a: add r7, sp, #0
21 task0_ctr += 1;
0800041c: ldr r3, [pc, #8] ; (0x8000428 <task0+16>)
0800041e: ldr r3, [r3, #0]
08000420: adds r3, #1
08000422: ldr r2, [pc, #4] ; (0x8000428 <task0+16>)
08000424: str r3, [r2, #0]
08000426: b.n 0x800041c <task0+4>
08000428: movs r4, r3
0800042a: movs r0, #0
Run Code Online (Sandbox Code Playgroud)
正如您在表中看到的,似乎存在一种模式:一个线程具有 speed1,两个线程具有 speed2,一个线程具有 speed1,4 个线程具有 speed2,然后重新启动该模式。
我不知道它是否相关,但在 Cortex M7 参考手册中,我找到了有关闪存的部分:
指令预取 每个闪存读取操作提供 256 位,根据启动的程序,表示 8 条 32 位指令到 16 条 16 位指令。因此,在顺序代码的情况下,至少需要 8 个 CPU 周期来执行先前的指令行读取。ITCM 总线上的预取允许在 CPU 请求当前指令行时读取闪存中连续的下一行指令。可通过设置 FLASH_ACR 寄存器的 PRFTEN 位来启用预取。如果需要至少一个等待状态来访问闪存,则此功能非常有用。当代码不是顺序的(分支)时,该指令可能既不存在于当前使用的指令行中也不存在于预取的指令行中。在这种情况下(未命中),周期数方面的惩罚至少等于等待状态的数量。自适应实时记忆
但我已经检查了表:完全包含在 256 位块中的函数可以具有 speed1 或 speed2,对于两个 256 位块之间共享的函数也是如此。
我不明白这种行为的原因可能是什么。
编辑1:根据要求,这里是线程调度程序代码:
__attribute__((naked)) void SysTick_Handler(void)
{
__asm("CPSID I"); // disable global interrupts, equivalent to __disable_irq();
/* save current thread's context: save R4, R5, ..., R11 (xPSR, PC, LR, R12, R3, R2, R1, R0 are automatically pushed on the stack by the processor). */
__asm("PUSH {R4-R11}");
/* OS_Tick += 1 */
__asm("LDR R0, =OS_Tick"); // R0 = &OS_Tick
__asm("LDR R1, [R0]"); // R1 = OS_Tick
__asm("ADD R1, #1"); // R1 += 1
__asm("STR R1, [R0]"); // OS_Tick = 1;
/* Systick_Tick += 1 */
__asm("LDR R0, =Systick_Tick"); // R0 = &Systick_Tick
__asm("LDR R1, [R0]"); // R1 = Systick_Tick
__asm("ADD R1, #1"); // R1 += 1
__asm("STR R1, [R0]"); // Systick_Tick = 1;
/* Scheduler: switch thread */
__asm("LDR R0, =os_kernel_threads_list"); // R0 = &os_kernel_threads_list
__asm("LDR R1, [R0]"); // R1 = current_thread
__asm("STR SP, [R1,#4]"); // stack_ptr = SP
__asm("LDR R2, [R1]"); // R2 = next_tcb
__asm("STR R2, [R0]"); // current_thread = next_tcb (new thread)
__asm("LDR SP, [R2,#4]"); // SP = stack_ptr (new thread)
__asm("POP {R4-R11}"); // restore context (new thread)
__asm("CPSIE I"); // enable global interrupts, equivalent to __enable_irq();
/* return from interrupt */
__asm("BX LR");
}
Run Code Online (Sandbox Code Playgroud)
OS_Tick 和 Systick_Tick 是两个 uint32_t 变量。os_kernel_threads_list 是一个 tcb_list 变量,见下文:
/*
* Thread Control Block (TCB) structure
*/
typedef struct tcb_
{
struct tcb_ *next_tcb; // linked-list, pointer to the next thread
int32_t *stack_ptr; // pointer to the top of the thread's stack (next item to pop / last value stacked)
int32_t stack[THREAD_STACK_SIZE]; // thread's stack
} tcb_struct;
/*
* Circular linked-list of threads.
*/
typedef struct
{
tcb_struct *current_thread; // pointer to the current running thread
tcb_struct threads[N_MAX_THREADS]; // array of threads
int n_threads; // number of threads created
} tcb_list;
Run Code Online (Sandbox Code Playgroud)
线程存储在数组中,并以循环链表方式连接。
编辑2:附加信息:这是我的时钟设置:
PLL源:晶体振荡器@25MHz
系统时钟 = PLL_时钟 = 216MHz
按照 STM32 数据表中的建议,闪存等待状态 = 7WS。
你基本上自己回答了这个问题。您会在 ARM 和 MIPS 等高性能内核上看到这种情况,在 x86 等高开销内核上您将无法看到这种情况(并不是说它不会发生)。
现在,公平地说,您可能会看到其他效果,并且在 RTOS 中运行时会产生其他开销。但我可以轻松地演示这一点,无需任何这些东西,裸机,无中断等。只需核心一次只做一件事。
我们都可以想象缓存会发生什么,第一次可能会出现缓存未命中,并且第一次可能会导致巨大的延迟(取决于系统,在 MCU 上不一定很大),然后是第二次,假设是在缓存中速度更快。同样,如果将循环对齐到缓存行的末尾附近,以便循环本身或下一个预取进入第二个缓存行。使第一个循环花费更长的时间。与我所说的获取线相同,但更糟糕的是,因为它们本身第二次不会变得更快。现在一些分支预测器会有所帮助。
分支预测不一定是看起来超级聪明的逻辑,它试图解码指令并展望可能导致该分支发生的指令/结果,然后开始读取。相反,更有可能的是,有一个很小的地址缓存,当它第一次执行一个地址并且该地址导致或可能导致获取时,它们会将其添加到这个简短的列表中,并且当您接近该地址时(即使您自己修改代码)它将抛出预取。如果必须发生的话,这可能会造成伤害。但现实情况是,分支预测只是比实际情况提前几个时钟开始预取(这很好,但绝不是魔法,也不是复杂的逻辑)。
我使用 NUCLEO-F767ZI,因为它是我方便使用的。它将与您的芯片中的 Cortex-M7 相同。我们不能保证整个芯片是一样的(st让芯片不被arm记住)。但是,由于使用的 STM32 芯片数量超过了我多年来从一端到另一端的整个范围的数量,cortex-m7 基础设施将更加相似而不是不同。使用 cortex-m7 st 具有更大的灵活性,您将看到,虽然它们仍然支持经典的 0x08000000 地址,但 ITCM 地址是 0x00200000,这就是您应该用于链接这些部分的地址。显然你可以而且应该在你的芯片上尝试这个。您应该会看到类似的结果。
我没有使用来自 st 或其他任何代码的任何代码,我的所有代码都是根据 ARM 和 ST 文档编写的。切换到板载晶体以获得更可靠的 UART 参考时钟。设置串口打印结果。较新的部件更好,但长期以来,许多供应商部件上的零等待状态意味着闪存以系统速率的一半运行。当您提高以前系统的速度时,可能仍然需要添加等待状态。最大化时钟并不会让你的系统运行得更快,你仍然受到闪存速度限制的约束(我们已经很长时间没有受到处理器的限制),核心可以快速地突发运行指令,但必须等待更多的时钟指令。或者外围设备(如果外围设备有另一个)。我没有查你的芯片文档,这个肯定有一个系统时钟电源表和等待状态表。欢迎您计时并正确等待状态并重复这些实验。它应该与仅以 8MHz 运行并添加这些等待状态相同或相当。基本上,欢迎您最大化时钟,但您仍然会看到我正在演示的问题,您可能还有其他问题,但演示这个问题是微不足道的。
我认为这是一种派对技巧,可能在实际的派对上不起作用,但你可以让你的同事感到困惑或开心。请注意,当您看到更原始的 cortex-ms 具有半字或字大小的提取时,您必须有一个提取线,您可能看不到这一点。不过m7。
预取单元
预取单元 (PFU) 提供:
64 位指令读取带宽。
4x64 位预取队列可将指令预取与 DPU 管道操作分离。
用于分支预测器状态和目标地址的单周期周转的分支目标地址缓存 (BTAC)。
未指定 BTAC 时的静态分支预测器。
转发标志以早期解析解码器中的直接分支和处理器管道的第一执行阶段。
第一个被测试的代码是
.balign 0x100
/* r0 count */
/* r1 timer address */
.thumb_func
.globl TEST
TEST:
push {r4,r5}
ldr r4,[r1]
loop:
sub r0,#1
bne loop
ldr r5,[r1]
sub r0,r4,r5
pop {r4,r5}
bx lr
nop
nop
nop
nop
Run Code Online (Sandbox Code Playgroud)
只是一个简单的计数循环(不统一语法)。对齐得很好。
我正在使用 systick 计时器,我可以通过 DWT 计时器给出相同结果的演示。确实,一些芯片供应商在系统控制杆上设置了除数。好吧,好吧,st 文档显示除以 8,但我认为这是一个长期存在的拼写错误......
从 systick 开始,检查 dwt,如果 dwt 没有好 8 倍,则返回 systick。
从 0x08000000 开始
08000100 <TEST>:
8000100: b430 push {r4, r5}
8000102: 680c ldr r4, [r1, #0]
ra=TEST(0x1000,STK_CVR); hexstring(ra&0x00FFFFFF);
ra=TEST(0x1000,STK_CVR); hexstring(ra&0x00FFFFFF);
ra=TEST(0x1000,STK_CVR); hexstring(ra&0x00FFFFFF);
ra=TEST(0x1000,STK_CVR); hexstring(ra&0x00FFFFFF);
00001029
00001006
00001006
00001006
Run Code Online (Sandbox Code Playgroud)
这看起来像是一些缓存。在其他 stm32 芯片上,您有无法关闭的缓存闪存功能。在这部分和其他 stm32 cortex-m7 上都可以。事实上,文档说(FLASH_ACR 寄存器)ART 和预取都被禁用。有趣的是,这些数字看起来很可疑,如果缓存关闭,第一个循环有何不同?是西棍吗?
载重吨
00001029
00001006
00001006
00001006
Run Code Online (Sandbox Code Playgroud)
Arm 文档讨论了分支预测等内容,但我认为它写得不好(如果您尝试搜索方法)。看起来 BTAC 默认情况下是启用的,我们可以在 ACTLR 寄存器中将其关闭(分支目标地址缓存,缓存一些地址及其预取的目的地)。
0000400F
0000400F
0000400F
0000400F
Run Code Online (Sandbox Code Playgroud)
好多了。
00200100 <TEST>:
200100: b430 push {r4, r5}
200102: 680c ldr r4, [r1, #0]
Run Code Online (Sandbox Code Playgroud)
这个芯片在两个地址之间没有看到性能差异,这很奇怪。还有一个实验需要弄清楚。
所以 64 位读取是四个 16 位的东西或 2 个 32 位的东西。假设总线是 32 位或 64 位宽。因此,对于上面的内容,我们假设在 0x100 处有一次提取,它开始通过管道运行这些提取,然后提取下一行,使其排队。
.balign 0x100
nop
/* r0 count */
/* r1 timer address */
Run Code Online (Sandbox Code Playgroud)
在这里放置一个 nop 或其他东西来改变我们简单测试的对齐方式。
00200102 <TEST>:
200102: b430 push {r4, r5}
200104: 680c ldr r4, [r1, #0]
00005002
00005002
00005002
00005002
Run Code Online (Sandbox Code Playgroud)
就这样吧。完全相同的机器代码,相同的芯片,相同的系统,一切都相同,只是完全相同的机器代码处于不同的对齐方式。
请注意,启用 BTAC 后,您仍然会获得不同的执行时间。
00002010
00002004
00002004
00002004
Run Code Online (Sandbox Code Playgroud)
如果我们添加另一个 nop 并将两条指令放在读取行的中间会怎么样?
00200104 <TEST>:
200104: b430 push {r4, r5}
200106: 680c ldr r4, [r1, #0]
00004003
00004003
00004003
00004003
Run Code Online (Sandbox Code Playgroud)
还:
00200106 <TEST>:
200106: b430 push {r4, r5}
200108: 680c ldr r4, [r1, #0]
00004003
00004003
00004003
00004003
Run Code Online (Sandbox Code Playgroud)
有趣的。我将切换到 sram,在许多 Mcus 上,即使在“零等待状态”下,闪存也比 sram 慢。使用 sram 让我可以进行一些自修改代码,每次运行可以进行多个测试。
第一个数字是被测代码前面的 nop 数量,控制对齐。
00000000 00004003
00000000 00004003
00000000 00004003
00000000 00004003
00000001 00005002
00000001 00005002
00000001 00005002
00000001 00005002
00000002 00004003
00000002 00004003
00000002 00004003
00000002 00004003
00000003 00004003
00000003 00004003
00000003 00004003
00000003 00004003
00000004 00004003
00000004 00004003
00000004 00004003
00000004 00004003
00000005 00005002
00000005 00005002
00000005 00005002
00000005 00005002
00000006 00004003
00000006 00004003
00000006 00004003
00000006 00004003
00000007 00004003
00000007 00004003
00000007 00004003
00000007 00004003
Run Code Online (Sandbox Code Playgroud)
所以从 0x20002000 到 0x20002002,从 4x2=8 0x2008 到 0x200A。8个字节就是64位。因此,弄乱获取行显然会给我们相同机器代码的两个结果。我错了上面两条指令是 4 个字节,这是取指令的四分之一?您可能会认为,如果在 64 位对齐上,并且超过了一个,它会导致速度变慢,那么您几乎会预期其他未对齐(64 字节中的 4 个字节)也会变慢。我将停止尝试分析它;我无权访问 sim 核心,即使我访问了,我也无法谈论它。
我们看到,至少在这个芯片上,闪存和缓存在闪存上处于零等待状态时是相同的,而不是在闪存上更慢。mcus 中的闪光越来越好。
00200100 <TEST>:
200100: b430 push {r4, r5}
200102: 680c ldr r4, [r1, #0]
00200104 <loop>:
200104: 3801 subs r0, #1
200106: d1fd bne.n 200104 <loop>
Run Code Online (Sandbox Code Playgroud)
循环不是在 0x100 处,而是在 0x104 处开始,然后我们将其移动到 0x106,使 bne 在下一个获取行中位于 0x108 处。我已经看到核心立即获取分支后的第二行,这一条可能正在等待,不知道,我无权访问它。
反正。
00200100 <TEST>:
200100: b430 push {r4, r5}
200102: 680c ldr r4, [r1, #0]
00200104 <loop>:
200104: 46c0 nop ; (mov r8, r8)
200106: 3801 subs r0, #1
200108: d1fc bne.n 200104 <loop>
Run Code Online (Sandbox Code Playgroud)
如果我在循环中添加一个 nop
00005002
00005002
00005002
00005002
Run Code Online (Sandbox Code Playgroud)
这是有道理的。
在它们之间放置 nop 会得到相同的结果。
如果我们使用 sram 和不同数量的对齐。
00000000 00005002
00000000 00005002
00000000 00005002
00000001 00005002
00000001 00005002
00000001 00005002
00000001 00005002
00000002 00005002
00000002 00005002
00000002 00005002
00000002 00005002
00000003 00005002
00000003 00005002
00000003 00005002
00000003 00005002
00000004 00005002
00000004 00005002
00000004 00005002
00000004 00005002
00000005 00005002
00000005 00005002
00000005 00005002
00000005 00005002
00000006 00005002
00000006 00005002
00000006 00005002
00000006 00005002
00000007 00005002
00000007 00005002
00000007 00005002
00000007 00005002
Run Code Online (Sandbox Code Playgroud)
不仅仅是每个循环都有这个问题。
循环中有两个 nop
00000000 00005003
00000000 00005003
00000000 00005003
00000000 00005003
00000001 00006002
00000001 00006002
00000001 00006002
00000001 00006002
00000002 00005003
00000002 00005003
00000002 00005003
00000002 00005003
00000003 00005003
00000003 00005003
00000003 00005003
00000003 00005003
00000004 00005003
00000004 00005003
00000004 00005003
00000004 00005003
00000005 00006002
00000005 00006002
00000005 00006002
00000005 00006002
00000006 00005003
00000006 00005003
00000006 00005003
00000006 00005003
00000007 00005003
00000007 00005003
00000007 00005003
00000007 00005003
Run Code Online (Sandbox Code Playgroud)
(如果处理器如此敏感或可能如此敏感,请考虑基准测试的价值)。
您可以在实际应用中看到这些性能差异的变化。在完全不相关的函数中添加或删除代码可能会对整个二进制文件产生级联效应。有些循环对位置敏感,有些则不敏感。循环包裹循环、内部有多个循环的循环可以相互抵消或放大问题。
0x2004/0x1006 = 199.8%。超过我在某处评论过的两位数。
00200100 <TEST>:
200100: b430 push {r4, r5}
200102: 680c ldr r4, [r1, #0]
00200104 <loop>:
200104: 3801 subs r0, #1
200106: d1fd bne.n 200104 <loop>
Run Code Online (Sandbox Code Playgroud)
启用 BTAC 后,我们之前看到过。
00001011
00001006
00001006
00001006
Run Code Online (Sandbox Code Playgroud)
FLASH_ACR寄存器中的PRFTEN没有变化。
ART 加速器打开,没有变化。
所以我继续摆弄FLASH_ACR寄存器。看起来我们受到处理器的限制。或者某个地方正在进行一些缓存。
例如,现在当您升级到 RTOS 时。不一定在这个核心中,但添加指令缓存会有所帮助,但请记住,无论有没有缓存,您仍然会获取相同的行并具有相同的获取行边界问题。后备内存有时可能会更快,但对齐问题仍然存在(在您可以首先检测到它的系统上)。添加我们在 cortex-m 上没有的 MMU,它们定义了内存区域,这样您就不必使用缓存来告诉系统什么是非缓存外设什么是指令内存什么是数据内存。mmu 增加了自己的性能影响,并且通常有不止一种方法将虚拟地址空间映射到物理地址空间,但是映射方式可能/将会出现性能问题等等。您的 rtos 可能会遇到更多问题,但是如果您如果严格使用相同的机器代码和不同的对齐方式获取两个特定的数字,您可能会遇到每个循环的获取次数的简单问题。
简短回答:
您基本上已经完成了所有工作并找到了答案。
cortex-m7 具有 64 位读取。当它确定需要向后分支并执行该提取时,您在该提取行和管道中的位置现在会影响每个循环的许多总提取。如果您保留完全相同的机器代码并将其移动到地址空间,则某些循环可以在每个循环中进行额外的提取。零等待状态并不意味着零时钟,取指不是空闲的,并且并不是所有系统内存都参与取指。相对于管道准备好将其用于分支的获取到达时间也会影响循环性能。循环的代码大小决定了额外的提取惩罚,每个循环的 3 次提取有时是 4 次,10 次提取有时是 11 次,将会有不同的相对命中。20%、10% 等。它与 rom/flash 中的对齐无关,但一般来说,如果您要在 sram 中运行该代码,您还应该能够找到两个执行时间。
请注意,不要假设任何其他核心与 cortex-m7 完全相同,其他 cortex-ms 要么没有足够的获取能力来查看这一点。不同的管道等。有些可能需要更多的系统时钟来完成同样的事情。较旧的内核与较旧的闪存技术相结合,这些系统设计可能会显示,例如,从闪存运行和从 sram 运行相同的代码,具有不同的性能。
在广泛的 STM32 系列中,还存在无法在某些芯片上禁用的闪存缓存和预取,这使得此类性能分析变得更加困难。虽然其他品牌可以购买相同的内核,但请注意,内核有编译时选项,并且 ARM 内核只是该芯片的一部分,其余逻辑是其他人的,并且假设没有两家公司拥有相同的 IP,当然,其中一些IP是芯片公司并没有购买的。例如,您应该在所有基于 cortex-m7 的芯片上看到这一点,但确切的差异可能会有所不同。
在具有 RTOS 效应的 RTOS 上运行也会导致性能问题。对于这个特定的 ARM 内核,可以为同一代码生成两个不同的执行时间,我相信您已经发现了这一点。除此之外,您可能还会发现其他性能问题。
在您的情况下,单循环时间根据对齐情况而变化,因此您不是测量 X 循环,而是测量 Y 时间内有多少个循环,同样,每个循环更多的时钟意味着每秒更少的计数。您没有像我一样严格测量时间(Michael Abrash:汇编语言的禅宗),因此您的中断延迟和开销可能会在这里产生影响。就我个人而言,我认为从您的结果以及该核心的工作方式等来看,至少每次编译的这两个任务的任务切换时间应该相等。我想我只是提出一个免责声明,除了上述内容之外,您可能还会看到其他内容。
问题是我们能对此做些什么(如果有的话)。好吧,这从过早优化讨论的答案开始。一旦您出于某种原因决定确实想要进行一些优化并且隔离了该代码,那么您会做什么。有时您可以更改 C 代码来帮助编译器使某些事情变得更简单或更快(请记住,更少的指令并不意味着更快...更快意味着更快...而更快是相对于系统而言的,因此一个系统上的相同代码可能会另一个速度较慢,反之亦然)。
如果这不起作用,常见的解决方案是让编译器先完成工作,然后手动优化。因此,使用 gcc,我们知道工具链步骤通过汇编器,C 代码被编译为汇编语言,然后调用汇编器将其转换为对象,然后调用链接器将其变成二进制文件(gcc 二进制文件本身不是编译器它只是启动一些实际完成工作的其他程序)。我发现输出很痛苦,并且宁愿通过反汇编来工作,但您的体验可能会有所不同。无论如何,现在您的函数或其他任何语言都是汇编语言,并且您可以通过这种方式管理手动调整。您也可以在实际汇编中从头开始编写关键代码。在这种情况下这可能有效。您可以看到维护是什么样子以及过早优化的人员来自哪里。
对于这个特定的平台/代码,我们可以做一些技巧,尽管知道我们所知道的,它仍然是相当手动的,并且可能需要大量的维护。
volatile unsigned int ctr1,ctr2;
void thread1(void)
{
while(1)
{
ctr1++;
}
}
void thread2(void)
{
while(1)
{
ctr2++;
}
}
Run Code Online (Sandbox Code Playgroud)
指责
Disassembly of section .text:
00200000 <reset-0x8>:
200000: 20001000 andcs r1, r0, r0
200004: 00200009 eoreq r0, r0, r9
00200008 <reset>:
200008: e7fe b.n 200008 <reset>
...
0020000c <thread1>:
20000c: 4a02 ldr r2, [pc, #8] ; (200018 <thread1+0xc>)
20000e: 6813 ldr r3, [r2, #0]
200010: 3301 adds r3, #1
200012: 6013 str r3, [r2, #0]
200014: e7fb b.n 20000e <thread1+0x2>
200016: bf00 nop
200018: 00200030 eoreq r0, r0, r0, lsr r0
0020001c <thread2>:
20001c: 4a02 ldr r2, [pc, #8] ; (200028 <thread2+0xc>)
20001e: 6813 ldr r3, [r2, #0]
200020: 3301 adds r3, #1
200022: 6013 str r3, [r2, #0]
200024: e7fb b.n 20001e <thread2+0x2>
200026: bf00 nop
200028: 0020002c eoreq r0, r0, ip, lsr #32
Disassembly of section .bss:
0020002c <ctr2>:
20002c: 00000000 andeq r0, r0, r0
00200030 <ctr1>:
200030: 00000000 andeq r0, r0, r0
Run Code Online (Sandbox Code Playgroud)
这与一些虚假代码相关联以演示某些内容。如果您反汇编该对象,它会从 00000000 开始,并且循环从那里开始进行某种对齐。而且它们相对于彼此应该保持静止。它们的整体对齐方式受到前面的代码的影响。使用 -save-temps 我们可以看到编译器的输出。
.cpu cortex-m7
...
.text
.align 1
.p2align 2,,3
.global thread1
.arch armv7e-m
.syntax unified
.thumb
.thumb_func
.fpu softvfp
.type thread1, %function
thread1:
@ Volatile: function does not return.
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 0, uses_anonymous_args = 0
@ link register save eliminated.
ldr r2, .L4
.L2:
ldr r3, [r2]
adds r3, r3, #1
str r3, [r2]
b .L2
.L5:
.align 2
.L4:
.word ctr1
.size thread1, .-thread1
.align 1
.p2align 2,,3
.global thread2
.syntax unified
Run Code Online (Sandbox Code Playgroud)
它有一些 .alignments ,还有一些 .p2align ,你可以查找并使用它们(将它们撒入一些 .bytes 中,看看它们如何影响下一件事,看看它们是否/用 nops 填充或者用什么填充)。
00200000 <reset-0x8>:
200000: 20001000 andcs r1, r0, r0
200004: 00200009 eoreq r0, r0, r9
00200008 <reset>:
200008: e7fe b.n 200008 <reset>
...
0020000c <thread1>:
Run Code Online (Sandbox Code Playgroud)
这...
可能是一个 nop 填充,以使 thread1 在单字边界上对齐。让我们尝试一下。
.thumb
.word 0x20001000
.word reset
.thumb_func
reset: b reset
nop
Run Code Online (Sandbox Code Playgroud)
给出
00200000 <reset-0x8>:
200000: 20001000 andcs r1, r0, r0
200004: 00200009 eoreq r0, r0, r9
00200008 <reset>:
200008: e7fe b.n 200008 <reset>
20000a: 46c0 nop ; (mov r8, r8)
0020000c <thread1>:
Run Code Online (Sandbox Code Playgroud)
相同的对齐方式。
20000e: 6813 ldr r3, [r2, #0]
200010: 3301 adds r3, #1
200012: 6013 str r3, [r2, #0]
200014: e7fb b.n 20000e <thread1+0x2>
20001e: 6813 ldr r3, [r2, #0]
200020: 3301 adds r3, #1
200022: 6013 str r3, [r2, #0]
200024: e7fb b.n 20001e <thread2+0x2>
Run Code Online (Sandbox Code Playgroud)
这两个循环可能具有相同的对齐方式并不是故意的。
也许我们想换个词来说。
volatile unsigned int ctr1,ctr2;
asm ("nop");
void thread1(void)
{
Run Code Online (Sandbox Code Playgroud)
这是多么糟糕啊,现在有一定比例的人感到畏缩;另一部分人则认为“你也可以那样做!哇!”
20000a: 46c0 nop ; (mov r8, r8)
20000c: bf00 nop
20000e: bf00 nop
00200010 <thread1>:
200010: 4a02 ldr r2, [pc, #8] ; (20001c <thread1+0xc>)
200012: 6813 ldr r3, [r2, #0]
200014: 3301 adds r3, #1
200016: 6013 str r3, [r2, #0]
200018: e7fb b.n 200012 <thread1+0x2>
20001a: bf00 nop
20001c: 00200034 eoreq r0, r0, r4, lsr r0
00200020 <thread2>:
200020: 4a02 ldr r2, [pc, #8] ; (20002c <thread2+0xc>)
200022: 6813 ldr r3, [r2, #0]
200024: 3301 adds r3, #1
200026: 6013 str r3, [r2, #0]
200028: e7fb b.n 200022 <thread2+0x2>
20002a: bf00 nop
20002c: 00200030 eoreq r0, r0, r0, lsr r0
Run Code Online (Sandbox Code Playgroud)
虽然工作了,但还是比我想要的要多。
void thread1(void)
{
asm ("nop");
while(1)
{
ctr1++;
}
}
void thread2(void)
{
while(1)
{
ctr2++;
}
}
Run Code Online (Sandbox Code Playgroud)
那行得通。
0020000c <thread1>:
20000c: bf00 nop
20000e: 4a02 ldr r2, [pc, #8] ; (200018 <thread1+0xc>)
200010: 6813 ldr r3, [r2, #0]
200012: 3301 adds r3, #1
200014: 6013 str r3, [r2, #0]
200016: e7fb b.n 200010 <thread1+0x4>
200018: 00200030 eoreq r0, r0, r0, lsr r0
0020001c <thread2>:
20001c: 4a02 ldr r2, [pc, #8] ; (200028 <thread2+0xc>)
20001e: 6813 ldr r3, [r2, #0]
200020: 3301 adds r3, #1
200022: 6013 str r3, [r2, #0]
200024: e7fb b.n 20001e <thread2+0x2>
200026: bf00 nop
200028: 0020002c eoreq r0, r0, ip, lsr #32
Run Code Online (Sandbox Code Playgroud)
把它从0x20000e推到0x200010,这也许是我想要的对齐方式,至少我可以移动它一点。它确实在执行路径中总体上添加了一条额外的指令,但是......对齐了一个循环。
对另一个做同样的事情,现在:
200010: 6813 ldr r3, [r2, #0]
200012: 3301 adds r3, #1
200014: 6013 str r3, [r2, #0]
200016: e7fb b.n 200010 <thread1+0x4>
200020: 6813 ldr r3, [r2, #0]
200022: 3301 adds r3, #1
200024: 6013 str r3, [r2, #0]
200026: e7fb b.n 200020 <thread2+0x4>
Run Code Online (Sandbox Code Playgroud)
我已经改变了他们的对齐方式。丑陋的?是的,但是它比从头开始编写 asm 或转换为 asm 更难看吗?有争议的。但您可以看到,链接到这些函数前面的任何代码更改都需要重新调整它们。这会变得非常痛苦。
volatile unsigned int ctr1,ctr2;
asm (".balign 0x10; .word 0,0,0");
void thread1(void)
{
asm ("nop");
while(1)
{
ctr1++;
}
}
void thread2(void)
{
asm ("nop");
while(1)
{
ctr2++;
}
}
Run Code Online (Sandbox Code Playgroud)
只要我们不更改这些函数并且不更改编译器或编译器选项,这应该就可以工作,基本上,如果编译器不断生成相同的机器代码,那么这个 hack 将使它们保持一致。是的,它并不优雅,但如果它有效,那是不是很愚蠢?
0020001c <thread1>:
20001c: bf00 nop
20001e: 4a02 ldr r2, [pc, #8] ; (200028 <thread1+0xc>)
200020: 6813 ldr r3, [r2, #0]
200022: 3301 adds r3, #1
200024: 6013 str r3, [r2, #0]
200026: e7fb b.n 200020 <thread1+0x4>
200028: 00200040 eoreq r0, r0, r0, asr #32
0020002c <thread2>:
20002c: bf00 nop
20002e: 4a02 ldr r2, [pc, #8] ; (200038 <thread2+0xc>)
200030: 6813 ldr r3, [r2, #0]
200032: 3301 adds r3, #1
200034: 6013 str r3, [r2, #0]
200036: e7fb b.n 200030 <thread2+0x4>
200038: 0020003c eoreq r0, r0, ip, lsr r0
Run Code Online (Sandbox Code Playgroud)
我删除的 1000 行/字符中包含哪些内容。基本上,您受编译器或针对您的特定平台的手动调整汇编语言的支配,周围有一些装甲,以免让工具弄乱您的对齐。例如,如果你正在尝试做一些计算空闲时间的事情,那么你会想要手动调整汇编......或者实际上“它有一个愚蠢的管道”人们会突然出现并告诉你使用计时器反而。他们是对的。
尝试一种尺寸适合所有手动调整......只会失败。
归档时间: |
|
查看次数: |
345 次 |
最近记录: |