我有一些昂贵的计算,我想在一组线程上划分和分布。 我将代码简化为一个最小的示例,在这个示例中,这种情况仍在发生。
简而言之:
我有N个任务,我想把它们分成“线程”线程。
每个任务就是下面运行一堆简单数学运算的简单函数。 (在实践中,我在这里验证了非对称签名,但为了简化起见,我将其排除在外)
while (i++ < 100000)
{
for (int y = 0; y < 1000; y++)
{
sqrt(y);
}
}
用1个线程运行上述代码会导致每个操作0.36秒(最外层为循环),因此总体执行时间约为36秒。
因此,并行化似乎是一种明显的加速方法。 但是,使用两个线程时,操作时间上升到0.72秒,完全破坏了任何速度的提高。
添加更多线程通常会导致性能越来越差。
我得到了一个Intel(R)Core(TM)i7-8750H CPU@2.20GHz,有6个物理核心。 因此,我希望在从1线程到2线程的情况下,至少使用它可以提高性能。 但实际上每个操作在增加线程数时都会变慢。
我是不是做错了什么?
完整代码:
using namespace std;
const size_t N = 100;
const size_t Threads = 1;
atomic_int counter(0);
struct ThreadData
{
int index;
int count;
ThreadData(const int index, const int count): index(index), count(count){};
};
void *executeSlave(void *threadarg)
{
struct ThreadData *my_data;
my_data = static_cast<ThreadData *>(threadarg);
for( int x = my_data->index; x < my_data->index + my_data->count; x++ )
{
cout << "Thread: " << my_data->index << ": " << x << endl;
clock_t start, end;
start = clock();
int i = 0;
while (i++ < 100000)
{
for (int y = 0; y < 1000; y++)
{
sqrt(y);
}
}
counter.fetch_add(1);
end = clock();
cout << end - start << ':' << CLOCKS_PER_SEC << ':' << (((float) end - start) / CLOCKS_PER_SEC)<< endl;
}
pthread_exit(NULL);
}
int main()
{
clock_t start, end;
start = clock();
pthread_t threads[Threads];
vector<ThreadData> td;
td.reserve(Threads);
int each = N / Threads;
cout << each << endl;
for (int x = 0; x < Threads; x++) {
cout << "main() : creating thread, " << x << endl;
td[x] = ThreadData(x * each, each);
int rc = pthread_create(&threads[x], NULL, executeSlave, (void *) &td[x]);
if (rc) {
cout << "Error:unable to create thread," << rc << endl;
exit(-1);
}
}
while (counter < N) {
std::this_thread::sleep_for(10ms);
}
end = clock();
cout << "Final:" << endl;
cout << end - start << ':' << CLOCKS_PER_SEC << ':' << (((float) end - start) / CLOCKS_PER_SEC)
<< endl;
}
clock()
返回整个进程的大约CPU时间。
最外面的循环每次迭代完成固定量的工作
int i = 0;
while (i++ < 100000)
{
for (int y = 0; y < 1000; y++)
{
sqrt(y);
}
}
因此,围绕这个循环报告的进程CPU时间将与正在运行的线程数成正比(它仍然花费每个线程相同的时间量,乘以N个线程)。
改用std::chrono::steady_clock
测量挂钟时间。 还要注意,诸如std::cout
之类的I/O会占用很多挂钟时间,而且不稳定。 因此测量到的总经过时间将会由于内部的I/O而发生倾斜。
一些补充说明:
从不使用sqrt()
的返回值; 编译器可以完全消除调用。 谨慎的做法是以某种方式使用该值来确定。
void*executeSlave()
没有返回void*
指针值(UB)。 如果它不返回任何东西,可能应该简单地声明为void
。
td.reserve(Threads)
保留内存,但不分配对象。 td[x]
然后访问不存在的对象(UB)。 使用td.emplace_back(x*each,eace)
而不是td[x]=...
。
技术上不是一个问题,但建议使用标准C++std::thread
而不是pthread
,以获得更好的可移植性。
通过以下方式,我看到了与线程数成正比的正确加速比:
#include <string>
#include <iostream>
#include <vector>
#include <atomic>
#include <cmath>
#include <thread>
using namespace std;
using namespace std::chrono_literals;
const size_t N = 12;
const size_t Threads = 2;
std::atomic<int> counter(0);
std::atomic<int> xx{ 0 };
void executeSlave(int index, int count, int n)
{
double sum = 0;
for (int x = index; x < index + count; x++)
{
cout << "Thread: " << index << ": " << x << endl;
auto start = std::chrono::steady_clock::now();
for (int i=0; i < 100000; i++)
{
for (int y = 0; y < n; y++)
{
sum += sqrt(y);
}
}
counter++;
auto end = std::chrono::steady_clock::now();
cout << 1e-6 * (end - start) / 1us << " s" << endl;
}
xx += (int)sum; // prevent optimization
}
int main()
{
std::thread threads[Threads];
int each = N / Threads;
cout << each << endl;
auto start = std::chrono::steady_clock::now();
for (int x = 0; x < Threads; x++) {
cout << "main() : creating thread, " << x << endl;
threads[x] = std::thread(executeSlave, x * each, each, 100);
}
for (auto& t : threads) {
t.join();
}
auto end = std::chrono::steady_clock::now();
cout << "Final:" << endl;
cout << 1e-6 * (end - start) / 1us << " s" << endl;
}