是什么导致线程执行速度比串行情况慢?
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 个单独的变量,但它们位于同一缓存行中,并且在每次访问时,缓存行都会从一个核心反弹到另一个核心并返回。这是一个相当慢的操作。这就是所谓的虚假分享。
因此您需要在 sum1
和 sum2
之间进行一些分隔,以加快这项工作。请参阅 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;
}
您仍然存在虚假共享,这两个线程将争夺对 sum1
和 sum2
的访问权限。但现在它只发生一次,变得无关紧要了。
我有一个简单的函数可以计算“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 个单独的变量,但它们位于同一缓存行中,并且在每次访问时,缓存行都会从一个核心反弹到另一个核心并返回。这是一个相当慢的操作。这就是所谓的虚假分享。
因此您需要在 sum1
和 sum2
之间进行一些分隔,以加快这项工作。请参阅 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;
}
您仍然存在虚假共享,这两个线程将争夺对 sum1
和 sum2
的访问权限。但现在它只发生一次,变得无关紧要了。