【问题标题】:How do C/C++ optimizations affect volatile variables?C/C++ 优化如何影响 volatile 变量?
【发布时间】:2017-05-10 15:09:15
【问题描述】:

我遇到了一些代码问题(见下文)。当所有优化都关闭(-O0)时,代码的行为与我预期的一样,但是当我打开优化时,代码不会像我预期的那样执行。所以我在这里发帖是想看看是否有人看到优化开启时代码运行方式不同(不正确)的原因。让我解释一下代码在做什么......

TPM0->CNT 是一个定时器寄存器,属于 volatile uint32_t 类型。当 TPM0->CNT 达到最大值并翻转时,将产生中断。在此中断中,sg_CpuCount 会增加自上次中断以来经过的定时器周期数。

GetCpuCount 函数的工作是返回瞬时计数(即 sg_CpuCount + TPM0->CNT),但是因为中断可能随时发生,并且计时器不会停止,因此在读取/计算时必须小心。读取这两个组件,然后再次测试第一个组件以查看它是否更改。如果它改变了它已知的在第一次和第二次读取之间触发的中断并重复该过程,但是如果它们相同,则已知没有发生中断并返回添加的组件。

逻辑合理吗?优化会产生错误的代码吗?有什么见解吗?提前致谢!

编辑:我应该提到优化开启时我注意到的行为。基本上,对 GetCpuCount 的后续调用并不总是像关闭优化时那样单调递增。也就是说,稍后对 GetCpuCount 的调用可能会返回比先前调用更小的数字 - 这应该是不可能的。

edit2:我应该提到运行此代码的 micro 是 32 位架构(我使用的是 64 位值)。我仍然认为应该没问题,但这是一个值得考虑的问题。

static volatile int64_t sg_CpuCount;

void TPM0_IRQHandler(void)
{
   TPM0->SC |= TPM_SC_TOF(1); //clear interrupt flag
   sg_CpuCount += CPU_CLOCK / TICKRATE_HZ;
}

int64_t GetCpuCount(void)
{
   for (;;) {
      int64_t tmpCpuCount = sg_CpuCount;
      uint32_t tmpCnt = TPM0->CNT;
      if (tmpCpuCount == sg_CpuCount) {
         return tmpCpuCount + tmpCnt;
      }
   }
}

edit3:添加 asm

优化(-0s)

GetCpuCount:
.LFB36:
    .loc 2 80 0
    .cfi_startproc
    @ args = 0, pretend = 0, frame = 0
    @ frame_needed = 0, uses_anonymous_args = 0
    push    {r4, r5, r6, lr}
    .cfi_def_cfa_offset 16
    .cfi_offset 4, -16
    .cfi_offset 5, -12
    .cfi_offset 6, -8
    .cfi_offset 14, -4
.LBB2:
    .loc 2 82 0
    ldr r4, .L20
    .loc 2 83 0
    ldr r6, .L20+4
