我试图在每个可用的硬件线程上运行一个简单的任务(获取当前处理器的 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>
不起作用?为什么将前三个输出存储到 r1
、 r2
和 r3
而不是忽略它们会导致程序运行? 最佳答案
我已经在启用优化 (-O) 的情况下重现了该问题。你怀疑编译器优化是对的。 CPUID 本身充当完整的内存屏障(对于处理器);但它是编译器生成代码而不调用循环中的 cpuid
函数,因为它威胁它作为一个常量函数。 asm volatile
阻止编译器进行这样的优化,说它有副作用。
有关详细信息,请参阅此答案:https://stackoverflow.com/a/14449998/2527797
关于c++ - 令人困惑的内存重新排序行为,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/25368037/