我试图在每个可用的硬件线程上运行一个简单的任务(获取当前处理器的 x2APIC ID)。我编写了以下代码来执行此操作,该代码可在我对其进行测试的机器上运行(有关完整的 MWE,请参阅 here,可在 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 替换 cpuid 函数中的 asm volatile 语句也解决了这个问题,但我不知道这有什么必要。

我的问题
  • 在调用 cpuid 之前插入一个获取栅栏,在调用 CPUID 之后插入一个释放栅栏,难道不应该足以阻止编译器执行内存重新排序吗?
  • 为什么将 r4 转换为 std::atomic<uint32_t> 不起作用?为什么将前三个输出存储到 r1r2r3 而不是忽略它们会导致程序运行?
  • 如何使用最少的同步来正确编写循环?
  • 最佳答案

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

    有关详细信息,请参阅此答案:https://stackoverflow.com/a/14449998/2527797

    关于c++ - 令人困惑的内存重新排序行为,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/25368037/

    10-12 22:34