【问题标题】:Can two sequential assignment statements in C be executed on hardware out of order?C中的两个顺序赋值语句可以在硬件上乱序执行吗?
【发布时间】:2019-04-22 08:28:11
【问题描述】:

给定以下 C 程序:

static char vals[ 2 ] = {0, 0};

int main() {

char *a = &vals[0];
char *b = &vals[1];

while( 1 ) {

    SOME_STUFF()

    // non-atomic operations in critical section
    if( SOME_CONDITION() )
        {
        *a = 1;
        *b = 2;
        }
    else
        {
        *a = 0;
        *b = 0;
        }


    SOME_OTHER_STUFF()

    }

return 0;
}

int async_interrupt( void ) {

PRINT( a );
PRINT( b );
}

硬件是否有可能首先将值2 加载到内存位置&vals[1] 中,以便执行中断例程并查看vals[1] == 2vals[0] == 0

如果这是可能的,任何会导致这种情况的加载/存储操作的描述将不胜感激。

编辑 1:在代码部分添加了更多上下文。不幸的是,我没有编译后的源代码。

【问题讨论】:

  • 是的;程序也可以优化为int main() { return 0; },因为它没有可观察到的行为
  • 发布您询问的“中断例程”示例会改善问题。一般来说,如果中断例程试图访问ab,这将是未定义的行为,所以这个问题可能没有实际意义。此外,某些平台为中断例程提供比标准 C 更强的保证。
  • 如果中断例程访问了vals[0]vals[1],则也是UB。 (ab 是具有自动存储功能的本地人,因此没有好的方法可以通过中断来获取它们。不确定它们的意义。)

标签: c interrupt memory-model signal-handling


【解决方案1】:

C 不能直接在硬件上运行。必须先编译。

未定义行为的细节(如非原子变量的非同步读取)完全取决于实现(包括编译器中的编译时重新排序,并且取决于目标 CPU 架构,该 ISA 的运行时重新排序规则) .

非原子变量的读/写在 C 或 C++ 中不被视为可观察到的副作用,因此它们可以被优化掉并重新排序,直至保留整个程序的行为(除非程序有未定义的行为——在这种情况下优化可以做任何事情,即使编译器不能“看到”它在编译时会有 UB。)

另见https://preshing.com/20120625/memory-ordering-at-compile-time/

【讨论】:

  • 正如我在您回答下的线程中评论的那样:C 仅保证因果关系适用于单个线程。 OP 想要阅读 vals[0]vals[1] 来自中断处理程序,它从主线程异步运行,因此 C 不保证如果它在没有同步的情况下读取 vals[0..1] 并且没有该数组是 _Atomic 会发现什么。 _Atomic 的全部意义在于在这种非同步单线程执行的情况下保证因果关系。您基于非原子 C 变量的因果关系争论的答案充其量是误导性的。
  • @EdwinBuck:正要在你的回答下的线程中回复 - 我认为你错过了 OP 确实说过“这样一个中断例程可以......”。所以,是的,我们正在谈论的案例超出了 C 的 as-if 规则要求保留任何代码转换的范围。由于您删除了答案,我猜您现在在问题中注意到了这一点:)
  • Peter,如果我们假设 C 被编译和链接,使得机器代码没有被优化,你是否知道任何可能以某种方式重新排序这些值的加载/存储的硬件细微差别,例如中断看到第二个分配,但不是第一个?
  • @curiousguy:我认为他的意思是“基本块”,或者只是 asm 的“块”/块,就像整个函数的定义一样。请注意,埃德温在我回复后删除了他的误导性答案(但不是评论),所以我认为我们不需要再进一步挑选。
  • 已删除评论以帮助澄清
【解决方案2】:

是的,这是可能的,因为编译器可能会按照Peter's answer 中的描述重新排序这些语句。

但是,您可能仍然想知道另一半:硬件 可以做什么。假设您的商店按照您在源代码中显示的顺序最终进入程序集1,如果在运行此代码的同一 CPU 上发生中断,您将在中断中看到一切都井井有条。也就是说,在中断处理程序中,您永远不会看到第二个存储已完成,但第一个没有。您将看到的唯一场景是两者都未完成、都已完成或第一个已完成而第二个未完成。

如果涉及多个核心,并且中断可能运行在不同的核心上,那么您只需经典的跨线程共享场景,是否是中断 - 而其他核心可以观察到的情况取决于硬件内存模型。例如,在相对强排序的 x86 上,您总是会按顺序观察存储,而在排序更弱的 ARM 或 POWER 内存模型上,您可能会看到存储无序。

然而,一般来说,CPU 可能会进行各种重新排序:您在中断处理程序中看到的排序是一种特殊情况,CPU 将在处理中断时恢复顺序执行的外观。对于线程观察自己的存储的任何情况都是如此。但是,当 不同 线程观察到存储时 - 会发生什么取决于硬件内存模型,这在架构之间会有很大差异。


1 还假设它们单独显示 - 没有什么可以阻止智能编译器注意到您正在分配内存中的相邻值,从而将两个存储转换为一个更宽的。 Most compilers 至少在某些情况下可以做到这一点。

【讨论】:

  • 我相信这就是我正在寻找的“从中断处理程序中,你永远不会看到第二个存储完成,但第一个没有。”。但是,由于我对架构的有限理解,我认为由于一些硬件细微差别,可能会首先发生第二个商店。也许事实并非如此。
  • @leo - 一个实际中断有问题的代码的中断(在有问题的代码运行的同一个 CPU 上运行)将始终看到商店的一致视图,就像运行的代码一样CPU 将按源顺序查看它的存储。如果涉及另一个 CPU,则在该第二个 CPU并发 上运行的中断代码(或任何真正的代码)与执行存储的代码可能会看到它们乱序,具体取决于硬件内存模型。跨度>
  • @curiousguy - 我不太确定你指的是什么。如果使用多个线程并且有多个 CPU,并且不涉及中断,那么您肯定还会看到不一致的存储顺序,具体取决于硬件。也许您可以澄清您的问题或创建一个单独的问题。
  • @curiousguy - 我正在考虑按照 OP 的规定发生中断并在他们的 main() 方法中显示的情况。所以我假设有两个执行上下文:运行存储的普通用户上下文(单线程)和中断上下文,它可能发生在与另一个上下文相同的 CPU 或不同的 CPU 上。当您询问多个线程时,您的意思是运行存储方法的多个线程吗?多个重叠中断?我不清楚,因为最初的问题显然不涉及线程。
  • @curiousguy - 不,我不是想说(我知道的系统会将信号传递给唯一的线程,如果只有一个线程) .事实上,我试图或多或少地回避 signal 处理语义的整个问题,并按要求回答问题,并附上任何必要的警告。请注意,OP 甚至没有谈论信号,而只是“中断”。我不知道他们使用的是什么类型的系统,也不知道这些中断是如何传递的。最初我认为中断实际上会中断有问题的代码,我这样写了我的答案(如“你会没事的”)。
猜你喜欢
  • 2013-05-18
  • 1970-01-01
  • 1970-01-01
  • 2011-01-12
  • 2013-10-17
  • 2018-02-18
  • 2019-07-23
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多