.L18:
    //int64_t tmpCpuCount = sg_CpuCount;
    ldr r0, [r4]
    ldr r1, [r4, #4]
    //uint32_t tmpCnt = TPM0->CNT;
    ldr r5, [r6, #4]
    //if (tmpCpuCount == sg_CpuCount) {
    ldr r2, [r4]
    ldr r3, [r4, #4]
    cmp r0, r2
    bne .L18
    cmp r1, r3
    bne .L18
    //return tmpCpuCount + tmpCnt;
    movs    r0, r5
    movs    r1, #0
.LBE2:
    .loc 2 88 0
    @ sp needed
.LBB3:
    .loc 2 85 0
    adds    r0, r0, r2
    adcs    r1, r1, r3
.LBE3:
    .loc 2 88 0
    pop {r4, r5, r6, pc}
.L21:
    .align  2
.L20:
    .word   sg_CpuCount
    .word   1073971200
    .cfi_endproc

未优化 (-00)

GetCpuCount:
.LFB36:
    .loc 2 80 0
    .cfi_startproc
    @ args = 0, pretend = 0, frame = 16
    @ frame_needed = 1, uses_anonymous_args = 0
    push    {r4, r7, lr}
    .cfi_def_cfa_offset 12
    .cfi_offset 4, -12
    .cfi_offset 7, -8
    .cfi_offset 14, -4
    sub sp, sp, #20
    .cfi_def_cfa_offset 32
    add r7, sp, #0
    .cfi_def_cfa_register 7
.L19:
.LBB2:
    //int64_t tmpCpuCount = sg_CpuCount;
    ldr r3, .L21
    ldr r4, [r3, #4]
    ldr r3, [r3]
    str r3, [r7, #8]
    str r4, [r7, #12]
    //uint32_t tmpCnt = TPM0->CNT;
    ldr r3, .L21+4
    ldr r3, [r3, #4]
    str r3, [r7, #4]
    //if (tmpCpuCount == sg_CpuCount) {
    ldr r3, .L21
    ldr r4, [r3, #4]
    ldr r3, [r3]
    ldr r0, [r7, #8]
    cmp r0, r3
    bne .L19
    ldr r0, [r7, #12]
    cmp r0, r4
    bne .L19
    //return tmpCpuCount + tmpCnt;
    ldr r3, [r7, #4]
    movs    r1, r3
    movs    r3, #0
    movs    r2, r3
    ldr r3, [r7, #8]
    ldr r4, [r7, #12]
    adds    r3, r3, r1
    adcs    r4, r4, r2
.LBE2:
    .loc 2 88 0
    movs    r0, r3
    movs    r1, r4
    mov sp, r7
    add sp, sp, #20
    @ sp needed
    pop {r4, r7, pc}
.L22:
    .align  2
.L21:
    .word   sg_CpuCount
    .word   1073971200
    .cfi_endproc

【问题讨论】:

  • 为了完整起见,请添加main函数。
  • 你真的想要 volatile 而不是 std::atomic
  • 这实际上是 C 代码而不是 C++ 代码。另外我不确定热 std::atomic 是否与中断交互。
  • 需要 sg_CpuCount 成为int64_t 吗?我会尝试将其改为volatile sig_atomic_t
  • main 函数对此并没有真正的帮助。我想我用您可能正在寻找的其他信息编辑了帖子。

标签: c gcc optimization


【解决方案1】:

如果您使用的是 32 位机器,则不能保证 64 位操作是原子的。由于优化,时间可能会发生变化。并且永远不要在禁用中断的情况下调用该函数。

static volatile uint32_t sg_CpuCount;

void TPM0_IRQHandler(void)
{
   TPM0->SC |= TPM_SC_TOF(1); //clear interrupt flag
   sg_CpuCount++; //count interrupts
}

int64_t GetCpuCount(void)
{
   for (;;) {
      uint32_t tmpCpuCount = sg_CpuCount;
      uint32_t tmpCnt = TPM0->CNT;
      if (tmpCpuCount == sg_CpuCount) {
         return (int64_t)tmpCpuCount * CPU_CLOCK / TICKRATE_HZ + tmpCnt;
      }
   }
} 

【讨论】:

  • 我意识到 64 位操作不会是原子的,但这就是为什么 GetCpuCount 函数中用于验证 sg_CpuCount 的 if 语句与第一次读取相比没有变化的原因。
【解决方案2】:

在分析 ASM 的优化和非优化代码后,我注意到一个主要区别。一个先加载 64 位 int 高字,另一个先加载低字。所以我相信 user5329483 是正确的,我会解释可能导致问题的确切顺序......

read sg_CpuCount low
read sg_CpuCount high
read TPM0->CNT
read sg_CpuCount low
***interrupt***
read sg_CpuCount high
compare first sg_CpuCount with second sg_CpuCount (equal!!!!!)

由于中断中sg_CpuCount的增量可能只改变低位字,上述序列未能检测到中断发生。

未优化的代码如下:

read sg_CpuCount high
read sg_CpuCount low
read TPM0->CNT
read sg_CpuCount high
***interrupt***
read sg_CpuCount low
compare first sg_CpuCount with second sg_CpuCount (**NOT** equal!!!!!)

此代码正确检测到发生的中断。

所以优化确实改变了代码 - 但是从编译器的角度来看,两个代码生成同样有效。所以答案是要么通过将 64 位 int 拆分为 2 个 32 位变量来明确定义操作顺序,要么更好地使用 user5329483 的建议代码。

编辑:不幸的是,32 位不足以运行足够长的时间。无论如何,这个问题现在已经很好理解了。

【讨论】:

  • 对此不太确定。在您的第一个示例中,您没有检测到中断,但该函数仍然返回一个有效值:来自中断之前的sg_CpuCount + TPM0->CNT。 TPM0 是什么类型的?
  • TPM0 是许多 CPU 寄存器的结构 CNT 是 volatile uint32_t。是的,您是正确的,第一个示例可以返回有效值,即使它未能检测到中断 - 但并非总是如此......我相信这里还有一个组件在起作用 - 中断延迟。我发现可以在中断执行之前将 TPM0->CNT 读取为 0 或 1(即翻转)。我的问题只发生在这种情况下。因此,在第一个示例中,读取 CNT(为 0 或 1)并在中断之前读取 sg_CpuCount low,但 sg_CpuCount 不正确,因为它尚未更新。
  • 你看到 GetCpuCount() 中设置的定时器溢出标志了吗?它应该立即触发一个中断并且应该在 ISR 中被清除,所以你看不到它。如果是,为什么?全局禁用中断?
  • 我没有调查标志在 GetCpuCount 函数中是否可观察。但是在标志置位后,cpu 可能会在中断过程开始之前再执行 1 条指令。通过在 if 语句之前放置一条 asm("nop") 指令来解决所有问题。我正在寻找有关中断延迟时间的文档,以确定一个 nop 是否能保证正确操作。
猜你喜欢
  • 1970-01-01
  • 2018-11-09
  • 2012-05-10
  • 2017-08-09
  • 1970-01-01
  • 2015-03-24
  • 1970-01-01
  • 2011-04-05
  • 2017-09-01
相关资源
最近更新 更多