membarrier 手册页中的示例在 x86 中毫无意义吗?
Is the example in the membarrier man page pointless in x86?
也许只有我一个人是这样,但是 membarrier
的 man 2
页面中的示例似乎毫无意义。
基本上,membarrier()
是一个异步内存屏障,给定两个协调的代码片段(让我们调用 fast path 和 slow path) 允许您将屏障的所有硬件成本移动到 慢速路径,而只留下 快速路径编译器障碍1。有几种不同的方法可以实现 membarrier
行为,例如向每个涉及的处理器发送 IPI 或等待每个处理器上的代码 运行 取消调度 - 但具体的实现细节是这里很重要。
现在,这是 man page 中给出的示例转换:
原代码
static volatile int a, b;
static void
fast_path(void)
{
int read_a, read_b;
read_b = b;
asm volatile ("mfence" : : : "memory");
read_a = a;
/* read_b == 1 implies read_a == 1. */
if (read_b == 1 && read_a == 0)
abort();
}
static void
slow_path(void)
{
a = 1;
asm volatile ("mfence" : : : "memory");
b = 1;
}
转换后的代码
(省略了一些系统调用和初始化样板文件)
static volatile int a, b;
static void
fast_path(void)
{
int read_a, read_b;
read_b = b;
asm volatile ("" : : : "memory");
read_a = a;
/* read_b == 1 implies read_a == 1. */
if (read_b == 1 && read_a == 0)
abort();
}
static void
slow_path(void)
{
a = 1;
membarrier(MEMBARRIER_CMD_SHARED, 0);
b = 1;
}
这里 slow_path
正在执行两次写入(a
,然后是 b
),由屏障分隔,fast_path
正在执行两次读取(b
, 然后 a
) 也被屏障隔开。
但是,x86 内存模型不允许加载-加载或存储-存储重新排序!据我所知,在这种情况下根本不需要 membarrier()
并且原始代码中也不需要 mfence
。似乎简单的编译器障碍在两个地方就足够了2.
一个实际有意义的例子,IMO,应该有一个存储后跟一个加载,在 快速路径.
中由障碍分隔
我是不是漏掉了什么?
1 编译器屏障阻止编译器在其上移动加载或存储(并且根据实现可能会强制将某些寄存器值存入内存),但不会发出任何类型的原子操作或内存栅栏,因此请避免这些指令中固有的通常数量级的减速。
2 当然,在较弱的平台上,可能会发生加载-加载重新排序,该示例可能有意义,但该示例明确为 x86,而 membarrier()
仅在 x86 上实现。
你是对的。 在 x86 上,membarrier()
的这种特殊用法完全没有用。事实上,这个确切的例子是第一个在Intel SDM中给出的用来说明x86内存排序规则的例子:
Intel SDM Vol. 3 §8.2.3.2 Neither Loads Nor Stores Are Reordered with Like Operations
我正在提交对此联机帮助页的修复以改为使用 Dekker 示例。参见 https://lkml.org/lkml/2017/9/18/779
顺便说一下,membarrier 系统调用不是特定于 x86 的,而是现在在大多数 Linux 架构上实现。
感谢反馈!
马修
也许只有我一个人是这样,但是 membarrier
的 man 2
页面中的示例似乎毫无意义。
基本上,membarrier()
是一个异步内存屏障,给定两个协调的代码片段(让我们调用 fast path 和 slow path) 允许您将屏障的所有硬件成本移动到 慢速路径,而只留下 快速路径编译器障碍1。有几种不同的方法可以实现 membarrier
行为,例如向每个涉及的处理器发送 IPI 或等待每个处理器上的代码 运行 取消调度 - 但具体的实现细节是这里很重要。
现在,这是 man page 中给出的示例转换:
原代码
static volatile int a, b;
static void
fast_path(void)
{
int read_a, read_b;
read_b = b;
asm volatile ("mfence" : : : "memory");
read_a = a;
/* read_b == 1 implies read_a == 1. */
if (read_b == 1 && read_a == 0)
abort();
}
static void
slow_path(void)
{
a = 1;
asm volatile ("mfence" : : : "memory");
b = 1;
}
转换后的代码
(省略了一些系统调用和初始化样板文件)
static volatile int a, b;
static void
fast_path(void)
{
int read_a, read_b;
read_b = b;
asm volatile ("" : : : "memory");
read_a = a;
/* read_b == 1 implies read_a == 1. */
if (read_b == 1 && read_a == 0)
abort();
}
static void
slow_path(void)
{
a = 1;
membarrier(MEMBARRIER_CMD_SHARED, 0);
b = 1;
}
这里 slow_path
正在执行两次写入(a
,然后是 b
),由屏障分隔,fast_path
正在执行两次读取(b
, 然后 a
) 也被屏障隔开。
但是,x86 内存模型不允许加载-加载或存储-存储重新排序!据我所知,在这种情况下根本不需要 membarrier()
并且原始代码中也不需要 mfence
。似乎简单的编译器障碍在两个地方就足够了2.
一个实际有意义的例子,IMO,应该有一个存储后跟一个加载,在 快速路径.
中由障碍分隔我是不是漏掉了什么?
1 编译器屏障阻止编译器在其上移动加载或存储(并且根据实现可能会强制将某些寄存器值存入内存),但不会发出任何类型的原子操作或内存栅栏,因此请避免这些指令中固有的通常数量级的减速。
2 当然,在较弱的平台上,可能会发生加载-加载重新排序,该示例可能有意义,但该示例明确为 x86,而 membarrier()
仅在 x86 上实现。
你是对的。 在 x86 上,membarrier()
的这种特殊用法完全没有用。事实上,这个确切的例子是第一个在Intel SDM中给出的用来说明x86内存排序规则的例子:
Intel SDM Vol. 3 §8.2.3.2 Neither Loads Nor Stores Are Reordered with Like Operations
我正在提交对此联机帮助页的修复以改为使用 Dekker 示例。参见 https://lkml.org/lkml/2017/9/18/779
顺便说一下,membarrier 系统调用不是特定于 x86 的,而是现在在大多数 Linux 架构上实现。
感谢反馈!
马修