Lou*_*Lou 6 c assembly gcc atomicity c11
与此线程相关,我有一个 FIFO,它应该可以跨 Cortex M4 上的不同中断工作。
头部索引必须是
移动 FIFO 头的函数看起来与此类似(在实际代码中也有检查头是否溢出,但这是主要思想):
#include <stdatomic.h>
#include <stdint.h>
#define FIFO_LEN 1024
extern _Atomic int32_t _head;
int32_t acquire_head(void)
{
while (1)
{
int32_t old_h = atomic_load(&_head);
int32_t new_h = (old_h + 1) & (FIFO_LEN - 1);
if (atomic_compare_exchange_strong(&_head, &old_h, new_h))
{
return old_h;
}
}
}
Run Code Online (Sandbox Code Playgroud)
GCC 会将其编译为:
acquire_head:
ldr r2, .L8
.L2:
// int32_t old_h = atomic_load(&_head);
dmb ish
ldr r1, [r2]
dmb ish
// int32_t new_h = (old_h + 1) & (FIFO_LEN - 1);
adds r3, r1, #1
ubfx r3, r3, #0, #10
// if (atomic_compare_exchange_strong(&_head, &old_h, new_h))
dmb ish
.L5:
ldrex r0, [r2]
cmp r0, r1
bne .L6
strex ip, r3, [r2]
cmp ip, #0
bne .L5
.L6:
dmb ish
bne .L2
bx lr
.L8:
.word _head
Run Code Online (Sandbox Code Playgroud)
这是一个没有操作系统/线程的裸机项目。此代码用于记录 FIFO,它不是时间关键,但我不希望头部的获取对程序其余部分的延迟产生影响,所以我的问题是:
dmb吗?dmb,它会产生多少个额外的延迟周期?TL:DR 是的,与禁用中断相比,LL/SC (STREX/LDREX) 可以通过重试使原子 RMW 可中断来减少中断延迟。
这可能会牺牲产量为代价,因为很明显上禁用的ARMv7 /重新启用中断是很便宜(如可能为1或2个周期,每个周期为cpsid if/ cpsie if),特别是如果你可以无条件允许中断而不是保存旧的状态。(暂时禁用 ARM 上的中断)。
额外的吞吐量成本是:如果 LDREX/STREX 在 Cortex-M4 上比 LDR/STR 慢,cmp/bne(在成功案例中不采用),以及任何时候循环必须重试整个循环体再次运行. (重试应该是非常罕见的;只有在另一个中断处理程序中的 LL/SC 中间实际发生了中断时。)
不幸的是,像 gcc 这样的 C11 编译器没有用于单处理器系统或单线程代码的特殊模式。因此,他们不知道如何利用这样一个事实来进行代码生成,即在同一核心上运行的任何东西都会在某个点之前按程序顺序查看我们的所有操作,即使没有任何障碍。
(乱序执行和内存重新排序的主要规则是它保留了单线程或单核按程序顺序运行指令的错觉。)
dmb即使在多线程代码的多核系统上,仅由一对 ALU 指令分隔的背靠背指令也是多余的。这是一个 gcc 遗漏优化,因为当前的编译器基本上没有对原子进行优化。(最好是安全和缓慢而不是冒险变得太弱。推理、测试和调试无锁代码而不必担心可能的编译器错误已经足够困难了。)
在这种情况下,您可以通过在 之后进行屏蔽来大大简化它atomic_fetch_add,而不是使用 CAS 模拟具有较早翻转的原子添加。(那么读者也必须戴口罩,但这很便宜。)
你可以使用memory_order_relaxed. 如果您希望针对中断处理程序进行重新排序保证,请使用atomic_signal_fence强制编译时排序,而不会针对运行时重新排序设置 asm 障碍。 用户空间 POSIX 信号在同一线程内异步,与中断在同一内核内异步的方式完全相同。
// readers must also mask _head & (FIFO_LEN - 1) before use
// Uniprocessor but with an atomic RMW:
int32_t acquire_head_atomicRMW_UP(void)
{
atomic_signal_fence(memory_order_seq_cst); // zero asm instructions, just compile-time
int32_t old_h = atomic_fetch_add_explicit(&_head, 1, memory_order_relaxed);
atomic_signal_fence(memory_order_seq_cst);
int32_t new_h = (old_h + 1) & (FIFO_LEN - 1);
return new_h;
}
Run Code Online (Sandbox Code Playgroud)
@@ gcc8.2 -O3 with your same options.
acquire_head_atomicRMW:
ldr r3, .L4 @@ load the static address from a nearby literal pool
.L2:
ldrex r0, [r3]
adds r2, r0, #1
strex r1, r2, [r3]
cmp r1, #0
bne .L2 @@ LL/SC retry loop, not load + inc + CAS-with-LL/SC
adds r0, r0, #1 @@ add again: missed optimization to not reuse r2
ubfx r0, r0, #0, #10
bx lr
.L4:
.word _head
Run Code Online (Sandbox Code Playgroud)
不幸的是,我知道在 C11 或 C++11 中无法表达包含任意操作集(例如添加和掩码)的LL/SC原子 RMW,因此我们可以在循环中获取 ubfx 以及存储的部分内容到_head。不过,对于 LDREX/STREX 有特定于编译器的内在函数:ARM 中的关键部分。
这是安全的,因为_Atomic整数类型保证是具有明确定义的溢出 = 环绕行为的 2 的补码。(int32_t已经保证是 2 的补码,因为它是固定宽度类型之一,但 no-UB-wraparound 仅适用于_Atomic)。我会使用uint32_t,但我们得到相同的 asm。
ARM® Synchronization Primitives(从 2009 年开始)有一些关于管理 LDREX/STREX 的 ISA 规则的详细信息。运行 LDREX 会初始化“独占监视器”以检测其他内核(或系统中其他非 CPU 事物?我不知道)的修改。Cortex-M4 是一个单核系统。
您可以为多个 CPU 之间共享的内存设置一个全局监视器,并为标记为不可共享的内存设置一个本地监视器。该文档说“如果配置为可共享的区域未与全局监视器关联,则对该区域的 Store-Exclusive 操作始终失败,并在目标寄存器中返回 0。” 因此,如果在测试代码时STREX 似乎总是失败(因此您陷入重试循环),那可能就是问题所在。
中断确实没有中止由LDREX启动的事务。如果您正在上下文切换到另一个上下文并恢复可能在 STREX 之前停止的某些内容,那么您可能会遇到问题。clrex为此引入了ARMv6K ,否则较旧的 ARM 将使用虚拟 STREX 到虚拟位置。
请参阅何时在 ARM Cortex M7 上实际需要 CLREX?,这与我即将谈到的观点相同,即在中断情况下,当不在线程之间进行上下文切换时,通常不需要 CLREX。
(有趣的事实:关于该链接问题的最新答案指出,Cortex M7(或一般的 Cortex M?)会在中断时自动清除监视器,这意味着中断处理程序中永远不需要 clrex。下面的推理仍然适用于较旧的单个-core ARM CPU,带有不跟踪地址的监视器,与多核 CPU 不同。)
但是对于这个问题,您要切换到的始终是中断处理程序的开始。你不是在做先发制人的多任务处理。 因此,您永远无法从一个 LL/SC 重试循环的中间切换到另一个循环的中间。 只要 STREX 在您返回时在较低优先级中断中第一次失败,就可以了。
这里就是这种情况,因为更高优先级的中断只会在成功执行 STREX(或根本没有执行任何原子 RMW)后返回。
所以我认为即使clrex在分派到 C 函数之前不使用from inline asm 或从中断处理程序也可以。 该手册说数据中止异常会使监视器在体系结构上未定义,因此请确保至少在该处理程序中使用 CLREX。
如果在 LDREX 和 STREX 之间出现中断,则 LL 已将旧数据加载到寄存器中(并且可能计算了一个新值),但尚未将任何内容存储回内存,因为 STREX 尚未运行.
更高优先级的代码将 LDREX,获得相同的old_h值,然后执行成功的 STREX old_h + 1。(除非它也被打断,但这种推理是递归的)。这可能会在第一次循环中失败,但我不这么认为。即使是这样,根据我链接的 ARM 文档,我认为不会存在正确性问题。该文档提到本地监视器可以像状态机一样简单,它只跟踪 LDREX 和 STREX 指令,即使前一条指令是针对不同地址的 LDREX,也能让 STREX 成功。假设 Cortex-M4 的实现是简单的,这就是完美的。
在 CPU 已经从之前的 LDREX 进行监控的同时为同一地址运行另一个 LDREX 看起来应该没有效果。对不同地址执行独占加载会将监视器重置为打开状态,但为此它始终是相同的地址(除非您在其他代码中有其他原子?)
然后(在做一些其他事情之后),中断处理程序将返回,恢复寄存器并跳回到低优先级中断的 LL/SC 循环的中间。
回到低优先级中断,STREX 将失败,因为高优先级中断中的 STREX 重置了监控状态。这很好,我们需要它失败,因为它会存储与在 FIFO 中占据一席之地的更高优先级中断相同的值。该cmp/bne检测故障并再次运行整个循环。这次它成功了(除非再次中断),读取由更高优先级中断存储的值并存储并返回 + 1。
所以我认为我们可以在任何地方不用 CLREX 就可以离开,因为中断处理程序总是在返回到它们中断的中间之前运行到完成。他们总是从头开始。
单写版本
或者,如果没有其他方法可以修改该变量,则根本不需要原子 RMW,只需要纯原子加载,然后是新值的纯原子存储。(_Atomic为了利益或任何读者)。
或者,如果根本没有其他线程或中断接触该变量,则它不需要是_Atomic.
// If we're the only writer, and other threads can only observe:
// again using uniprocessor memory order: relaxed + signal_fence
int32_t acquire_head_separate_RW_UP(void) {
atomic_signal_fence(memory_order_seq_cst);
int32_t old_h = atomic_load_explicit(&_head, memory_order_relaxed);
int32_t new_h = (old_h + 1) & (FIFO_LEN - 1);
atomic_store_explicit(&_head, new_h, memory_order_relaxed);
atomic_signal_fence(memory_order_seq_cst);
return new_h;
}
Run Code Online (Sandbox Code Playgroud)
acquire_head_separate_RW_UP:
ldr r3, .L7
ldr r0, [r3] @@ Plain atomic load
adds r0, r0, #1
ubfx r0, r0, #0, #10 @@ zero-extend low 10 bits
str r0, [r3] @@ Plain atomic store
bx lr
Run Code Online (Sandbox Code Playgroud)
这与我们为 non-atomic 得到的 asm 相同head。
| 归档时间: |
|
| 查看次数: |
706 次 |
| 最近记录: |