下面的代码是我在C++中使用多线程的优点而运行的一个实验。给定一个数字10000000000,它计算出在1到10000000000的范围内,有多少个数字是偶数,可被5整除,可被8整除,可被10整除。
首先,它运行单线程函数,然后运行多线程函数.
然而,问题是,我得到的结果并不像预期的那样。这表明多线程根本没有任何好处。我甚至不使用互斥,而是使用多个线程。
使用的
MicrosoftVisualStudio2019社区版,C++17,x64发布版本构建与/O2优化。
#include <iostream>
#include <thread>
#include <chrono>
#include <string>
#include <vector>
#define CALC_NUMBER 10000000000ull
struct Counters {
unsigned long long int CountDivTen = 0;
unsigned long long int CountDivEight = 0;
unsigned long long int CountDivFive = 0;
unsigned long long int CountEven = 0;
};
Counters DivCounter;
// For multi-threading
std::vector<std::pair<unsigned long long, unsigned long long>> parts = {
{1, 2500000000}, {2500000001, 5000000000}, {5000000001, 7500000000},
{7500000001, 10000000000}
};
// Multi-threading counters.
std::vector<Counters> MyCounters(4);
void SingleThreaded()
{
std::chrono::high_resolution_clock::time_point StartTime =
std::chrono::high_resolution_clock::now();
for (unsigned long long x = 1; x <= CALC_NUMBER; ++x)
{
// Count the even number
if ((x % 2) == 0)
++DivCounter.CountEven;
// Count divisible by 5
if ((x % 5) == 0)
++DivCounter.CountDivFive;
// Count divisible by 8
if ((x % 8) == 0)
++DivCounter.CountDivEight;
// Count divisible by 10
if ((x % 10) == 0)
++DivCounter.CountDivTen;
}
auto elapsed = std::chrono::high_resolution_clock::now() - StartTime;
auto seconds = std::chrono::duration_cast<std::chrono::seconds>(elapsed).count();
std::cout << "Time in seconds: " << seconds << std::endl;
}
void MultiThread_Merge(int index)
{
for (unsigned long long x = parts[index].first; x <= parts[index].second; ++x)
{
// Count the even number
if ((x % 2) == 0)
++MyCounters[index].CountEven;
// Count divisible by 5
if ((x % 5) == 0)
++MyCounters[index].CountDivFive;
// Count divisible by 8
if ((x % 8) == 0)
++MyCounters[index].CountDivEight;
// Count divisible by 10
if ((x % 10) == 0)
++MyCounters[index].CountDivTen;
}
}
void DoThreadUsingMerge()
{
// Start timer
std::chrono::high_resolution_clock::time_point StartTime =
std::chrono::high_resolution_clock::now();
// Create four Threads
std::vector<std::thread> MyThreads(4);
// Create Threads
for (size_t i = 0; i < MyThreads.size(); ++i) {
MyThreads[i] = std::thread(MultiThread_Merge, i);
}
// Wait for all threads to finish.
for (size_t i = 0; i < MyThreads.size(); ++i) {
MyThreads[i].join();
}
// When threads are done, add up numbers.
for (auto i : MyCounters)
{
// Add all the numbers.
DivCounter.CountEven += i.CountEven;
DivCounter.CountDivFive += i.CountDivFive;
DivCounter.CountDivEight += i.CountDivEight;
DivCounter.CountDivTen += i.CountDivTen;
}
// Stop timer and get time.
auto elapsed = std::chrono::high_resolution_clock::now() - StartTime;
auto seconds = std::chrono::duration_cast<std::chrono::seconds>(elapsed).count();
std::cout << "Time in seconds: " << seconds << std::endl;
}
void DisplayCounters()
{
std::cout << "Count divisible by 2: " << DivCounter.CountEven << std::endl;
std::cout << "Count divisible by 5: " << DivCounter.CountDivFive << std::endl;
std::cout << "Count divisible by 8: " << DivCounter.CountDivEight << std::endl;
std::cout << "Count divisible by 10: " << DivCounter.CountDivTen << std::endl;
}
int main()
{
std::cout << "Calculation number: " << CALC_NUMBER << std::endl;
std::cout << "\n============================================\n";
std::cout << "\nSingle-Threaded ..\n";
SingleThreaded();
DisplayCounters();
// Reset for multi-thread
DivCounter.CountEven = 0;
DivCounter.CountDivFive = 0;
DivCounter.CountDivEight = 0;
DivCounter.CountDivTen = 0;
std::cout << "\n============================================\n";
std::cout << "\nMulti-Threaded (Merge) ..\n";
DoThreadUsingMerge();
DisplayCounters();
system("pause");
}下面是4个核心CPU的输出,Windows8.1操作系统:
计算编号: 10000000000 ============================================单线程.时间(秒):36 计数可被2: 50000000堆栈除以纽线计数可被5: 20000000堆栈除以纽线计数可除以8: 12500000堆栈纽线计数可除以10: 1000000000 ============================================多线程(合并)。时间(秒):42 计数可被2: 50000000堆栈整除,纽线计数可被5: 20000000堆栈除以纽线计数可除以8: 12500000堆栈纽线计数可被10: 10000000StackNewline计数除以10: 1000000000按任意键继续。。。
下面是6核心CPU ( Windows 10 OS)的输出:
计算编号: 10000000000 ============================================单线程.时间(秒):32 计数可被2: 50000000堆栈除以纽线计数可被5: 20000000堆栈除以纽线计数可除以8: 12500000堆栈纽线计数可除以10: 1000000000 ============================================多线程(合并)。时间(秒):45 计数可被2: 50000000堆栈除以纽线计数可被5: 20000000堆栈除以纽线计数可除以8: 12500000堆栈纽线计数可被10: 10000000StackNewline计数除以10: 1000000000按任意键继续。。。
结果表明,无论多少次,无论内核多少次,多线程代码都不会从线程中受益。
这段代码不像预期那样运行的原因是什么?我是不是偶然发现了一些代码,这些代码不能或不能从多线程中获益?
我还试图把计算次数增加10倍,20倍,30倍,等等。但我没有看到任何更好的表现。
发布于 2019-11-03 19:34:43
struct Counters比缓存行小。对于良好的只读数据结构,数据的“干净”副本可以在缓存中处于共享状态,因此如果两个或多个内核想要拥有相同的数据,那就无关紧要了。但是在这里,它被用于许多读/写/修改操作,多个核试图跳到同一条高速缓存线上--而不是完全相同的数据,所以这不是真正的共享,数据在逻辑上是独立的,但从共享硬件的角度来看,由于它位于同一高速缓存线上:错误共享。
将Counters填充到64个字节可以工作。在某种程度上。至少它将开始使用线程计数进行适当的扩展,但是代码仍然足够慢,在它超过单线程版本之前,我需要4个线程。
从编译器的角度来看,存在对共享内存的写入(和从共享内存中读取)。也许它们是必要的,它怎么知道它们不是呢?但是我们人类,用我们的全程序推理技巧,知道其中的大部分是不必要的,因为主线程等待工人完成,然后收集结果,部分计数在过程中不被观察,所以我们可以这样做:
void MultiThread_Merge(int index)
{
Counters local;
for (unsigned long long x = parts[index].first; x <= parts[index].second; ++x)
{
// Count the even number
if ((x % 2) == 0)
++local.CountEven;
// Count divisible by 5
if ((x % 5) == 0)
++local.CountDivFive;
// Count divisible by 8
if ((x % 8) == 0)
++local.CountDivEight;
// Count divisible by 10
if ((x % 10) == 0)
++local.CountDivTen;
}
MyCounters[index] = local;
}现在它很快了。
发布于 2019-11-03 16:54:53
我正在写一个单独的答案,因为我已经测试了它。
我编译了您的代码并运行了相同的测试,结果相同。经过一些尝试之后,问题似乎是四个线程都访问相同的结构计数向量(std::vector<Counters> MyCounters(4);)。
我用一个简单变量(unsigned long long int My1Counter;)替换了计数器,对于多线程运行,我现在得到了3.8分的改进(这仍然在dev环境中,剩下的.2可能是dev env吃了一点)。
我的猜测是向量类是‘线程安全’,因此每次从其中一个线程访问它时都会锁定它,因此其他三个线程必须等待。
您可以尝试一个简单的C-数组来验证它,因为它并不是隐式线程安全的。
发布于 2019-11-03 15:26:52
你肯定会看到一个巨大的进步,几乎是N的一倍。
提到的开销在微秒范围内,每线程一次。
我有一个类似的例子,运行时下降了2,4,8的因素,这取决于我启动的线程数量(当然,直到我拥有的内核数量)。
我无法确切地告诉您为什么您的示例不起作用;也许编译器足够聪明,一开始可以在多个线程中运行,或者您的可执行文件无法访问来运行多个线程。你是不是在虚拟机里?检查int N = std::thread::hardware_concurrency(),看看你的程序也有多少线程可以访问。
我认为你的代码是正确的,应该有效。
https://codereview.stackexchange.com/questions/231773
复制相似问题