9

我有一个 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快几倍,但是当我有独立操作时,我会期待更好的结果。至少附近的东西number_of_cores


更新:

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

线程的工作负载

4

2 回答 2

16

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

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

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

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

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

编辑:还有可能:

  • 您的代码经常调用的某些函数正在锁定/阻塞其他线程[可能以繁忙等待的方式,如果实现需要很短的等待时间,因为调用操作系统等待几十个时钟周期是没有意义的] - 像newandmalloc和它们的释放对应物是合理的候选者。
  • True of false sharing - 数据在 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]. 这当然是一个比较极端的例子……

于 2015-07-15T23:11:11.280 回答
-2

您的答案取决于用户线程或内核线程。如果您使用的线程是在用户空间中实现的,则内核不知道它们,因此它们不能以真正的“并行”方式跨多个物理 cpu 内核执行。

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

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

于 2015-07-15T23:12:51.400 回答