42

我正在尝试新的 C++11 线程,但我的简单测试具有糟糕的多核性能。作为一个简单的例子,这个程序将一些平方随机数相加。

#include <iostream>
#include <thread>
#include <vector>
#include <cstdlib>
#include <chrono>
#include <cmath>

double add_single(int N) {
    double sum=0;
    for (int i = 0; i < N; ++i){
        sum+= sqrt(1.0*rand()/RAND_MAX);
    }
    return sum/N;
}

void add_multi(int N, double& result) {
    double sum=0;
    for (int i = 0; i < N; ++i){
        sum+= sqrt(1.0*rand()/RAND_MAX);
    }
    result = sum/N;
}

int main() {
    srand (time(NULL));
    int N = 1000000;

    // single-threaded
    auto t1 = std::chrono::high_resolution_clock::now();
    double result1 = add_single(N);
    auto t2 = std::chrono::high_resolution_clock::now();
    auto time_elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(t2-t1).count();
    std::cout << "time single: " << time_elapsed << std::endl;

    // multi-threaded
    std::vector<std::thread> th;
    int nr_threads = 3;
    double partual_results[] = {0,0,0};
    t1 = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < nr_threads; ++i) 
        th.push_back(std::thread(add_multi, N/nr_threads, std::ref(partual_results[i]) ));
    for(auto &a : th)
        a.join();
    double result_multicore = 0;
    for(double result:partual_results)
        result_multicore += result;
    result_multicore /= nr_threads;
    t2 = std::chrono::high_resolution_clock::now();
    time_elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(t2-t1).count();
    std::cout << "time multi: " << time_elapsed << std::endl;

    return 0;
}

在 Linux 和 3 核机器上使用 'g++ -std=c++11 -pthread test.cpp' 编译,典型结果是

time single: 33
time multi: 565

所以多线程版本要慢一个数量级以上。我使用了随机数和 sqrt 来使示例变得不那么琐碎并且易于编译器优化,所以我没有想法。

编辑

  1. 这个问题适用于更大的 N,所以问题不在于运行时间短
  2. 创建线程的时间不是问题。排除它不会显着改变结果

哇,我发现了问题。确实是 rand()。我将其替换为 C++11 等效项,现在运行时可以完美扩展。感谢大家!

4

4 回答 4

27

在我的系统上,行为是相同的,但正如 Maxim 所提到的, rand 不是线程安全的。当我将 rand 更改为 rand_r 时,多线程代码会按预期更快。

void add_multi(int N, double& result) {
double sum=0;
unsigned int seed = time(NULL);
for (int i = 0; i < N; ++i){
    sum+= sqrt(1.0*rand_r(&seed)/RAND_MAX);
}
result = sum/N;
}
于 2013-05-23T15:14:55.173 回答
21

正如你所发现的,rand是这里的罪魁祸首。

对于那些好奇的人,这种行为可能来自您rand使用互斥锁实现线程安全。

例如,eglibc 根据定义rand__random定义

long int
__random ()
{
  int32_t retval;

  __libc_lock_lock (lock);

  (void) __random_r (&unsafe_state, &retval);

  __libc_lock_unlock (lock);

  return retval;
}

这种锁定会强制多个线程串行运行,从而导致性能下降。

于 2013-05-23T16:10:59.423 回答
8

执行程序所需的时间非常短(33 毫秒)。这意味着创建和处理多个线程的开销可能超过了真正的好处。尝试使用需要更长执行时间(例如,10 秒)的程序。

于 2013-05-23T14:10:22.647 回答
3

为了加快速度,请使用线程池模式。

这将使您可以在其他线程中排队任务,而无需std::thread每次要使用多个线程时创建一个开销。

不要计算在性能指标中设置队列的开销,只计算排队和提取结果的时间。

创建一组线程和一个任务队列(包含 a 的结构std::function<void()>)来提供它们。线程在队列中等待新任务执行,执行它们,然后等待新任务。

这些任务负责将它们的“完成”传达回调用上下文,例如通过std::future<>. 让您将函数排入任务队列的代码可能会为您执行此包装,即此签名:

template<typename R=void>
std::future<R> enqueue( std::function<R()> f ) {
  std::packaged_task<R()> task(f);
  std::future<R> retval = task.get_future();
  this->add_to_queue( std::move( task ) ); // if we had move semantics, could be easier
  return retval;
}

它将裸std::function返回R变为 nullary packaged_task,然后将其添加到任务队列中。请注意,任务队列需要移动感知,因为packaged_task它是仅移动的。

注1:我不是很熟悉std::future,所以上面可能有误。

注意 2:如果放入上述队列的任务相互依赖以获得中间结果,则队列可能会死锁,因为没有描述“回收”被阻塞的线程并执行新代码的规定。但是,“裸计算”非阻塞任务应该可以在上述模型中正常工作。

于 2013-05-23T14:22:11.313 回答