当 2 个线程共享同一物理内核时,具有错误共享的易失性增量在发布中的运行速度比在调试中慢

volatile increments with false sharing run slower in release than in debug when 2 threads are sharing the same physical core

本文关键字:共享 易失性 布中 速度 调试 运行 有错误 线程 内核      更新时间:2023-10-16

我正在尝试测试错误共享对性能的影响。测试代码如下:

constexpr uint64_t loop = 1000000000;
struct no_padding_struct {
no_padding_struct() :x(0), y(0) {}
uint64_t x;
uint64_t y;
};
struct padding_struct {
padding_struct() :x(0), y(0) {}
uint64_t x;
char padding[64];
uint64_t y;
};
alignas(64) volatile no_padding_struct n;
alignas(64) volatile padding_struct p;
constexpr core_a = 0;
constexpr core_b = 1;
void func(volatile uint64_t* addr, uint64_t b, uint64_t mask) {
SetThreadAffinityMask(GetCurrentThread(), mask);
for (uint64_t i = 0; i < loop; ++i) {
*addr += b;
}
}
void test1(uint64_t a, uint64_t b) {
thread t1{ func, &n.x, a, 1<<core_a };
thread t2{ func, &n.y, b, 1<<core_b };
t1.join();
t2.join();
}
void test2(uint64_t a, uint64_t b) {
thread t1{ func, &p.x, a, 1<<core_a  };
thread t2{ func, &p.y, b, 1<<core_b  };
t1.join();
t2.join();
}
int main() {
uint64_t a, b;
cin >> a >> b;

auto start = std::chrono::system_clock::now();
//test1(a, b);
//test2(a, b);
auto end = std::chrono::system_clock::now();
cout << (end - start).count();
}

结果大致如下:

x86                                         x64             
cores    test1           test2              cores       test1        test2  
debug  release  debug  release               debug release  debug  release
0-0      4.0s   2.8s     4.0s   2.8s        0-0       2.8s  2.8s     2.8s   2.8s
0-1      5.6s   6.1s     3.0s   1.5s        0-1       4.2s  7.8s     2.1s   1.5s
0-2      6.2s   1.8s     2.0s   1.4s        0-2       3.5s  2.0s     1.4s   1.4s
0-3      6.2s   1.8s     2.0s   1.4s        0-3       3.5s  2.0s     1.4s   1.4s
0-5      6.5s   1.8s     2.0s   1.4s        0-5       3.5s  2.0s     1.4s   1.4s

图像中的测试结果

我的 CPUintel core i7-9750h."core0"和"core1"是物理核心,"core2"和"core3"等也是如此。MSVC 14.24 用作编译器。

记录的时间是几次运行中最佳分数的近似值,因为有大量的后台任务。我认为这是公平的,因为结果可以清楚地分成几组,0.1s~0.3s的误差不会影响这种划分。

Test2 很容易解释。由于xy位于不同的缓存行中,因此在 2 个物理内核上运行可以获得 2 倍的性能提升(在单个内核上运行 2 个线程时上下文切换的成本在这里可以忽略不计(,并且在一个内核上运行 SMT 的效率低于 2 个物理内核,受到咖啡湖吞吐量的限制(相信 Ryzen 可以做得稍微好一点(, 并且比临时多线程更有效。这里似乎64位模式更有效。

但是test1的结果让我感到困惑。 首先,在调试模式下,0-2、0-3 和 0-5 比 0-0 慢,这是有道理的。我解释说,某些数据反复从 L1 移动到 L3 和 L3 移动到 L1,因为缓存必须在 2 个内核之间保持一致,而在单个内核上运行时,它将始终保持在 L1 中。但这个理论与0-1对总是最慢的事实相冲突。从技术上讲,两个线程应共享相同的 L1 缓存。0-1 的运行速度应该是 0-0 的 2 倍。

其次,在发布模式下,0-2、0-3和0-5比0-0快,这反驳了上述理论。

最后,在 64 位和 32 位模式下,0-1 在release中的运行速度比在debug中慢。这是我最无法理解的。我阅读了生成的汇编代码,没有发现任何有用的东西。

@PeterCordes 感谢您的分析和建议。 我终于使用 Vtune 分析了该程序,事实证明您的期望是正确的。

在同一内核的 SMT 线程上运行时,machine_clear会消耗大量时间,而且在发布中比在调试中更严重。这在 32 位和 64 位模式下都会发生。

当在不同的物理内核上运行时,瓶颈是内存(存储延迟和错误共享(,并且发布总是更快,因为它在关键部分包含的内存访问明显少于调试,如调试程序集(godbolt(和发布程序集(godbolt(所示。在发布中停用的总指令也较少,这加强了这一点。看来我昨天在Visual Studio中找到的程序集不正确。

这可以通过超线程来解释。 作为 2 个超线程内核共享的内核不会像 2 个完全独立的内核那样获得双倍的整个内核。 相反,您可能会获得大约 1.7 倍的性能。

事实上,如果我正确阅读了所有这些内容,您的处理器有 6 个内核和 12 个线程,而 core0/core1 是同一底层内核上的 2 个线程。

事实上,如果你在脑海中想象超线程是如何工作的,2个独立内核的工作交错,这并不奇怪。