这是一篇英文翻译,原文见这里
异构应用程序,也即同时在 CPU 和 GPU 等多个处理器上运行代码的应用程序,具有天生难以优化的内在特性。您不仅需要考虑代码的不同部分在不同处理器上执行的情况如何,还需要思考它们之间的交互效率。是否有哪个处理器在不必要地等待其他处理器?是否不必要地复制了大量的存储器数据?对 GPU 的利用程度是怎样?是否有瓶颈?了解所有这些的复杂程度让胆小者望而却步。
显然,至少在部分程度上,答案是性能分析工具。DS-5 Streamline 性能分析器就是这样的一款工具,它最近还新增了一些面向 OpenCL 的有趣功能。Streamline 是 ARM DS-5 Development Studio 的一个组件,后者是面向任何 ARM 处理器上软件开发的端对端工具套件。
那么,在拥有 DS-5 Streamline 和复杂的异构应用程序后,您又将如何进行优化呢?这篇博文旨在为您提供一个起点,介绍 DS-5 工具以及关于优化的一些概念。
通过 DS-5 Streamline,可以连接至运行中的设备,实时检索硬件计数器。所选的计数器将显示在时间轴中,这可在同一个记录中包含来自 CPU 和 GPU 的数据。例如,上图显示了包含多个记录的时间轴。自上而下分别是用绿色显示的双核 CPU 活动、用淡蓝色显示的 GPU 图形活动,以及用红色显示的 GPU 计算活动。其后则是各种硬件计数器和其他系统记录。
除了时间轴外,在 CPU 方面,还可以深入探究要分析的各个进程,然后对应用程序的不同部分进行性能分析,一直细化到系统调用。使用 Mali GPU 时,可以指定性能计数器,将它们以图形方式显示在 CPU 的旁边。这样,您就能对图形和 OpenCL 计算作业进行分析,从而对核心及其组件中执行的处理执行高度详细的分析。 最近增加的 OpenCL 时间轴功能更进了一步,可对一系列内核中的单个内核进行分析。
那么,根据以上介绍的基本信息,复杂异构应用程序的典型优化流程是什么呢?
如果目标是为一个软件创建 CPU 和 GPU 组合解决方案,那么通常可以从 CPU实现 开始。这可以从您需要实施的算法中剔除干扰,然后用作所执行计算准确性的基准,以及性能参照,这样您可以了解将算法移植到异构处理器时能够获得的益处。
下一步通常是创建“初始”移植。这是从 CPU 到 GPU 的代码转换可以工作但又相对粗糙的地方。 在这一阶段,不一定要奢求有很大甚至是任何的性能提升,但如果没有别的工作,那就务必要建立一个可以运作的异构模型。
这时,您通常要开始考虑优化了。对初始移植进行性能分析,或许是不错的下一步骤,因为这通常可以凸显应用程序中的利用率,从中可以推断出要将精力集中到的地方。在这一阶段,通常要寻找的是如何以最佳方式在算法中实施并行化。
当然,为了能最充分地利用要使用的硬件,必须至少对目标架构有个基本的了解。所以,我们首先来了解 Mali GPU 的一些架构背景。
首先,下面是 OpenCL 执行模型到 Mali GPU 的映射情况。
工作单元就是着色管线上的线程,各有各的寄存器、程序计数器、堆栈指针和专属堆栈。 一个核心上最多可同时运行 256 个工作单元,每个工作单元都可原生处理矢量数据。
OpenCL 工作组(即工作单元的集合)也在各个核心上运行。 工作组可以包含屏障、局部原子,以及缓存的局部存储器。
ND 范围(即 OpenCL 作业的完整工作负载)将工作组拆散,并分配到可用的 Mali GPU 核心。 支持全局原子操作,而且也支持带缓存的全局存储器。
正如我们看到的那样,与其他一些 GPU 架构相比,Mali GPU 核心是相对复杂的设备,能够在任一时间同时处理数百个线程。
我们来更细致地看看其中一个核心:
此处显示了双 ALU、加载/存储,以及纹理管线。 线程从顶部进入其中一个管道,再转回到顶部等待下一个指令,直到线程完成,这时它从底部退出。我们通常有许多线程以这种方式运行,逐个指令围绕管线运转。
我们假设第一个指令是加载。它进入加载/存储管道并在其中执行。如果数据可用,线程可以在下个周期循环以执行下一指令。如果主存储器的数据尚未到达,指令将必须在该管道中等待,直至数据可用为止。
我们再假设下一指令是算术指令。线程现在进入其中一个算术管道。ALU 指令支持 SIMD(单指令多数据),允许同时在多个组件上进行多个运算。指令格式本身为 VLIW(超长指令字),支持单指令多数据运算。例如,这可以将矢量加法、矢量乘法,以及各种标量运算全都放在一个指令中。这就能达到特定运算表现为“免费”的效果,因为 ALU 中的算法单元可以在一个周期中并行处理许多这样的运算。最后,内置函数库“BIFL”对许多算法和其他运算函数具有硬件加速功能。
所以,这是个复杂而又强悍的核心,设计为同时运行多个线程,从而隐藏延迟。隐藏延迟是我们的终极目标。 只要管线能够继续处理其他的线程,我们不在乎是否有个别线程必须等待一些数据到达。
这些管线各自独立,与之类似,各个线程也完全相互独立。程序执行的总时间则由让每个线程执行程序中所有指令时所需周期最多的管线来决定。 例如,如果我们主要执行加载/存储运算,那么加载/存储管道将成为限制因素。所以,为了能优化程序,我们需要找出这个管线究竟是哪一个,以便我们能够高效地确定优化的目标区域。
为了帮助确定这一点,我们需要访问 GPU 的硬件计数器。 这些计数器将确定核心的哪些部分正在由特定的作业使用。反过来,这也有助于我们将精力投入到处理性能中的瓶颈。
此类硬件计数器有许多。 例如,有针对各个核心的计数器,以及针对核心中个别组件的计数器,让您可以对内部一窥究竟,了解管线本身的具体情况。而且,我们也有针对 GPU 整体的计数器,包括活动周期数量等等。
要访问这些计数器,我们得回到 DS-5 Streamline。我们来看一些 Streamline 工作时的屏幕快照。
要强调的第一点是,我们这里看到的是全系统视图。最上一行中的垂直绿条表示 CPU,下面的蓝条表示应用程序在 GPU 上运行的图形部分,红条则表示应用程序在 GPU 上运行的计算相关部分。
您可以通过各种各样的方式进行自定义,我这里就不做详细说明了。不过,您可以根据需要测量的具体内容,从系统的大量计数器中进行挑选。 Streamline 支持针对具体应用程序隔离 CPU 和 GPU 的计数器,让您能够专注于需要查看的细节。
在屏幕中往下,您可以看到 L2 缓存测量——这里中间的蓝色波纹记录,再往下我们会看到显示 Mali GPU 算术管线中活动的计数器。我们可以往下滚动查找更多信息,甚至也可在任一点上放大显示更为详细的视图。
DS-5 Streamline 通常可以非常快速地显示特定应用程序中问题出在哪里。下面这张图取自一个计算机视觉应用程序,它在 CPU 上运行并使用 GPU 上的 OpenCL。该程序在几秒钟内运行正常,然后速度似乎随机性地突然大大减慢,处理帧率丢失一半。
您可以看到,记录已捕捉到了这一减速发生的时刻。 在时间轴标记的左侧,我们可以看到 CPU 和 GPU 的工作效率比较合理。然后,突然延迟了许多,GPU 工作负载之间出现了非常大的空隙,CPU 活动激增。顶部绿条之间的红条表示平台上的系统活动增加。这一记录和其他类似的记录具有宝贵的价值,它们可以显示此应用程序的初始问题在于它对视频的流化和处理方式。
拥有这种全系统视图的一大优点是,我们可以获得应用程序在多个处理器和多种处理器类型上的性能全貌,而这在本例中尤其有用。
这里,我们向下滚动到了时间轴中的可用计数器,展示一些其他信息,特别是 Mali GPU 核心内的各种活动。您可以看到许多方面的计数器行,尤其是算术、加载-存储管道和纹理管道,以及缓存命中和未命中数等。将鼠标悬停在这些图中时间轴的任意点上可以显示实际的计数器数目。
例如,我们可以在顶部看到加载/存储管道指令发布数,在底部看到实际的指令数。此情形中的差别是对时间轴中这一点上所需加载/存储重新发布数的测量,本质上是对存储器访问效率的测量。我们在这一点上看到的内容代表着这一方面合理的健康状态。
下一记录取自我们早前探讨的同一应用程序,但这次启用了更加复杂的 OpenCL 过滤器链。
如果我们更细致一些,可以看到该应用程序的运行效率如何。我们展开了 CPU 记录(顶部的绿条),以显示此平台上具有的两个核心。请记住,图形元素为蓝条,图像处理滤波则由红色代表。
我们看看应用程序对每一帧上要经历的周期:
所以,在一个快照中,我们有了全面的异构应用程序运行状况概貌。显然,我们可以在此把目标定为提高性能,方法是通过管线化工作负载来避免我们看到的闲置。没道理不让 CPU 和 GPU 更加高效地并行运行,从这条记录可以明确看出这点。
DS-5 Streamline 有许多功能,这里就不一一详述。但有一个要特别说明,它将最新发布的 Mali GPU 驱动程序与最新版本的 DS-5 (v5.20) 衔接起来,那就是 OpenCL 时间轴。
在此图中,我们已启用了这项功能,即底部的水平区域。这里显示了每个 OpenCL 内核的运行状况、运行它们所需的时间,以及 CPU 和 GPU 之间同步点的任何开销,等等。
这里,我们可以看到正在运行的各个内核的名称,以及支持的主机端设置进程。如果将鼠标悬停在此时间轴的任何部分上…
… 我们可以看到关于该内核或运算所需的个别时间的详细信息。在了解如何确定优化的目标区域方面,它有着宝贵的价值。
以下是这一功能的另一视图。
我们可以单击“Show all dependencies”(显示所有依赖项)按钮,此时 Streamline 将以图形方式显示内核的交错方式。同样,这些都显示在时间轴内,与系统的这个完整视图完美搭配。特别是对于复杂的多内核 OpenCL 应用程序而言,能够进行这样的操作将是开发人员的一个非常有价值的工具,有助于了解和改进要求越来越高的应用程序的性能。
那么,有了这些硬件计数器后,应当如何加以利用呢?
一般而言,首先要关注的是对存储器的使用。 SoC 在系统中仅有一个由编程器控制的存储器;换而言之,它没有局部存储器,都是全局的。CPU 和 GPU 对此存储器具有相同的可见性,并且通常具有共享的存储器总线。因此,存储器访问若有任何重叠,可能会导致问题。
如果我们希望在 CPU 和 GPU 之间来回切换,则不需要复制存储器(如桌面架构上那样)。相反,我们仅需要执行缓存清空。这些也需要一定时间,应尽力缩减。 我们可以通过 Streamline 获取程序的概貌,了解何时 CPU 在运行,以及何时 GPU 在运行,这一方式与我们之前看到的时间轴相似。我们可能要优化同步点,以便 GPU 或 CPU 仅需等待必要时间。 在以可视化呈现这些信息方面,Streamline 效果非常不错。
在优化存储器访问后,下一步是更加细致地观察内核的执行。正如我们所看到的,借助 Streamline,我们可以放大到内核的执行,判断单个管线的执行情况,特别是确定哪一管线是限制因素。此处的关键是对峰值优化的测量,以便找出每个周期发布指令的限制管道。
前面提到过,我们拥有可容忍延迟的架构,因为我们预计在任一时间上系统中都会运行大量的线程。 然而,寄存器使用方面的压力将限制同时保持活跃的线程数量。当掉队的线程达到一定数量时,这将带来延迟问题。这是因为,如果每个线程都需要很多寄存器,整体上用于这些线程的寄存器数量就不够。而这本身体现为限制管道中发布的指令数过少。而且,如果我们使用的寄存器数过多,将有会数据溢出并返回到主存储器,因此我们会看到额外的加载/存储操作。编译器可以管理所有这些工作,但这样做会有性能影响。
寄存器使用过量也会导致我们可以利用的本地工作组大小上限缩水。
解决办法是使用较少的寄存器。如果可能,我们可以使用较小的寄存器类型。所以,可行的话就从 32 位换为 16 位。或者,我们将内核拆分为多个内核,各自带有较少数量的寄存器。我们曾经看到非常大的内核运行情况不佳,但拆分成 2 个或更多内核时,总体性能增强许多,因为各个内核需要的寄存器数量变少。这允许同时运行更多的线程,因而提高了延迟容忍度。
最后,我们来看看缓存使用。如果这方面表现不佳,我们会看到许多 L/S 指令围绕 L/S 管道徘徊,等待它们请求的数据被返回。 而这涉及到重新发布指令,直至数据可用。有 GPU 硬件计数器正好能显示我们所需的信息,DS-5 就可以给出这些计数器。
这仅仅是与 Mali GPU 相关的计算优化世界的冰山一角,还有许多留待您去探索。为此,我在下方提供了 malideveloper.arm.com 上的一些链接,您可以从中查看各种有用的指南、开发者视频和论文等。
下载 DS-5 Streamline: ARM DS-5 Streamline - Mali 开发者中心
Mali-T600 系列 GPU OpenCL 开发者指南: Mali-T600 系列 GPU OpenCL 开发者指南 - Mali 开发者中心
GPU 计算、OpenCL 和 RenderScript 教程: http://malideveloper.arm.com/develop-for-mali/opencl-renderscript-tutorials/