多线程编程:为什么两个程序比一个更好?

12

简要描述我的问题:

我的计算机配备有2个AMD Opteron 6272插槽和64GB内存。

我在所有32个核心上运行一个多线程程序,与我在两个16个核心插槽上分别运行两个程序的情况相比,速度要慢15%。

如何使单个程序版本与双程序版本一样快?


更多细节:

我有大量任务并希望充分利用系统的所有32个核心。因此,我将任务分组为每组1000个。这样的一组任务需要约120Mb的输入数据,并需要大约10秒钟才能在一个核心上完成。为了使测试更加理想,我将这些组复制32次,并使用ITBB的parallel_for循环在32个核心之间分发任务。

我使用pthread_setaffinity_np确保系统不会让我的线程在核心之间跳跃。为了确保所有核心都被连续使用。

我使用mlockall(MCL_FUTURE)来确保系统不会使我的内存在插槽之间跳跃。

所以代码看起来像这样:

  void operator()(const blocked_range<size_t> &range) const
  {
    for(unsigned int i = range.begin(); i != range.end(); ++i){

      pthread_t I = pthread_self();
      int s;
      cpu_set_t cpuset;
      pthread_t thread = I;
      CPU_ZERO(&cpuset);
      CPU_SET(threadNumberToCpuMap[i], &cpuset);
      s = pthread_setaffinity_np(thread, sizeof(cpu_set_t), &cpuset);

      mlockall(MCL_FUTURE); // lock virtual memory to stay at physical address where it was allocated

      TaskManager manager;
      for (int j = 0; j < fNTasksPerThr; j++){
        manager.SetData( &(InpData->fInput[j]) );
        manager.Run();
      }
    }
  }

对我来说只有计算时间很重要,因此我会在一个单独的parallel_for循环中准备输入数据。并且不将准备时间包括在时间测量中。

  void operator()(const blocked_range<size_t> &range) const
  {
    for(unsigned int i = range.begin(); i != range.end(); ++i){

      pthread_t I = pthread_self();
      int s;
      cpu_set_t cpuset;
      pthread_t thread = I;
      CPU_ZERO(&cpuset);
      CPU_SET(threadNumberToCpuMap[i], &cpuset);
      s = pthread_setaffinity_np(thread, sizeof(cpu_set_t), &cpuset);

      mlockall(MCL_FUTURE); // lock virtual memory to stay at physical address where it was allocated
      InpData[i].fInput = new ProgramInputData[fNTasksPerThr];

      for(int j=0; j<fNTasksPerThr; j++){
        InpData[i].fInput[j] = InpDataPerThread.fInput[j];
      }
    }
  }

现在我在32个核心上运行所有这些任务,每秒速度大约为1600个任务。

然后我创建了程序的两个版本,并使用tasksetpthread确保第一个版本在第一插槽的16个核心上运行,第二个版本在第二插槽上运行。 我使用简单的&命令在shell中将它们并行运行:

program1 & program2 &

每个程序都可以达到约900个任务/秒的速度。总共有超过1800个任务/秒,比单程序版本多15%。

我错过了什么?

我认为问题可能在于我只将库加载到主线程的内存中。这可能是个问题吗?我能否复制库数据以便独立地在两个插座上使用?


32个单线程程序无法解决问题,这很可能是内存分配在错误的NUMA节点上。他只有2个节点,因此他只需要2个程序,每个程序都绑定到一个单独的节点。 - Len Holgate
2
Numa节点??我不知道那是什么,但听起来很不错,我要去了解一下。 - Dennis
@Dennis,是的,numa。如果您想要,我可以展示拓扑和可扩展性测试。 - klm123
有趣的东西。我以前没有研究过它,但我可以看出它在大数据集的并行处理中能够带来多少好处。 - Dennis
Hristo - 现在,我需要一个在Windows上运行的那个工具的版本... - Len Holgate
显示剩余5条评论
2个回答

3
我猜测这可能是STL/boost内存分配的问题,由于它们不具备NUMA感知能力,所以会将集合等内存在NUMA节点之间分散。同时,你的程序中有线程在每个节点上运行。
为所有你使用的STL/boost组件创建自定义分配器可能会有所帮助(但这可能是一项巨大的工作)。

mlockall(MCL_FUTURE)不应该有用吗?这里http://linux.die.net/man/2/mlock说它必须帮助所有未来的内存分配。 - klm123
1
我期望库在此之前分配内存,并且由于它们对NUMA一无所知,它们很可能会在集合之间或在内部重复使用内存。在我看来,使用自定义容器可能是最好的选择。 - Len Holgate
你似乎是对的。最小化使用std::vector::reserve,我成功将时间差降低了2%。 - klm123

1
你可能正在遭受缓存错误共享的困扰:http://en.wikipedia.org/wiki/False_sharing 你的线程可能通过block_range引用共享对同一数据结构的访问。如果速度是您所需的全部,您可能需要将副本传递给每个线程。如果您的数据太大而无法适应调用堆栈,则可以在不同的缓存段中动态分配每个范围的副本(即只需确保它们足够远离)。
或者也许我需要看到代码的其余部分才能更好地理解您正在做什么。

我不确定我理解你的意思。你所说的数据是什么?block_range 是非常小的结构体,在程序(TaskManager)内部没有使用。所有被使用的数据,我已经复制并动态分配了。 - klm123
好的。你是对的。我误解了block_range的目的和性质。我以为它是你正在操作的一些常见数据。现在我明白它是一个用于整数区间的TBB模板。我的错。InpData是如何定义的? - Robert Jørgensgaard Engdahl

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接