从多个插槽 (Xeon Scalable) 上的所有内核到共享缓存行的存储争用导致系统变得迟钝

Contention for stores to a shared cache line from all cores on multiple sockets (Xeon Scalable) is causing system to become sluggish

提问人:Vern 提问时间:11/1/2023 最后编辑:Peter CordesVern 更新时间:11/1/2023 访问量:130

问:

#include <glibmm/thread.h>
#include <sys/sysinfo.h>
#include <stdio.h>

void threadLoop(int *PtrCounter)
{

    struct timespec sleep = {0};

    while (1)
    {
        *PtrCounter += 1; // #1: commenting this fixes the slow responsiveness
        //clock_nanosleep(CLOCK_MONOTONIC, 0, &sleep, NULL); // #2 uncommenting this fixes the slow responsiveness
        // sched_yield(); // #3 uncommenting this does nothing (still get slow responsiveness)
    }
}

int counter = 0;

void ExitHandler(int signum)
{
    printf("counter=%d\n", counter);
    exit(0);
}

int main(int argc, char *argv[])
{
    int numThreads = get_nprocs();
    int i;

    printf("using %d threads... (Ctrl-C to stop)\n", numThreads);

    signal(SIGINT, ExitHandler);

    Glib::Thread *threadArray[numThreads];

    for (i=0; i<numThreads; i++)
    {
        threadArray[i] = Glib::Thread::create(std::bind(&threadLoop, &counter));
    }

    // never returns
    for (i=0; i<numThreads; i++)
    {
        threadArray[i]->join();
    }
}

在没有优化的情况下编译,因此 ASM 会按照源代码所说的去做(从每个线程加载和存储):counter

g++ `pkg-config --libs --cflags glibmm-2.4` threads.cpp

我在 Dell PowerEdge R760 上的 Fedora 38、内核 6.5.8-200 上运行此代码(Xeon Platinum 8480+,256G 内存,get_nprocs() 返回 224)

当我运行此代码时,系统变得非常无响应(对鼠标点击和键盘按下的响应速度很慢)。在 firefox 中导航变得难以忍受。为什么会这样?

您的第一反应可能是这是可怕的代码,因为 *PtrCounter 不受任何形式的同步保护,而您是对的。我最初使用 __atomic_add_fetch() 进行增量,我看到了同样的缓慢响应。为了得到最小的可重复的例子,我删除了原子的东西。所以是的,它会错误地计算,但我认为这有助于证明这个问题。

你的下一个反应可能是,“你当然会变得迟钝,你正在占用CPU”。是的,我正在占用 cpu,但这本身并不会导致这种迟钝。正如评论中的“#1”所说,如果我注释掉“*PtrCounter +=1”,迟钝就会消失,在这种情况下,我仍然会占用 cpu(从顶部看)。

另一件事是,如果我在其他系统(更旧的 8 cpu intel,或更新的 128 cpu amd)上运行此代码,我不会感到迟钝。我邀请您在系统上尝试代码。除非,它是一个较新的高 cpu 数英特尔,我敢打赌它工作得很好,你不会遇到迟钝。

您可以在代码中看到 #2 和 #3 注释。不知道该怎么做,但我认为它足够有趣。

那么,这种迟钝的根源是什么呢?我知道有很多争用是不好的,会导致大量的缓存抖动,并且会降低算法的性能,但我不认为它应该影响操作系统的响应能力。 操作系统不应该强制非自愿的上下文切换和处理中断,无论这些算法是否遇到争用?

C++ Linux 多线程 CPU 架构 NUMA

评论

3赞 Richard Critten 11/1/2023
int counter = 0;正在多个线程上修改,没有同步。没有解释迟钝,而是把代码放进了UB的土地上。
2赞 Yksisarvinen 11/1/2023
你有三种不同的未定义行为。如果你注释掉递增,你就会减少到一个(但这同样糟糕)。任何推理都将纯粹基于编译器实现,在看到它决定用这段代码做什么之后。
5赞 user4581301 11/1/2023
我删除了原子的东西避免这样做。人们会锁定唾手可得的果实,而忽略你真正想要解决的问题。一般来说,保持示例较小,但尽可能接近原始示例,并且如果您注入了新的错误......你会得到处理新错误的答案,所以不要做任何你知道是错误的/愚蠢的事情来简化这个例子。
2赞 Craig Estey 11/1/2023
随着您递增相同的内存位置。在 x86 上,它有一个紧凑的内存模型。CPU 是缓存监听和在每个 CPU 缓存的 L1 之间反弹数据。您受内存总线限制并淹没了内存控制器。在成为瓶颈之前,它可能只能处理相对较少的线程。即使没有原子,也有很多序列化正在进行。从 1 个线程开始,逐渐增加线程数。您可能会在 8 个线程左右后滚落*PtrCounter +=1
3赞 Vern 11/1/2023
哦,很酷,看起来有代码可以监控 UPI 性能。明天当我再次访问系统时,我会看看这个。github.com/intel/pcm

答: 暂无答案