我注意到一种我无法解释的行为:函数的执行时间似乎取决于它在闪存中的位置.我使用的是STM32F746NGH微控制器(基于ARM-CORCEL M7)和STM32CubeIDE(GCC针对ARM的编译器).

Here are my tests:

我对SysTick计数器进行了初始化,以触发具有固定周期T=1ms的中断.在中断处理程序中,我在两个线程之间切换(就像RTOS一样):让我们将它们命名为Thread1和Thread2.

每个线程只是递增一个变量.

以下是这两个线程的代码:


uint32_t ctr1, ctr2;

void thread1(void)
{
    while(1)
    {
        ctr1++;
    }
}


void thread2(void)
{
    while(1)
    {
        ctr2++;
    }
}

在监控这些变量时,我注意到ctr2的增量比ctr1快得多.

使用此代码:线程1的S地址为0x08000418,线程2的S的地址为0x0800042C.

Then, I tried to put another function in memory before thread1: let's name it thread0.

所以我的新代码是:


uint32_t ctr0, ctr1, ctr2;


void thread0(void)
{
    while(1)
    {
        ctr0++;
    }
}

void thread1(void)
    {
    while(1)
    {
        ctr1++;
    }
}


void thread2(void)
{
    while(1)
    {
        ctr2++;
    }
}

使用这个新代码:线程0‘S地址是0x08000418(带有先前代码的线程1’S位置),线程1‘S地址是0x0800042C(带有先前代码的线程2’S位置),并且线程2‘S地址是0x08000440.

我可以看到,ctr1和ctr2以相同的速率递增,而ctr0的递增速度比这两个慢得多.

Finally, I've tried with 20 different threads.每个线程递增一个变量(类似于上面共享的代码).我观察到变量以两种不同的速率递增:Speed1和Speed2;Speed1低于Speed2.

Thread Address Speed
Thread0 0x08000418 speed1
Thread1 0x0800042C speed2
Thread2 0x08000440 speed2
Thread3 0x08000454 speed1
Thread4 0x08000468 speed2
Thread5 0x0800047C speed2
Thread6 0x08000490 speed2
Thread7 0x080004A4 speed2
Thread8 0x080004B8 speed1
Thread9 0x080004CC speed2
Thread10 0x080004E0 speed2
Thread11 0x080004F4 speed1
Thread12 0x08000508 speed2
Thread13 0x0800051C speed2
Thread14 0x08000530 speed2
Thread15 0x08000544 speed2
Thread16 0x08000558 speed1
Thread17 0x0800056C speed2
Thread18 0x08000580 speed2
Thread19 0x08000594 speed1

我还签入了程序集中,所有线程都有类似的代码(相同的代码大小、相同的指令和相同数量的指令);因此它与代码本身无关. 每个线程有10条指令,因此代码大小为20字节(每条指令的宽度为2字节).它对应于每个线程的内存地址之间的增量(20=0x14).

Here is the code of a thread (as said, other threads have a similar code):

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

正如您在表中看到的,似乎有一种模式:一个线程的速度为1,两个线程的速度为2,一个线程的速度为1,4个线程的速度为2,然后重新启动模式.

I don't know if it is relevant/related, but in the Cortex M7 reference manual, I've found this section about the flash memory:

指令预取 每个闪存读取操作提供256位,代表32位至16位的8条指令 16位指令按程序启动.因此,在顺序代码的情况下,在 执行先前的指令行读取需要至少8个CPU周期.预取 允许读取闪存中顺序的下一行指令,而 当前指令行是由CPU请求的.预取可以通过设置 FLASH_ACR寄存器的PRFTEN位.如果至少有一个等待状态是 需要访问闪存.当代码不是顺序的(分支)时,指令可以 既不存在于当前使用的指令行中,也不存在于预取指令中 排队.在这种情况下(未命中),周期数方面的惩罚至少等于 等待状态数. 自适应实时存储器

但我已经查看了表:256位块中完全包含的函数可以具有Speed1或Speed2,对于两个256位块之间共享的函数也可以具有相同的值.

我不明白这一行为的原因可能是什么.

EDIT 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");
}

Os_tick和sytick_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;

线程存储在数组中,并以循环链接列表的方式连接.

EDIT2:附加信息:以下是我的时钟设置:

锁相源:晶体振荡器@25 MHz

