【问题标题】:Explicit code parallelism in c++C++ 中的显式代码并行性
【发布时间】:2010-09-13 14:57:12
【问题描述】:

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

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

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

是否有一种直接的方法可以让我的“for”循环主体交错交错,以便一起完成两个(或多个)迭代? (这与我理解的“循环展开”略有不同。)

我的任务是通过一组指令运行的“虚拟机”,我将其简化为:

无效运行(int num){
  for(int n=0;n

所以执行轨迹可能如下所示:

data(1) insn(0) 解析
数据(1)insn(0)评估
数据(1) insn(1) 解析
...
数据(2)insn(1)评估
数据(2) insn(2) 解析
数据(2)insn(2)评估

现在,我希望能够显式地并行进行两次(或多次)迭代:

data(1) insn(0) 解析
data(2) insn(0) parse \ processor 可以做OOO,因为这两个流入
数据(1)insn(0)评估/
data(2) insn(0) eval \ OOO 机会也在这里
数据(1)insn(1)解析/
数据(2)insn(1)解析

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

是否有一些 C++ 模板疯狂来生成这种显式并行性?

当然,我自己可以在代码中进行交错 - 甚至是惊人的 - 但这会导致代码的可读性大大降低。如果我真的想要不可读,我可以去汇编!但是这种事情肯定有一些模式吗?

【问题讨论】:

  • 我认为这个问题很有趣也很重要,但是让我觉得不好的是“现在我有高度并行的任务。而且我通常有一个老化的单核 x86 处理器,没有 hyper ——穿线。”如果您没有可并行化的 CPU,那为什么要这样做?
  • 我相信混合“解析”和“在寄存器中执行操作”的想法根本不会带来任何加速,因为这是 CPU 供应商自己使用寄存器重命名等技术所做的事情, 存储转发。
  • 在一个线程中混合两个 VM 的解析和执行 - 自从提出这个问题以来 - 到目前为止已经提高了 16%。但是我的混合是反复试验,所以我极有可能还没有接近可能的改进。我仍在寻找一种非意大利面的方式来组织代码
  • 太棒了。测量总是正确的,因此我衷心承认我错了。我现在会尝试考虑一些可以帮助您的模板魔法(并不是说我会非常依赖我带来任何东西,但谁知道呢,也许我会遇到一些事情)

标签: c++ performance design-patterns


【解决方案1】:

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

【讨论】:

  • 这也是我想说的。当今处理器的线性代码流是虚构的。它只是看起来以这种方式工作,而代码的实际执行是通过分支预测和其他一切超乎想象的流水线。
  • 到目前为止,我所见过的任何 c++ 编译器都无法自行创建多核代码。您需要为此使用一些显式构造。
  • @Suma:如果你碰巧对我的回答投了反对票,请重新阅读这个问题,然后回答这个问题:当“而且我通常有一个老化的单核 x86 处理器时,多核代码有什么用?没有超线程。”保持上下文,人们!
  • 好的。然后,我将对这个问题投反对票。你是对的,如果你没有资源可以并行化,那么并行化计算是没有用的。
【解决方案2】:

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

【讨论】:

    【解决方案3】:

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

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

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

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

    【讨论】:

    • 微软的编译器确实支持 OpenMP。
    • 已修复,谢谢。如果您知道添加支持的版本,我也会添加它。 ICC 版本也是如此
    • “我通常有一个老化的单核 x86 处理器没有超线程。”他不想要多线程。这是一个完全不同的问题。
    【解决方案4】:

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

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

    【讨论】:

      【解决方案5】:

      当前的 C++ 标准不支持并行执行。这将在明年左右发布的下一个标准版本中发生变化。

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

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

      【讨论】:

        【解决方案6】:

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

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

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

        【讨论】:

          【解决方案7】:

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

          【讨论】:

            【解决方案8】:

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

            正如another answer 中已经发布的那样,OpenMP 是一种可移植的方式来执行此操作。但是我的经验是 OpenMP 开销非常高,很容易被它打败 滚动 DIY(自己动手)实现。希望 OpenMP 会随着时间的推移而改进,但就目前而言,我不建议将它用于原型设计之外的任何其他用途。

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

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

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

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2012-10-02
              • 1970-01-01
              • 1970-01-01
              • 2017-04-15
              • 2015-08-02
              • 2012-05-16
              • 2011-07-17
              相关资源
              最近更新 更多