【问题标题】:Confusing Memory Reordering Behavior令人困惑的内存重新排序行为
【发布时间】:2014-10-11 15:29:57
【问题描述】:

我正在尝试在每个可用的硬件线程上运行一个简单的任务(获取当前处理器的 x2APIC ID)。我编写了以下代码来执行此操作,它可以在我测试过的机器上运行(请参阅here 了解完整的 MWE,可在 Linux 上编译为 C++11 代码)。

void print_x2apic_id()
{
        uint32_t r1, r2, r3, r4;
        std::tie(r1, r2, r3, r4) = cpuid(11, 0);
        std::cout << r4 << std::endl;
}

int main()
{
        const auto _ = std::ignore;
        auto nprocs = ::sysconf(_SC_NPROCESSORS_ONLN);
        auto set = ::cpu_set_t{};
        std::cout << "Processors online: " << nprocs << std::endl;

        for (auto i = 0; i != nprocs; ++i) {
                CPU_SET(i, &set);
                check(::sched_setaffinity(0, sizeof(::cpu_set_t), &set));
                CPU_CLR(i, &set);
                print_x2apic_id();
        }
}

在一台机器上输出(使用 g++ 编译时,版本 4.9.0):

0
2
4
6
32
34
36
38

每次迭代都会打印一个不同的 x2APIC ID,因此一切都按预期工作。现在是问题开始的地方。我用以下代码替换了对print_x2apic_id 的调用:

uint32_t r4;
std::tie(_, _, _, r4) = cpuid(11, 0);
std::cout << r4 << std::endl;

这会导致循环的每次迭代都打印相同的 ID:

36
36
36
36
36
36
36
36

我对发生的事情的猜测是编译器注意到对cpuid 的调用不依赖于循环迭代(即使它确实如此)。然后编译器通过在循环外提升对 CPUID 的调用来“优化”代码。为了解决这个问题,我将r4 转换为原子:

std::atomic<uint32_t> r4;
std::tie(_, _, _, r4) = cpuid(11, 0);
std::cout << r4 << std::endl;

这未能解决问题。令人惊讶的是,这确实解决了问题:

std::atomic<uint32_t> r1;
uint32_t r2, r3, r4;
std::tie(r1, r2, r3, r4) = cpuid(11, 0);
std::cout << r4 << std::endl;

...好吧,现在我很困惑。

编辑:asm volatile 替换cpuid 函数中的asm 语句也解决了这个问题,但我不明白这应该是什么必要的。

我的问题

  1. 不应该在调用 cpuid 之前插入获取栅栏并在调用 CPUID 之后插入释放栅栏足以阻止编译器执行内存重新排序吗?
  2. 为什么将r4 转换为std::atomic&lt;uint32_t&gt; 不起作用?为什么将前三个输出存储到r1r2r3 而不是忽略它们会导致程序运行?
  3. 如何正确编写循环,使用最少的同步?

【问题讨论】:

  • @Anton 是的,可能是std::ignore 导致了问题。但我不明白这是怎么可能的。

标签: c++ multithreading c++11 atomic


【解决方案1】:

我已经在启用优化 (-O) 的情况下重现了该问题。您怀疑编译器优化是正确的。 CPUID 本身用作完整的内存屏障(对于处理器);但它是编译器生成代码而不调用循环中的cpuid 函数,因为它威胁它是一个常量函数。 asm volatile 阻止编译器进行这样的优化,说它有副作用。

详情请看这个答案:https://stackoverflow.com/a/14449998/2527797

【讨论】:

  • 我应该提到我已经尝试过了,但问题仍然存在。
  • cpuid 函数中的asm 语句替换为asm volatile 也可以解决此问题。但我不认为这应该是必要的。
  • 啊!这意味着您对编译器优化是相当正确的。我会尝试修改答案。
  • 谢谢,这是有道理的。这是答案中的相关引用:“ volatile 关键字告诉编译器不允许移动此程序集块。例如,如果编译器确定每次调用中的输入值都相同,它可能会被提升出循环. 我不确定在什么条件下编译器会决定它对程序集有足够的了解以尝试优化其位置,但 volatile 关键字完全抑制了这一点。”
  • 可能编译器可以证明它们是本地的并且对其他线程不可见,因此没有副作用。
猜你喜欢
  • 1970-01-01
  • 2011-11-09
  • 1970-01-01
  • 1970-01-01
  • 2018-05-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-10-22
相关资源
最近更新 更多