SYSCLK=PLL_CLK=216 MHz

闪存等待状态=7WS,如STM32数据手册中所建议.

推荐答案

你基本上自己回答了这个问题.您将在ARM和MIPS等高性能核心上看到这种情况,而在x86等高开销核心上不会看到这种情况(并不是说它没有发生).

现在,公平地说,您可能会看到其他效果,并在RTOS中运行其他开销.但我可以很容易地演示这一点,而不需要任何这些东西,裸机,没有中断,等等.只需核心一次做一件事.

我们都可以直观地想象缓存会发生什么,第一次可能会有缓存未命中,这可能会导致第一次和第二次之间的巨大延迟(取决于系统,不一定很大),假设它在缓存中速度更快.同样,如果您将循环对齐到缓存线的末尾附近,以便循环本身或a/下一个预取进入第二个缓存线.使第一个循环持平 更久.对于我所说的FETCH行也是如此,但更糟糕的是,因为它们本身并不会第二次变得更快.现在,一些分支预测指标将有所帮助.

分支预测不一定是超级智能的寻找逻辑,其试图对指令进行解码并提前查看可能导致该分支发生的指令/结果,然后作为结果开始提取.相反,当它执行地址时,更有可能是有一个很小的地址缓存 第一次和该地址导致或可能导致获取时,它们会将其添加到这个短列表中,当您接近该地址时(即使您自行修改代码),它将抛出一个预取.现在,如果你不得不这样做,这可能会造成伤害.但实际情况是,分支预测只是启动预取的时间比预取早了几个时钟(这很好,但既不神奇也不复杂的逻辑).

我在Nucleo-F767ZI上,因为它是我手头上的.它将是与你芯片中相同的皮质-M7.我们不能保证整个芯片都是一样的(这会使芯片无法记住).但随着STM32芯片的使用量超过我多年来的统计,从一端到另一端的整个范围内,Corcorp-M7基础设施将更相似而不是不同.CORCEL-M7 st具有更大的灵活性,您将看到,虽然它们仍然支持其classic 的0x08000000地址,但ITCM地址是0x00200000,这就是您应该用于链接这些部分的地址.显然,你可以也应该在你的芯片上try 这一点.您应该会看到类似的结果.

我没有使用来自st或其他任何人的任何代码,我的所有代码都是从ARM和ST文档编写的.切换到板载水晶以获得 更可靠的UART参考时钟.设置UART以打印结果.更新的部件更好,但在许多供应商的部件上,很长一段时间内,零等待状态意味着闪存以系统速度的一半运行.当你提高系统的速度时,你习惯了,可能仍然需要添加等待状态.使时钟最大化并不会让你的系统运行得更快,你仍然受到闪存速度限制的限制(我们已经很长时间没有被处理器限制了)内核可以快速地运行指令,但随后不得不等待更多的时钟来等待指令.或外围设备(如果外围设备有另一个外围设备).我没有查过你的芯片文档,这一份绝对有一张电力表,要系统时钟等待状态表.欢迎您打卡并正确等待状态,并重复这些实验.它应该与仅以8 MHz运行并添加那些等待状态相同或相当.基本上,我们欢迎您最大限度地使用时钟,但您仍然会看到我演示的问题,您可能有其他问题,但演示这个问题是微不足道的.

我认为这是一个派对把戏,在实际的派对上可能行不通,但你可以迷惑或娱乐你的同事.请注意,当您看到更原始的皮质-ms具有半字或字大小的提取时,您必须有一条提取线,您可能无法看到这一点.不过,这是M7.

预取单元

The 预取单元 (PFU) provides:

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

只是一个简单的计数循环(不是统一的语法).排列得很整齐.

我使用的是系统计时器,我可以演示一下,DWT计时器给出了相同的结果.诚然,一些芯片供应商在系统杆上加了一个除数.好的,好的,文件显示除以8,但我认为这是一个长期存在的打字错误……

从sytick开始,判断Dwt,如果不是Dwt的8倍,则返回到sytick.

从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 

这看起来像是某种缓存.在其他STM32芯片上,缓存闪存是无法关闭的.在这一部分和其他STM32皮质-m7,你可以.事实上,DOC说明(FLASH_ACR寄存器)ART和预取都被禁用.有趣的是,这些数字看起来很可疑,如果缓存关闭,第一个循环有什么不同?是球杆的问题吗?

