是什么导致线程执行速度比串行情况慢?

What is causing the threads to execute slower than the serial case?

我有一个简单的函数可以计算“n”个数字的总和。

我正在尝试使用线程并行实现求和。代码如下,

void Add(double &sum, const int startIndex, const int endIndex)
{
    sum = 0.0;
    for (int i = startIndex; i < endIndex; i++)
    {
        sum = sum + 0.1;
    }    
}
int main()
{
    int n = 100'000'000;

    double sum1;
    double sum2;

    std::thread t1(Add, std::ref(sum1), 0, n / 2);
    std::thread t2(Add, std::ref(sum2), n / 2, n);

    t1.join();
    t2.join();

    std::cout << "sum: " << sum1 + sum2 << std::endl;

    // double serialSum;
    // Add(serialSum, 0, n);
    // std::cout << "sum: " << serialSum << std::endl;

    return 0;
}

但是,代码运行速度比串行版本慢很多。如果我修改函数,使其 接受总和变量,那么我将获得所需的加速(将近 2 倍)。

我在网上阅读了一些资源,但似乎都表明变量不能被多线程访问。我不明白为什么这个例子会这样。

有人可以澄清我的错误吗?

这里的问题是硬件。

你可能知道 CPU 有缓存来加速操作。这些缓存比内存快很多倍,但它们以称为缓存行的单元工作。在您的系统上可能是 64 字节。您的 2 个双打每个都是 8 字节大,并且几乎肯定会最终位于堆栈上相同的 64 字节区域中。 cpu 中的每个核心通常都有自己的 L1 缓存,而更大的缓存可能会在核心之间共享。

现在,当一个线程访问sum1时,核心会将相关的缓存行加载到缓存中。当第二个线程访问 sum2 时,另一个核心尝试将相同的缓存行加载到它自己的缓存中。 x86 架构非常好地试图帮助您,它会要求第一个缓存移交缓存行,以便两个线程始终看到相同的数据。

因此,虽然您有 2 个单独的变量,但它们位于同一缓存行中,并且在每次访问时,缓存行都会从一个核心反弹到另一个核心并返回。这是一个相当慢的操作。这就是所谓的虚假分享。

因此您需要在 sum1sum2 之间进行一些分隔,以加快这项工作。请参阅 std::hardware_destructive_interference_size 了解您需要达到的距离。

另一种可能更简单的方法是修改辅助函数以使用局部变量:

void Add(double &sum, const int startIndex, const int endIndex)
{
    double t = 0.0;
    for (int i = startIndex; i < endIndex; i++)
    {
        t = t + 0.1;
    }    
    sum = t;        
}

您仍然存在虚假共享,这两个线程将争夺对 sum1sum2 的访问权限。但现在它只发生一次,变得无关紧要了。