当 2 个线程共享同一物理内核时,具有错误共享的易失性增量在发布中的运行速度比在调试中慢
volatile increments with false sharing run slower in release than in debug when 2 threads are sharing the same physical core
我正在尝试测试错误共享对性能的影响。测试代码如下:
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 很容易解释。由于x
和y
位于不同的缓存行中,因此在 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个独立内核的工作交错,这并不奇怪。
- 易失性sig_atomic_t的内存安全性
- C++易失性:保证 32 位访问?
- 避免易失性和非易失性成员函数的代码重复
- 当 2 个线程共享同一物理内核时,具有错误共享的易失性增量在发布中的运行速度比在调试中慢
- 如何访问常量易失性 std::array?
- 为什么在 C++20 中弃用易失性?
- 根据 MSVC,具有易失性成员的结构不再是 POD
- 是否允许编译器优化掉局部易失性变量
- 访问共享内存而不使用易失性、std::atomic、信号量、互斥锁和自旋锁
- 如何避免对无锁程序使用易失性?
- C++:易失性实例中的易失性成员函数 - 将数组分配给指针是无效的转换?
- g++ 6.3,avx 内联函数上的 Kahan 求和用易失性关键字进行序列化
- 是什么让这种易失性打破了结构的指针算法?
- 如果不需要易失性,为什么 std::atomic 方法会提供易失性重载
- *(易失性无符号整数 *) 的含义 0x00 = 0x00;
- 使用易失性 c 字符串和 std::cout
- 易失性结构 = 结构不可能,为什么?
- 如何强制 GCC 以线性方式转换易失性内联程序集语句
- 如何使用易失性与2D共享内存
- 对 WinAPI 线程之间的共享变量使用易失性