5

我正在玩 C++ 中的线程,特别是使用它们来并行化映射操作。

这是代码:

#include <thread>
#include <iostream>
#include <cstdlib>
#include <vector>
#include <math.h>
#include <stdio.h>

double multByTwo(double x){
  return x*2;
}

double doJunk(double x){
  return cos(pow(sin(x*2),3));
}

template <typename T>
void map(T* data, int n, T (*ptr)(T)){
  for (int i=0; i<n; i++)
    data[i] = (*ptr)(data[i]);
}

template <typename T>
void parallelMap(T* data, int n, T (*ptr)(T)){
  int NUMCORES = 3;
  std::vector<std::thread> threads;
  for (int i=0; i<NUMCORES; i++)
    threads.push_back(std::thread(&map<T>, data + i*n/NUMCORES, n/NUMCORES, ptr));
  for (std::thread& t : threads)
    t.join();
}

int main()
{
  int n = 1000000000;
  double* nums = new double[n];
  for (int i=0; i<n; i++)
    nums[i] = i;

  std::cout<<"go"<<std::endl;

  clock_t c1 = clock();

  struct timespec start, finish;
  double elapsed;

  clock_gettime(CLOCK_MONOTONIC, &start);

  // also try with &doJunk
  //parallelMap(nums, n, &multByTwo);
  map(nums, n, &doJunk);

  std::cout << nums[342] << std::endl;

  clock_gettime(CLOCK_MONOTONIC, &finish);

  printf("CPU elapsed time is %f seconds\n", double(clock()-c1)/CLOCKS_PER_SEC);

  elapsed = (finish.tv_sec - start.tv_sec);
  elapsed += (finish.tv_nsec - start.tv_nsec) / 1000000000.0;

  printf("Actual elapsed time is %f seconds\n", elapsed);
}

multByTwo并行版本实际上稍慢(1.01 秒对 0.95 实时),而 doJunk 更快(51 对 136 实时)。这对我来说意味着

  1. 并行化正在运行,并且
  2. 声明新线程的开销非常大。关于为什么开销如此之大以及如何避免它的任何想法?
4

4 回答 4

7

只是一个猜测:您可能会看到multByTwo代码如此之快,以至于您正在达到内存饱和。无论你投入多少处理器能力,代码都不会运行得更快,因为它已经在尽可能快地从 RAM 中获取位。

于 2012-06-22T15:50:31.533 回答
3

您没有指定测试程序的硬件,也没有指定编译器版本和操作系统。我确实在我们的 64 位 Scientific Linux 下的四插槽 Intel Xeon 系统上尝试了您的代码,其中g++4.7 是从源代码编译的。

首先在较旧的 Xeon X7350 系统上,我得到了以下时间:

multByTwomap

CPU elapsed time is 6.690000 seconds
Actual elapsed time is 6.691940 seconds

multByTwo3parallelMap

CPU elapsed time is 7.330000 seconds
Actual elapsed time is 2.480294 seconds

并行加速为 2.7 倍。

doJunkmap

CPU elapsed time is 209.250000 seconds
Actual elapsed time is 209.289025 seconds

doJunk3parallelMap

CPU elapsed time is 220.770000 seconds
Actual elapsed time is 73.900960 seconds

并行加速为 2.83 倍。

请注意,X7350 来自相当古老的 Nehalem 之前的“Tigerton”系列,带有 FSB 总线和位于北桥的共享内存控制器。这是一个没有 NUMA 效果的纯 SMP 系统。

然后我在四插槽 Intel X7550 上运行您的代码。这些是 Nehalem(“Beckton”)至强处理器,内存控制器集成到 CPU 中,因此是 4 节点 NUMA 系统。在一个套接字上运行并访问位于另一个套接字上的内存的线程运行速度会慢一些。对于可能通过一些愚蠢的调度程序决定迁移到另一个套接字的串行进程也是如此。从时序中可以看出,在这样的系统中绑定非常重要:

multByTwomap

CPU elapsed time is 4.270000 seconds
Actual elapsed time is 4.264875 seconds

multByTwomap绑定到 NUMA 节点0

CPU elapsed time is 4.160000 seconds
Actual elapsed time is 4.160180 seconds

multByTwo绑定到mapNUMA 节点 0 和 CPU 插槽 1

CPU elapsed time is 5.910000 seconds
Actual elapsed time is 5.912319 seconds

mutlByTwo3parallelMap

CPU elapsed time is 7.530000 seconds
Actual elapsed time is 3.696616 seconds

并行加速仅为 1.13 倍(相对于最快的节点绑定串行执行)。现在绑定:

multByTwoparallelMap3 个内核绑定到 NUMA 节点 0

CPU elapsed time is 4.630000 seconds
Actual elapsed time is 1.548102 seconds

并行加速是 2.69 倍 - 与 Tigerton CPU 一样多。

multByTwo3个parallelMap内核绑定到 NUMA 节点 0 和 CPU 插槽 1

CPU elapsed time is 5.190000 seconds
Actual elapsed time is 1.760623 seconds

并行加速是前一种情况的 2.36 倍 - 88%。

(我迫不及待地等待doJunk代码在相对较慢的 Nehalems 上完成,但我希望能像 Tigerton 案例中那样有更好的性能)

不过,NUMA 绑定有一个警告。如果您强制绑定到 NUMA 节点 0,numactl --cpubind=0 --membind=0 ./program这将限制仅分配给该节点的内存,并且在您的特定系统上,连接到 CPU 0 的内存可能不够,并且很可能会发生运行时故障。

正如您所看到的,除了创建线程的开销之外,还有一些因素会显着影响您的代码执行时间。同样在非常快的系统上,与每个线程完成的计算工作相比,开销可能太高了。这就是为什么在询问有关并行性能的问题时,应始终尽可能多地包含有关用于测量性能的硬件和环境的详细信息。

于 2012-06-22T17:31:26.977 回答
2

多线程只能在多核机器上以更少的时间完成更多的工作。

否则,他们只是以循环方式轮流。

于 2012-06-22T15:49:40.027 回答
0

根据平台的不同,产生新线程可能是一项昂贵的操作。避免这种开销的最简单方法是在程序启动时产生几个线程并拥有某种作业队列。我相信 std::async 会为你做到这一点。

于 2012-06-22T15:48:07.377 回答