使用DWT

00001029 
00001006 
00001006 
00001006 

ARM文档谈到了分支预测之类的内容,而我认为它写得不好(如果你试图通过搜索的方式).看起来BTAC在默认情况下是启用的,我们可以在ACTLR寄存器中关闭它(分支目标地址缓存,缓存一些地址及其目的地以供预取).

0000400F 
0000400F 
0000400F 
0000400F 

好多了.

00200100 <TEST>:
  200100:   b430        push    {r4, r5}
  200102:   680c        ldr r4, [r1, #0]

这个芯片看不到两个地址之间的性能差异,这很奇怪.这是另一项有待研究的实验.

所以64位取数是4个16位或2个32位.假设该总线为32位或64位宽.因此,对于上面的情况,我们假设在0x100处有一个FETCH,它开始通过管道运行这些FETCH,然后继续获取下一行,让它排队.

.balign 0x100
nop
/* r0 count */
/* r1 timer address */

在这里放一个NOP或其他东西来更改我们简单测试的对齐方式.

00200102 <TEST>:
  200102:   b430        push    {r4, r5}
  200104:   680c        ldr r4, [r1, #0]


00005002 
00005002 
00005002 
00005002 

这就对了.完全相同的机器代码,相同的芯片,相同的系统,相同的一切,除了完全相同的机器代码在不同的对齐上.

注意:在启用BTAC的情况下,您仍然可以获得不同的执行时间.

0002010 0002004 0002004 0002004

如果我们添加另一个NOP,并将这两条指令放在FETCH行的中间,会怎么样?

00200104 <TEST>:
  200104:   b430        push    {r4, r5}
  200106:   680c        ldr r4, [r1, #0]

00004003 
00004003 
00004003 
00004003 

嗯,嗯

00200106 <TEST>:
  200106:   b430        push    {r4, r5}
  200108:   680c        ldr r4, [r1, #0]

00004003 
00004003 
00004003 
00004003

有意思的.我将切换到SRAM,在许多MCU上,闪存速度比SRAM慢,即使是在"零等待状态".使用SRAM允许我执行一些self 修改代码,每次运行不止一次测试.

第一个数字是被测代码前面的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 

从0x20002000到0x20002002,从4x2=80x2008到0x200A.8字节是64位.因此,对FETCH行的处理清楚地给出了相同机器代码的两个结果.我错了,上面的两条指令都是4个字节,这是取数行的四分之一?您可能会认为,如果在64位对齐和一次过后,它会导致它变慢,那么您几乎会预期其他未对齐,64个字节中的4个字节也会变慢.我将停止试图分析它,我无法访问模拟核心,如果我访问了,我无论如何也不能谈论它.

我们看到,至少在这个芯片上,闪存和缓存在闪存上处于零等待状态时,在闪存上是相同的,而不是更慢.MCU中的闪烁变得更好了.

哈哈,是的是的...

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>

循环不是在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>

如果我在循环中插入NOP

00005002 
00005002 
00005002 
00005002

这是有道理的.

将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 

并不是每个循环都有这个问题.

环路中的两个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 

(如果处理器如此敏感或可能如此敏感,请考虑基准测试的价值.)

您将在实际应用程序中看到这些性能差异的变化.在完全不相关的函数中添加或删除代码可能会对事物所在位置的整个二进制代码产生级联效应.一些循环将是位置敏感的,而另一些则不是.循环包裹循环,内部有多个循环的循环可能会相互抵消或放大问题.

0x2004/0x1006=199.8%.比我在某个地方 comments 的两位数还多.

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>

在启用BTAC的情况下,我们以前看到过.

00001011 
00001006 
00001006 
00001006 

FLASH_ACR寄存器中的PRFTEN不变.

艺术加速器开着,没有变化.

因此,我继续使用FLASH_ACR寄存器.看起来我们被处理器限制了.或者,某个地方正在进行某些缓存.

现在,例如,当您升级到RTOS时.不一定是在这个核心中,但添加指令高速缓存会有所帮助,但请记住,无论有没有高速缓存,您仍在提取相同的行,并具有相同的提取行边界问题.后备内存有时可能会更快,但对齐问题仍然存在(在您可以首先检测到的系统上).增加了一个我们在Corcorp-m上没有的MMU,他们定义了内存区域,这样您就不必使用缓存来告诉系统什么是非缓存外设什么是指令内存什么是数据内存.MMU会增加其自身的性能影响,并且通常有多种将虚拟地址空间映射到物理地址的方法,但是如何映射可能/将会有性能问题等.您的RTO可能会遇到更多问题,但如果严格地使用相同的机器码和不同的对齐来获取两个特定的数字,您可能会陷入每个循环的FETCH数的简单问题.


简短的回答:

你基本上已经完成了所有的工作,并找到了答案.

CORCEL-M7的FETCH为64位.当FETCH行和管道确定需要向后分支并执行FETCH操作时,您所在的位置会影响每个循环的总FETCH数.如果保留相同的机器码并在地址空间中移动该机器码,则某些循环在每个循环中可能会有一个额外的FETCH.零等待状态并不意味着零时钟,获取不是空闲的,并且获取涉及的不是所有系统内存.当FETCH到达时,相对于管道准备接受它的时间,分支也会影响循环性能.循环在代码大小中的大小决定了额外的FETCH惩罚,每个循环三个FETCH有时是四个,而十个FETCH有时是11个将具有不同的相对命中率.20%,10%,等等. 它与只读存储器/闪存中的对齐没有特别的关系,但一般来说,如果你要在SRAM中运行该代码,你应该也能找到两个执行时间.

注意:不要假设任何其他核心与皮质-M7完全相同,其他皮质-ms要么没有足够的提取能力来看到这一点.不同的管道,等等.有些可能需要更多的系统时钟来做相同的事情.较旧的核心与较旧的闪存技术相匹配,这些系统设计可能会显示,例如,从闪存运行和从SRAM运行,相同的代码具有不同的性能.

在广泛的STM32系列中,还有一些芯片不能禁用的闪存缓存和预取,这使得像这样的性能分析变得更加困难.尽管其他品牌可以购买相同的内核,但请注意,内核有编译时间选项,ARM内核只是该芯片的一部分,其余的逻辑是某人,没有两家公司被假设拥有相同的IP,当然,其中一些IP是芯片公司而不是购买的.例如,你应该会在所有基于皮质M7的芯片上看到这一点,但确切的差异可能会有所不同.

在具有RTOS影响的RTOS上运行也可能导致性能问题.对于这个特定的ARM核心,可以为相同的代码生成两种不同的执行时间,我相信您已经发现了这一点.在此基础上,您可能会发现其他性能问题.

在您的例子中,单循环时间根据对齐而变化,所以您不是测量X个循环,而是测量Y时间内有多少个循环,相同的情况下,每个循环的时钟越多,每秒的计数就越少.您不像我那样严格地测量时间(Michael Abrash:汇编语言的禅宗),所以您的中断延迟和开销可能会在这里产生影响.就我个人而言,我认为从你的结果和这个核心是如何工作的,等等,对于每个编译来说,这两个任务的任务切换时间至少应该相等.我猜我只是发布了一份免责声明,你可能看到了上述之外的其他东西(或者你可能没有看到上面的内容,如果是这样的话,请让我知道,我会删除这个答案).

C++相关问答推荐

生成C代码时自动复制/生成' tmwtypes.h '依赖项

常数函数指针优化

如何将不同长度的位转换成字节数组?

如何判断宏参数是否为C语言中的整型文字

特定闪存扇区的内存别名

变量>;-1如何在C中准确求值?

如何调试LD_PRELOAD库中的构造函数?

当我更改编译优化时,相同的C代码以不同的方式运行

C语言编译阶段与翻译阶段的关系

二进制计算器与gmp

C中函数类型的前向声明

我的程序在收到SIGUSR1信号以从PAUSE()继续程序时总是崩溃()

判断X宏的空性

错误Cygwin_Except::Open_stackdupfile:正在转储堆栈跟踪是什么?

这个计算C中阶乘的函数正确吗?

<;unistd.h>;和<;sys/unistd.h>;之间有什么区别?

如何将另一个数组添加到集合中,特别是字符串?

C语言中浮点数的取整方式浮点数尾数超过23位时如何取整剩余部分

Fscanf打印除退出C代码为1的程序外的所有内容

窗口消息处理函数以某种方式更改了应保持不变的 int 变量的值