【问题标题】:Why isn't N independent calculations N times faster on N threads?为什么在 N 个线程上 N 个独立计算不能快 N 倍?
【发布时间】:2015-10-05 05:02:42
【问题描述】:

我有一个 N 核处理器(在我的例子中是 4 个)。为什么 N 个线程上的 N 个完全独立的函数调用不快大约 N 倍(当然有创建线程的开销,但请进一步阅读)?

看下面的代码:

namespace ch = std::chrono;
namespace mp = boost::multiprecision;
constexpr static unsigned long long int num = 3555;

// mp_factorial uses boost/multiprecision/cpp_int, so I get legit results

    ch::steady_clock::time_point s1 = ch::steady_clock::now();
    auto fu1 = std::async(std::launch::async, mp_factorial, num);
    auto fu2 = std::async(std::launch::async, mp_factorial, num);
    auto fu3 = std::async(std::launch::async, mp_factorial, num);
    auto fu4 = std::async(std::launch::async, mp_factorial, num);
    fu1.get(); fu2.get(); fu3.get(); fu4.get();
    ch::steady_clock::time_point e1 = ch::steady_clock::now();

    ch::steady_clock::time_point s2 = ch::steady_clock::now();
    mp_factorial(num);
    mp_factorial(num);
    mp_factorial(num);
    mp_factorial(num);
    ch::steady_clock::time_point e2 = ch::steady_clock::now();

    auto t1 = ch::duration_cast<ch::microseconds>(e1 - s1).count();
    auto t2 = ch::duration_cast<ch::microseconds>(e2 - s2).count();

    cout << t1 << " " << t2 << endl;

我得到如下结果:

11756 20317

这大约快 2 倍。我也用大量的数字尝试过这个,比如num = 355555。我得到了非常相似的结果:

177462588 346575062

为什么会这样?我完全了解阿姆达尔定律,并且多核处理器并不总是快number_of_cores 倍,但是当我有独立 strong> 操作,我期待更好的结果。至少在number_of_cores 附近。


更新:

如您所见,所有线程都按预期工作,所以这不是问题:

【问题讨论】:

  • 真的有四个核心吗?还是只有两个内核和四个超线程? (例如英特尔 i3)。
  • 另外:您是否检查过顺序测试中的 4 个函数调用没有内联?
  • @BaummitAugen 是的。我已经在 i7-4700MQ 上对其进行了测试。 4 个核心,8 个超线程。
  • 这取决于操作系统调度程序的效率。其他程序和后台任务将被分配给内核,并从线程正在使用的一些但不是全部内核中获取量子,用户对此几乎没有控制权;很难强制操作系统将内核完全专用于程序中的线程(设置亲和性只会将线程锁定到内核,不会阻止其他进程使用它)。
  • @DanielJour 我没有。如何明确禁止内联?还没有真正考虑到这一点,这些数字很快就会变得很大,比如 5 MB-s 的内存。但是,在 DDR3 RAM 中复制 5 MB-s 并不能与 IMO 的计算相比。

标签: c++ multithreading multicore cpu-cores parallelism-amdahl


【解决方案1】:

这里的问题是你肯定有一些大块的数字,它们不适合你的处理器的 L1 和 L2 缓存,这意味着当内存控制器在整个内存控制器上跳跃时,处理器会坐着并旋转它的小 ALU 手指试图为每个处理器读取一点内存的地方。

当您在一个线程中运行时,该线程至少在很大程度上只能在三个不同的内存区域上工作(a = b * c,从bc 读取,写入a)。

当您执行 4 个线程时,您有四个不同的 a = b * c;,每个具有三个不同的数据流,导致缓存、内存控制器和“打开页面”的更多颠簸[这里的页面是 DRAM 术语,没有与 MMU 页面有关,但您也可能会发现 TLB 未命中也是一个因素]。

因此,运行更多线程可以获得更好的性能,但不是 4 倍,因为每个线程消耗和产生大量数据,内存接口是瓶颈。除了让机器具有更高效的内存接口 [这可能不是那么容易],您对此无能为力 - 只需接受对于这种特殊情况,内存比计算更多是一个限制因素。

使用多线程求解的理想示例是那些需要大量计算但不占用太多内存的示例。我有一个简单的素数计算器和一个计算“奇怪数字”的计算器,当在 N 个内核上运行时,两者都能提供几乎完全 Nx 的性能提升 [但我是否会开始将这些用于比 64 位大很多倍的数字,它会停止给予同样的好处]

编辑:还有可能:

  • 您的代码经常调用的某些函数正在锁定/阻塞其他线程 [可能以繁忙等待的方式,如果实现需要较短的等待时间,因为调用操作系统等待几十个时钟 -循环毫无意义] - 像 newmalloc 和它们的释放对应物是合理的候选者。
  • 真假共享 - 数据在 CPU 内核之间共享,导致缓存内容在处理器之间来回发送。从每个线程访问 [和更新] 的特别小的共享数组可能会导致出错,即使更新是无锁地并使用原子操作完成的。

当你有这样的事情时使用术语“虚假”共享

 // Some global array. 
 int array[MAX_THREADS];
 ....
 // some function that updates the global array
 int my_id = thread_id();
 array[my_id]++;

虽然每个线程都有自己的数组条目,但相同的缓存行会从一个 CPU 反弹到另一个 CPU。我曾经有一个 SMP(多核之前)dhrystone 基准测试,当在 2 个处理器上运行时,它的性能是一个处理器的 0.7 倍——因为一个经常访问的数据项被存储为int array[MAX_THREADS]。这当然是一个比较极端的例子……

【讨论】:

  • 是的,就是这样。我正在开发一个线程池类,在那期间我遇到了这个问题。我尝试向池中添加较少的内存密集型任务,并且缩放增加了很多(接近 N 倍)。谢谢。
【解决方案2】:

您的答案取决于用户线程或内核线程。如果您正在使用的线程是在用户空间中实现的,那么内核不会意识到它们,因此它们无法以真正的“并行”方式跨多个物理 cpu 内核执行。

如果线程是在内核空间中实现的,那么内核就知道这些线程并且可以跨多个物理 cpu 内核以并行方式处理它们。

还有线程创建、销毁和上下文切换的开销。每次线程上下文切换线程库需要存储值和加载值等。

【讨论】:

  • 鉴于问题中的图表,我非常怀疑第一段是否适用。对于在现代台式计算机操作系统/处理器组合上花费超过几毫秒的基准测试,我想说线程创建和销毁也是微不足道的。
  • @MatsPetersson 我不熟悉任何在 CPU 上有 5 个线程的系统,所以我想知道他最初是从哪里得到图表的。
  • @Matt main 函数在一个线程中运行。那是线程 1。4 个std::asyncs 创建了 4 个其他线程,这些线程正在工作(至少在并行部分中)。顺便说一句,我从我的 IDE ( Xcode ) 中得到了图表。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-06-03
  • 2020-01-23
  • 1970-01-01
  • 2010-09-08
相关资源
最近更新 更多