3

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

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

图像中的测试结果

我的 CPU 是intel core i7-9750h. “core0”和“core1”是物理内核,“core2”和“core3”等也是。MSVC 14.24 被用作编译器。

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

Test2 很容易解释。x和在y不同的缓存行中一样,在 2 个物理内核上运行可以获得 2 倍的性能提升(在此可以忽略在单个内核上运行 2 个线程时的上下文切换成本),并且在具有 SMT 的一个内核上运行效率低于 2物理内核,受coffee-lake的吞吐量限制(相信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 快,这反驳了上述理论。

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

4

2 回答 2

1

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

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

在不同的物理内核上运行时,瓶颈是内存(存储延迟和错误共享),而 Release 总是更快,因为它包含的内存访问明显少于关键部分的 Debug,如Debug 程序集(godbolt)Release 程序集(godbolt)所示. Release 中退休的总指令也更少,这加强了这一点。我昨天在 Visual Studio 中找到的程序集似乎不正确。

于 2020-05-21T07:49:35.030 回答
-1

这可以用超线程来解释。作为 2 个超线程内核共享的内核不会像 2 个完全独立的内核那样获得两倍的吞吐量。相反,您可能会获得 1.7 倍的性能。

事实上,如果我没看错的话,你的处理器有 6 个内核和 12 个线程,并且 core0/core1 是同一个底层内核上的 2 个线程。

事实上,如果你在脑海中想象超线程是如何工作的,两个独立的内核交错工作,那就不足为奇了。

于 2020-05-20T19:47:56.437 回答