c ++中的显式代码并行性

时间:2008-09-26 21:58:36

标签: c++ performance design-patterns

CPU中的乱序执行意味着CPU可以重新排序指令以获得更好的性能,这意味着CPU必须做一些非常漂亮的簿记等。还有其他处理器方法,例如超线程。

一些花哨的编译器在一定程度上理解指令的(un)相关性,并且会自动交错指令流(可能在比CPU看到的更长的窗口上)以更好地利用处理器。浮动和整数指令的有意编译时交织是另一个例子。

现在我有高度并行的任务。而且我通常拥有老化的单核x86处理器,没有超线程。

是否有一种直接的方式来让我的'for'循环的主体为这个高度并行的任务交错,以便两个(或更多)迭代一起完成? (这与我理解的'循环展开'略有不同。)

我的任务是运行一系列指令的“虚拟机”,我将真正简化为图示:

void run(int num) {
  for(int n=0; n<num; n++) {
     vm_t data(n);
     for(int i=0; i<data.len(); i++) {
        data.insn(i).parse();
        data.insn(i).eval();
     }
  }  
}

因此执行跟踪可能如下所示:

data(1) insn(0) parse
data(1) insn(0) eval
data(1) insn(1) parse
...
data(2) insn(1) eval
data(2) insn(2) parse
data(2) insn(2) eval

现在,我想要的是能够并行显式地进行两次(或更多次)迭代:

data(1) insn(0) parse
data(2) insn(0) parse  \ processor can do OOO as these two flow in
data(1) insn(0) eval   /
data(2) insn(0) eval   \ OOO opportunity here too
data(1) insn(1) parse  /
data(2) insn(1) parse

我知道,从分析中,(例如使用带有--simulate-cache = yes的Callgrind),解析是关于随机存储器访问(缓存丢失),而eval是关于在寄存器中执行操作然后再写回结果。每一步都是几千条指令。因此,如果我可以同时将这两个步骤混合在一起进行两次迭代,那么当解析步骤的缓存未命中时,处理器将有希望做些事情......

是否有一些c ++模板疯狂才能产生这种显式并行性?

当然,我可以在代码中进行交错 - 甚至是交错 - 但是它使代码的可读性低得多。如果我真的想要不可读,我可以做汇编程序!但是这种事情肯定存在一些模式吗?

8 个答案:

答案 0 :(得分:5)

鉴于优化编译器和流水线处理器,我建议您只编写清晰易读的代码。

答案 1 :(得分:4)

您最好的计划可能是调查OpenMP。它基本上允许您在代码中插入“pragma”,告诉编译器如何在处理器之间进行分割。

答案 2 :(得分:3)

超线程是一种比指令重新排序更高级别的系统。它使处理器看起来像操作系统的两个处理器,因此您需要使用实际的线程库来利用它。同样的事情自然也适用于多核处理器。

如果您不想使用低级别的线程库,而是想使用基于任务的并行系统(听起来就像您所追求的那样),我建议您查看OpenMP或英特尔的Threading Building Blocks

TBB是一个库,因此它可以与任何现代C ++编译器一起使用。 OpenMP是一组编译器扩展,因此您需要一个支持它的编译器。 GCC / G ++将从版本4.2和更新版本开始。最新版本的英特尔和微软编译器也支持它。但是,我不知道其他任何人。

编辑:另一个注意事项。使用像TBB或OpenMP这样的系统将尽可能地扩展处理 - 也就是说,如果你有100个对象可以工作,它们将在双核系统中分成大约50/50,25/25/25 / 25个四核系统等

答案 3 :(得分:2)

像Core 2这样的现代处理器有近乎100条指令的巨大的指令重新排序缓冲区;即使编译器相当愚蠢,CPU仍然可以弥补它。

主要问题是如果代码使用了很多寄存器,在这种情况下,寄存器压力可能会迫使代码按顺序执行,即使理论上它可以并行执行。

答案 4 :(得分:2)

目前的C ++标准中不支持并行执行。这将在明年即将发布的下一版标准中发生变化。

但是,我看不出你想要完成什么。您指的是一个单核处理器,还是多个处理器或核心?如果你只有一个核心,你应该做任何最少的缓存未命中,这意味着无论什么方法使用最小的内存工作集。这可能要么进行所有解析,然后进行所有评估,要么交替进行解析和评估。

如果您有两个核心,并且想要有效地使用它们,那么您将不得不使用特别智能的编译器或语言扩展。您正在开发一种特定的操作系统,还是应该用于多个系统?

答案 5 :(得分:1)

听起来你遇到了芯片设计人员面临的同样问题:执行一条指令需要付出很多努力,但它涉及一系列可以在execution pipeline中串联起来的不同步骤。 (当你可以用不同的硬件块构建它们时,更容易并行执行。)

最明显的方法是将每个任务分成不同的线程。您可能希望创建单个线程来执行每个指令以完成,或者为两个执行步骤中的每一个创建一个线程并在它们之间传递数据。在任何一种情况下,您都必须非常小心如何在线程之间共享数据,并确保处理一条指令影响以下指令结果的情况。即使您只有一个核心,并且在任何给定时间只能运行一个线程,您的操作系统应该能够安排计算密集型线程,而其他线程正在等待其缓存未命中。

(几个小时的时间可能会支付一台非常快的计算机,但是如果你试图在廉价硬件上广泛部署它,那么以你看待它的方式来考虑这个问题可能是有意义的。无论如何,这是一个值得考虑的有趣问题。)

答案 6 :(得分:0)

看看cilk。它是ANSI C的扩展,它有一些很好的结构,用于在C中编写并行化代码。但是,由于它是C的扩展,因此它具有非常有限的编译器支持,并且可能很难处理。

答案 7 :(得分:0)

这个答案是在假设问题不包含“我通常拥有老化的单核x86处理器而没有超线程”的情况下编写的。我希望它可以帮助其他想要并行化高度并行任务的人,但需要针对双核/多核CPU。

正如another answer中已经发布的那样,OpenMP是一种可移植的方式。不过我的经验是OpenMP开销非常高,很容易被它击败 滚动DIY(自己动手)实现。希望OpenMP会随着时间的推移而改进,但就像现在一样,我不建议将其用于除原型之外的其他任何事情。

鉴于您的任务的性质,您想要做的事情很可能是基于数据的并行性,根据我的经验,这很简单 - 编程风格可能非常类似于单核代码,因为您知道其他什么线程正在做,这使得维护线程安全变得更加容易 - 这种方法对我有用:避免依赖并且只从循环中调用线程安全函数。

要创建DYI OpenMP并行循环,您需要:

  • 作为准备创建一个循环模板的序列,并更改您的代码以使用仿函数来实现循环体。这可能很乏味,因为您需要跨仿函数对象传递所有引用
  • 为仿函数创建一个虚拟JobItem接口,并从该接口继承您的仿函数
  • 创建一个能够处理单个JobItems对象的线程函数
  • 使用此线程函数
  • 创建线程的线程池
  • 尝试各种同步原语,看看哪种方法最适合您。虽然信号量非常容易使用,但它的开销非常大,如果你的循环体很短,你不希望为每次循环迭代支付这个开销。对我来说最有效的是手动重置事件+原子(互锁)计数器的组合作为一种更快的替代方案。
  • 尝试各种JobItem调度策略。如果你有足够长的循环,那么每个线程一次获取多个连续的JobItem会更好。这减少了同步开销,同时使线程更加缓存友好。您可能还希望以某种动态方式执行此操作,在耗尽任务时减少计划序列的长度,或让单个线程从其他线程计划中窃取项目。