为什么设置CPU亲和力会使线程运行得更慢?

时间:2016-09-14 16:10:19

标签: c++ linux multithreading performance

所有

我写了一个小案例来测试多线程生产者/消费者模型。我的测试平台是一台低性能PC(8G RAM,J1900 CPU,4核)。我为Linux内核隔离了核心0,核心1-3未使用。生产者线程在核心1上运行,分配5000000个小对象,将它们放入全局队列。使用者线程在核心2上运行并从队列中释放对象。但我发现如果我没有设置它们的CPU亲和性(也就是说,它们运行在相同的核心0上),那么时间性能会比设置CPU亲和力(8.76s VS 14.66s)更好。测试结果保持相似。有人可以解释一下我的原因吗?如果我的前提不正确("设置CPU亲和力可以改善多线程进程'性能"),它至少应该不会变得更糟。我的代码片段如下:

void producer() {
  Timestamp begin;

  for ( int i = 0; i<data_nb; ++i ) {
    Test* test = new Test(i, i+1);
    queue.enqueue(test);
  }

  Timestamp end;
  TimeDuration td = end-begin;
  printf("producer: %ldms(%.6fs)\n", td.asMicroSecond(), td.asSecond());
}

void consumer() {
  Timestamp begin;

  do {
    Test* test = queue.dequeue();
    if ( test ) {
      nb.add(1); // nb is an atomic counter
      delete test;
      test = nullptr;
    }
  } while ( nb.get() < data_nb );

  Timestamp end;
  TimeDuration td = end-begin;
  //printf("%d data consumed\n", nb.get());
  printf("consumer: %ldms(%.6fs)\n", td.asMicroSecond(), td.asSecond());
}

1 个答案:

答案 0 :(得分:5)

从CPU亲和力中获取性能并不像将线程1推送到核心1而线程2推送到核心2那么简单。这是一个复杂且经过深入研究的主题,我将触及亮点。

首先,我们需要定义&#39;表现&#39;。通常,我们对throughputlatency和/或scalability感兴趣。将这三者结合起来是一个棘手的架构问题,在电信,金融和其他行业受到了严格的审查。

您的案例似乎是由吞吐量指标驱动的。我们希望跨线程的挂钟时间总和最小。陈述问题的另一种方式可能是,&#34;影响多线程进程吞吐量的因素有哪些?&#34;

以下是一些因素:

  1. 算法复杂度影响最大。 Big O,theta,little O在复杂情况下都非常有用。这个例子很简单,但这仍然很重要。从表面上看,问题是O(n)。根据要分配/解除分配的元素数量,时间将是线性的。你的问题触及了这个问题的核心,因为它表明物理计算机并不能完美地模拟理想的计算机。
  2. CPU资源。如果可以并行化问题,让CPU解决问题可能会有所帮助。你的问题有一个潜在的假设,即两个线程将优于一个线程。如果是这样,也许四个将介于两个之间。同样,您的实际结果与理论模型相矛盾。
  3. 排队模型。如果要实现性能提升,理解Queuing模型至关重要。示例问题似乎是经典的单一生产者/单一消费者模型。
  4. 其他资源。根据问题,各种其他资源可能会限制性能。一些因素包括磁盘空间,磁盘吞吐量,磁盘延迟,网络容量,插槽可用性。这个例子似乎没有受到影响。
  5. 内核依赖项。迁移到较低级别,性能可能会受到内核交互量的显着影响。通常,内核调用需要上下文切换,如果不断进行,这可能是昂贵的。您的示例可能会通过调用new / delete来解决此问题。
  6. 串行访问。如果资源需要串行访问,那么它将成为并行算法的瓶颈。您的示例似乎有两个问题,new / delete和enqueue / dequeue。
  7. CPU缓存。评论提到CPU caching是可能的。 L2 / L3缓存可能是缓存未命中的来源以及false sharing。我怀疑这是你的例子中的主要问题,但它可能是一个因素。
  8. 将这些想法应用到您的示例中,我看到了一些问题。我假设您有两个独立的线程并行运行。一个线程产生(新),另一个消耗(删除)。

    堆是串行的。在不同的线程中调用new和delete是一个已知的性能瓶颈。有几个小块并行分配器可用,包括Hoard

    队列可能是串行的。没有显示实现,但是enqueue / dequeue可能是两个线程之间的序列化点。有许多lock free ring buffers可以在多个线程之间使用的例子。

    线程饥饿。在该示例中,如果生产者比消费者慢,那么消费者将在大部分时间内闲置。这是编制高性能算法时必须考虑的队列理论的一部分。

    在所有这些背景下,我们现在可以得出结论,在序列化和饥饿问题得到解决之前,线程亲和力可能不重要。事实上,这两个线程可能运行速度较慢,因为它们相互竞争共享资源或者只是浪费cpu时间空闲。结果,整体吞吐量下降,因此挂钟时间上升。

    对于了解这些算法的工程师来说,行业需求巨大。教育自己可能是一个有利可图的冒险。

相关问题