【问题标题】:May a compiler remove a local variable in favor of multiple memory accesses?编译器可以删除局部变量以支持多次内存访问吗?
【发布时间】:2026-02-15 18:20:03
【问题描述】:

我们讨论了如何确保某些共享(不受信任)内存区域中的数据只被访问一次,复制到本地内存,然后从那里检查和处理。环境是一个嵌入式多核 µC,带有用于 IPC 的共享 RAM,代码是用 C99 编写的。 目前我们基本上是做Type local_copy = *(Type*)shared_memory_pointer;,之后只对local_copy进行操作。

现在一位同事提出了一个问题,是否允许编译器不执行复制到本地内存,而是直接在下面访问shared_memory_pointer 处的数据,这(理论上)将允许操作数据使用时。

编译器有可能做到这一点吗?如果是这样,我们如何确保它不会发生?如果不是,请详细说明。

谢谢大家!

编辑:有问题的核心上没有操作系统,它是一个裸机系统。

【问题讨论】:

  • instead access the data at shared_memory_pointer directly in the following, which (in theory) would allow for manipulation 如何使用副本操作数据? then only operate on local_copy afterwards 无论编译器做什么,都不能改变你的代码——如果你对一个副本进行操作并更改一个副本,它不会影响原始代码。对共享指针的访问可能是读取,而不是写入。
  • @KamilCuk 他询问编译器是否可以优化掉副本。我认为,添加一些 volatile voodoo 可以防止这种情况发生。
  • @KamilCuk 我认为允许编译器重新加载shared_memory_pointer,只要它可以证明两者之间的任何操作都不会影响存储在shared_memory_pointer中的值
  • 我认为您必须使用一些原子操作来加载值。喜欢atomic_load。这将防止编译器重新排序操作或拆分负载。
  • Type local_copy = *(Type*)shared_memory_pointer; 之类的代码是代码异味,强烈表明您的程序中有未定义的行为或其他类型相关的错误。

标签: c embedded


【解决方案1】:

是否允许编译器不执行复制到本地内存,而是直接访问 shared_memory_pointer 处的数据

是的,允许编译器这样做。您可以强制读取的唯一方案是通过volatile 合格访问。在您的情况下,局部变量和强制转换都应该是 volatile 合格的。

但是请注意...

  • volatile 不能解决重入问题。您需要一个互斥锁、临界区或类似方法来阻止代码出现竞争条件错误。使用您的操作系统提供的方法。
  • Type local_copy = *(Type*)shared_memory_pointer; 是非常可疑的代码,表明您的程序中有未定义的行为或与类型相关的错误。像这样的狂野类型双关语可能会导致不对齐、不正确的严格指针别名优化、丢弃的限定符等等——所有这些都是未定义的行为。此外,如果您选择了正确的类型,则无需首先进行转换。

【讨论】:

  • 感谢您的回答!毫无疑问,关于演员阵容中的 volatile ,但在您看来,为什么局部变量应该是 volatile 限定的?这不只是在任何时候都假设外部更改,即每次访问都从内存重新加载吗?本地副本不需要这样做。我认为在这种情况下类型双关语是安全的,如果我错了,请纠正我:shared_memory_pointer 是对齐的void*Type 只是为共享内存中的原始字节提供一些结构。它永远不会在这一行之外的任何地方访问。
  • @Gileos 您只需要确保访问发生在您希望访问发生的时间点。不允许编译器重新排序volatile 访问。至于底层硬件发生了什么,那就是另一回事了。例如,如果您的 CPU 有数据缓存,您将希望确保该变量不会被缓存。
  • 对于指向的数据,void* 无关紧要,它只是一个中间人。如果编译器不知道实际存储数据的类型,您可以键入双关,以防您每次都使用相同的类型。如果它作为不同的类型访问,那么 UB。
  • 知道了。在我们的例子中没有违反严格的别名规则,所以双关语在这里是安全的。
  • 其要点是共享内存用于单向IPC。另一个核心使用相同的结构 Type 并将其写入该内存,在我们的核心上,我们读取该结构并验证这些字段是否对请求有意义。对于其他操作模式,共享内存的使用方式不同,因此无法将内存直接解释为Type*
【解决方案2】:

我们讨论了如何确保某些共享中的数据 (不受信任的)内存区域只访问一次,复制到本地 内存,然后从那里检查和处理。

memcpy 与二进制信号量一起使用,而不是指针双关语。

freeRTOS 示例: 它确保了

if(xSemaphoreTake( xSemaphoreSharedVariable, (TickType_t) 10 ) == pdTRUE) 
{
    taskENTER_CRITICAL(); //make sure that the shared resource will not be changed during not atomic access
    memcpy(&local_copy, shared_memory_pointer, sizeof(local_copy);
    taskEXIT_CRITICAL();
}
else
{
    /* the shared resource was alredsy read */
    /* do something */
}

【讨论】:

  • 如果 OP 的编译器支持,memcpy 与类型双关语有何根本不同?
  • 问题不在于如何复制数据,而在于如何防止编译器在taskEXIT_CRITICAL() 之后重新排序加载(不管它是什么......)
  • 我认为这无关紧要。如果类型双关语解决方案有效,则对齐正确
  • freertos.org/taskENTER_CRITICAL_taskEXIT_CRITICAL.html 仅提及禁用/启用中断。它没有说明“退出”处的内存屏障。我想那里一定有一些隐含的障碍
  • 编译器可能不使用“严格的别名规则”。因此,可以为这个特定的实现定义这个负载。防止多次重新加载它的问题