在过去的二十年中,Adobe Photoshop 已成为全球数字摄影爱好者、艺术家和平面设计师事实上的图像编辑软件。其广泛的吸引力部分归功于其用户界面,该界面使得应用一些极其复杂的图像编辑和滤镜技术变得相当简单。然而,在该表象背后,隐藏着大量复杂且计算量大的代码。为了提高这些计算的性能,Photoshop 的设计师在 20 世纪 90 年代中期就成为了并行处理的早期采用者,他们努力利用当时由双处理器或四处理器驱动的尖端桌面系统提供的额外性能。当时,Photoshop 是为数不多的提供这种功能的消费级桌面应用程序之一。
Photoshop 的并行处理诞生于专用扩展卡时代,在过去十年中,对于已经出现的双核和四核机器,其扩展性一直良好。然而,随着 Photoshop 的工程师为即将到来的八核和十六核机器做准备,他们开始遇到越来越多的扩展问题,这主要是由于阿姆达尔定律和内存带宽限制的影响。
在这篇 案例研究中,Adobe Photoshop 首席科学家 Russell Williams 与英特尔集群就绪计划的架构师 Clem Cole 探讨了 Photoshop 团队如何应对这些挑战。虽然他们目前的职位代表了并行计算领域不同的方面,但他们都在操作系统层面处理并行处理方面拥有悠久的历史。
在加入 Photoshop 开发团队之前,Williams 曾在苹果等公司担任操作系统设计师,在那里他从事 Copland 微内核的工作,并在 Elxsi 帮助开发小型超级计算机。这种背景的多样性使他现在能够对堆栈不同层面的并行处理保持全面的视角。
Cole 是一位经验丰富的操作系统开发人员,在 Unix 内核和工具开发方面拥有多年的经验。他目前致力于推进利用多处理器的方法——使用英特尔的下一代多核芯片——这使他成为 Williams 的合适采访者,Williams 的工作在很大程度上建立在 Cole 在英特尔帮助创建的平台之上。
虽然 Photoshop 带来了一系列独特的问题和限制,但它提出的许多工程挑战对于任何曾经尝试在应用程序中实现并行处理的软件工程师来说无疑都会感到熟悉。尽管如此,为了了解 Photoshop 工程师今天面临的问题,我们首先必须考虑该应用程序在过去 15 年中与并行处理的历史。
COLE 您编写软件已经很长时间了,在过去的 11 年里,您一直在使用 Photoshop,并且越来越多地参与其并行方面的工作。其中哪些部分被证明是容易的,哪些部分被证明出奇地困难?
WILLIAMS 容易的部分是 Photoshop 实际上已经并行处理了很长时间。在一个非常简单的层面上,它有一些工作线程来处理诸如异步光标跟踪之类的事情,同时还在另一个线程上管理异步 I/O。使这类事情正常工作非常简单,因为问题非常简单。跨越该边界共享的数据很少,并且目标不是获得计算扩展;而只是让异步任务运行起来。
但是,我应该注意到,即使是队列磁盘 I/O 请求以便它们可以被另一个线程异步处理的极其简单的任务,我也知道 Photoshop 中寿命最长的错误最终也隐藏在该代码中。它在那里隐藏了大约 10 年。我们会打开异步 I/O 并最终遇到该错误。我们会搜索它数周,但之后不得不放弃并在未打开异步 I/O 的情况下发布应用程序。每隔几个版本,我们会重新打开它,以便我们可以再次开始寻找错误。
COLE 我认为 Butler Lampson 曾说过,串行机器的美妙之处在于你可以停止它们并查看一切。当我们在并行工作时,总是有其他事情在发生,因此停止一切以进行检查的概念真的很困难。实际上,我对您的错误能够在 I/O 系统中隐藏这么长时间并不感到震惊。
WILLIAMS 事实证明这是一个非常简单的问题。就像 Photoshop 的许多其他方面一样,这与应用程序最初是为 Macintosh 编写的,然后才迁移到 Windows 的事实有关。在 Macintosh 上,set file position 调用是原子的——单个调用——而在 Windows 上,它是一对调用。放入该代码的人没有考虑到,当您跨线程共享文件位置时,这对调用必须是原子的。
COLE 现在,当然,您可以回顾并说这很明显。
WILLIAMS 事实上,最初将该错误放入代码中的人在我们多次开始寻找该错误时,走下走廊,拍了一下额头,并意识到问题所在——在 10 年后。
无论如何,Photoshop 中我们在并行处理方面取得成功的另一个重要领域涉及基本的图像处理例程。每当您在 Photoshop 中运行滤镜或调整时,它都会分解为许多基本的图像处理操作,这些操作在通过跳转表访问的库中实现。早期,这使我们能够发布这些“瓶颈例程”的加速版本。在 20 世纪 90 年代,当公司销售用于加速 Photoshop 的专用 DSP(数字信号处理器)卡时,我们可以修补这些瓶颈,在加速卡上执行我们的例程,然后将控制权返回给 68-KB 处理器。
这为我们在应用程序中引入并行处理提供了一个绝佳的机会,这种方式不会使我们的瓶颈例程算法的实现复杂化。当调用其中一个例程时,它将被传递一个——或两个或三个——指向内存中字节的指针。它无法访问 Photoshop 基于软件的虚拟内存,也无法调用操作系统;它只是底层的数学例程。这为我们提供了一种非常简单的方法——在深入到数学例程之前——插入一些东西,将我们想要处理的内存块分割到多个 CPU 上,然后将单独的块交给每个 CPU 上的线程。
COLE 关键在于你有一个可以分解成更小对象的对象,而无需上层部件担心它。它还有助于你有一个干净的地方进行拆分。
WILLIAMS 另一个好的方面是底层的对象不需要知道同步。它仍然只是一个数学例程,它被传递一个源指针——或者可能是几个源指针和计数——以及一个目标指针。所有同步都存在于多处理器插件中,该插件将自身插入到瓶颈例程的跳转表中。该架构大约在 1994 年被放入 Photoshop 中。它使我们能够利用 Windows NT 的对称多处理架构,用于两个或四个 CPU,这在当时是高端机器上可以获得的。它还使我们能够利用 Macintosh 上的 DayStar 多处理 API。您可以从 DayStar Digital 购买 90 年代中期至 90 年代后期的多处理器机器,Mac 操作系统的其余部分无法利用它们——但 Photoshop 可以。
Photoshop 已经使用了十多年多处理器来执行其对像素进行的基本图像处理工作。在过去十年中,人们通常能够在系统中获得的 CPU 数量(即两个或四个处理器)范围内,这种方法扩展性相当好。从本质上讲,在所有这些系统中,都没有出现同步错误。
COLE 这是一个惊人的说法!您能否分享与此相关的见解?您认为我们其他人可以从中学习到什么?
WILLIAMS 我认为最大的成功来自于不必为要并行化的处理例程编写同步代码。换句话说,人们可以编写卷积核或他们需要的任何其他像素处理方面的内容,而无需担心正确处理所有这些同步问题。如果获取一个异步 I/O 线程就需要我们引入一个能够逃脱我们 10 年的错误,那么很明显,最小化同步问题非常重要。
也就是说,10 年前处理同步的方式涉及使用比我们今天可用的同步原语更易出错的同步原语。Windows 上的“进入临界区”和“离开临界区”之类的东西可能非常快,但它们也很容易出错。尝试跟踪您是否在可能需要它们的所有地方都放置了临界区,以及您是否记得离开的次数与进入的次数一样多,所有这些都可能非常困难且容易出错。
**
设法在 Photoshop 的同步代码中隐藏了 10 年的棘手错误,说明了并行编程是多么棘手。但它也突出了在改进资源以管理某些复杂性方面取得的进展。例如,如果 Photoshop 的同步是用今天的 C++ 基于堆栈的锁定编写的,那么不太可能引入此类错误。随着处理器获得更多内核并变得更加复杂,将迫切需要新的工具和更好的编程原语,以向开发人员隐藏复杂性,并允许他们在更高的抽象级别上进行编码。
与此同时,软件架构师还需要关注其他一些基本问题。例如,尽管在原始设计中使用了不太复杂的同步原语,但 Photoshop 团队基本上能够忘记复杂的线程同步问题,部分原因是图像处理问题本身非常适合并行化。此外,Photoshop 的架构也使得建立一些非常干净的对象边界成为可能,这反过来又使程序员可以轻松地分割对象并将生成的片段分散到多个处理器上。也就是说,Photoshop 的架构师非常清楚他们并行化的最佳机会在哪里,并且他们相应地设计了应用程序。
概括而言,很明显——无论工具和编程抽象方面是否有进步——为了使开发人员充分利用即将到来的多核架构,他们需要擅长识别程序中可以从并行化中获益最多的部分。正是在代码的这些部分,新的工具、技术和并行编程抽象最有可能产生最大的影响。
**
COLE 作为操作系统设计师,我们都在一个必须处理并行处理的世界中长大。我们为操作系统提出的解决方案是否被证明是正确的,这一点并不总是很清楚。在之前的对话中,您提到了您创建和删除互斥锁的经验。多年来,我们变得更聪明了。我们已经学会了如何做更有效率的事情,但这并不意味着它变得更容易了。我们有什么妙招可以使其更容易?
WILLIAMS 并行处理在几个方面仍然很困难。要求一个新的 Photoshop 滤镜来并行处理像素网格是一回事。说“我要并行化,从而加速 Photoshop 运行 JavaScript 动作的方式”又是另一回事。例如,我有一个 JavaScript 例程,它一个接一个地打开 50 个文件,然后对每个文件执行一组 50 个步骤。我无法控制该脚本。我的工作只是让它——或用户必须运行的任何其他脚本——更快。
我可以这样说,“重写所有脚本,以便我们可以设计一个全新的界面,让您可以指定所有这些图像都将并行处理。”这是一个答案,但这将需要用户和我们付出大量工作。并且它仍然会给我们留下与并行化图像的打开、解析图像内容、解释 JavaScript、通过应用程序框架运行关键命令对象以及更新用户界面相关的问题——所有这些通常都与应用程序框架捆绑在一起,因此涉及调用一些极其顺序的脚本解释器。一旦您开始查看此类事物上的阿姆达尔定律数字,很快就会清楚地发现,尝试将其并行化为八路将完全是徒劳的。
在光谱的另一端,您可能会发现,例如,一种数学算法,它在概念上适合并行处理,仅仅是因为它有许多需要共享的数据结构。那么正确实现该数学上可并行化的算法有多难?
我认为,在过去 20 年中,我们在处理并行处理的能力方面取得了一些渐进的进步,就像我们在所有其他编程方面都取得了逐步的进步一样。请记住,早在 70 年代,就有很多关于“软件危机”的讨论,关于软件如何变得越来越复杂,以至于我们无法再管理错误。好吧,为了应对这种情况,软件生产力并没有取得巨大的突破,但我们确实从面向对象编程、改进的集成开发环境以及更好的符号调试和查找内存泄漏的检查器工具的出现中获得了一系列渐进的进步。所有这些都帮助我们逐步提高了生产力,并提高了我们管理复杂性的能力。
我认为我们正在看到并行处理方面发生的事情非常相似。也就是说,早期的 Photoshop 同步代码是用“进入临界区,离开临界区”编写的,而我们现在拥有诸如 Boost 线程和 OpenGL 之类的工具,它们本质上是编程语言,可以帮助我们处理这些问题。如果您查看 Pixel Bender [Adobe 用于表达可以在 GPU 上运行的并行计算的库],您会发现它处于更高的级别,因此需要编码算法的人员进行更少的同步工作。
COLE 关键在于您每次都尝试提高到一个更高的级别,以便您需要处理的细节越来越少。如果我们能够自动化更多发生在下面的事情,我们将设法变得更有效率。
您还提到我们现在拥有比以前更好的工具。这是否表明我们需要更好的工具才能迈出下一步?如果是这样,我们缺少什么?
WILLIAMS 完全调试多线程程序仍然非常困难。调试基于 GPU 的编程,无论是在 OpenGL 还是 OpenCL 中,仍然处于石器时代。在某些情况下,您运行它,您的系统会蓝屏,然后您尝试弄清楚刚刚发生了什么。
COLE 我们很清楚这一点。我们试图构建更强大的库,以便程序员不必再担心许多低级的东西。我们还在创建更好的原语库,例如开源 TBB(线程构建块)。您认为这些是开发人员正在向供应商和研究界寻求的那种东西吗?
WILLIAMS 这些东西绝对有很大的帮助。我们现在正在认真研究 TBB。跨平台工具也至关重要。当有人推出仅限 Windows 的东西时,这对我们来说是不可接受的——除非 Mac 端也有完全等效的技术。Boost 或 TBB 等跨平台工具的创建对我们来说非常重要。
我们可以隐藏在更多库层下的东西越多,我们就越好。然而,最终限制这些库好处的一件事是阿姆达尔定律。例如,假设作为某些操作的一部分,我们需要将图像转换为频域。我们有一个 FFT(快速傅里叶变换)的并行实现,我们可以直接调用它,甚至我们可能在它之上有一个库来决定是否将所有这些都发送到 GPU 以执行 FFT 的 GPU 实现,然后再将其发送回来是否有意义。但这只是我们算法中的一步。也许下一步有一个并行库,但是从 FFT 步骤到我们调用并行库的步骤需要进行一些处理。正是所有这些步骤间的设置,阿姆达尔定律会让你崩溃。即使您只花费 10% 的时间做这些事情,也足以阻止您扩展到 10 个以上的处理器。
尽管如此,库方法非常棒,我们可以掌握的每个通用算法的并行库实现都非常受欢迎。然而,就像我们今天可用的许多技术一样,它在 8 到 16 个处理器左右开始耗尽动力。这并不意味着不值得这样做。我们肯定会自己走库路径,因为如果我们要扩展到 8 到 16 个处理器,这是我们唯一能想象到的可行方法。
**
对于 Photoshop 开发团队的工程师来说,阿姆达尔定律施加的扩展限制在过去几年中已经变得非常熟悉。虽然该应用程序当前的并行处理方案在双处理器和四处理器系统上扩展性良好,但在具有八个或更多处理器的系统上进行的实验表明,性能提升远不如人意。部分原因是随着内核数量的增加,正在处理的图像块(称为瓦片)最终会被切成更多更小的碎片,从而导致同步开销增加。另一个问题是,在每个以可并行化块并行处理图像数据的步骤之间,都有顺序的簿记步骤。由于这一切,阿姆达尔定律很快就变成了阿姆达尔墙。
Photoshop 的工程师试图通过增加瓦片大小来减轻这些影响,这反过来使每个子片段更大。这有助于减少同步开销,但它给开发人员带来了另一个并行计算的难题:内存带宽限制。使问题复杂化的是,Photoshop 无法中断像素处理操作,直到整个瓦片完成。因此,如果过度增加瓦片大小,肯定会导致延迟问题,因为应用程序将无法响应用户输入,直到它完成处理整个瓦片。
尽管 Williams 仍然相信他的团队可以在不久的将来通过使用更好的工具、库以及对当前并行处理方法的渐进式更改来继续提高 Photoshop 的扩展性,但最终这些技术将耗尽动力。这意味着现在是时候开始考虑将应用程序迁移到完全不同的方法,该方法涉及新的并行方法和更多地使用 GPU。
**
COLE 我认为您在图像处理方面已经有一些有趣的拆分事物的方法,但对于您的基本应用程序,您是否考虑过其他编程范例,例如 MPI(消息传递接口)?
WILLIAMS 不,我们没有,因为我们一直忙于从四核世界转移到八核到十六核世界,我们看到 Photoshop 在未来几年内仍将停留在那个世界中。我们没有认真考虑转向消息传递式接口的另一个原因是,这将需要如此巨大的重新架构工作,而且我们根本不清楚这对我们有什么好处。
COLE 我之所以问这个问题,是因为英特尔显然希望在一个盒子中尽可能多地启用内核,并且您提到您之前曾遇到过内存带宽问题。这部分是英特尔的另一个部门对 NUMA(非统一内存架构)的组合方式感兴趣的原因。我当然觉得我们将会有既有线程部分又有高度并行部分的应用,并且我们将会看到工作站内部的处理器在许多方面变得更像集群。我们可能不一定脱离芯片或脱离盒子,但我们将以某种方式分解内存。并且我们将不得不做很多其他事情来恢复一些内存带宽,仅仅因为这将对像您这样的人产生巨大的影响。
WILLIAMS 这以多种不同的方式出现。我们经常被问到我们将如何处理像 Larrabee [英特尔 MIC(多集成内核)架构的工程芯片] 这样的东西。答案是:目前基本上什么都不做。原因是,任何这些承诺解决某些特定并行处理问题或某些特定性能问题的未来架构,在这一点上都具有推测性。另一方面,Photoshop 是一款大众市场应用程序。因此,除非我们相当确定将会有数百万个这样的平台出现,否则我们无法承担将我们软件的架构方向押注于此的风险。目前,我们看不到桌面架构超越我们今天看到的温和且缓存一致的 NUMA 形式。
作为一项规则,我们避免编写大量特定于处理器或特定于制造商的代码,尽管我们确实进行了一些有针对性的性能调整。对于我们来说,当我们能够使用 OpenGL、OpenCL 和 Adobe 的 Pixel Bender 等库——或任何利用这些库的更高级别的库——以更通用的方式访问所有 GPU 性能时,生活将开始变得有趣。
我们还一直在关注英特尔 Nehalem 架构在该领域带来的变化。在所有之前的多核 CPU 上,单个线程基本上可以消耗掉所有内存带宽。鉴于我们的许多低级操作都受到内存带宽的限制,对它们进行线程化只会增加开销。我们在其他多核 CPU 上的经验是,它们在只有几个线程运行时就会受到带宽限制,因此并行加速受到内存带宽的限制,而不是阿姆达尔定律或算法性质的限制。对于 Nehalem,每个处理器内核都被限制为芯片总内存带宽的一部分,因此即使是一个大的memcpy也可以通过多线程处理得到极大的加速。
COLE 实际上,我只是想发表更多架构方面的声明,而不是其他任何内容。请放心,您将会看到尽可能多的内核在那里,但在某个时候,我所说的“内存带宽守恒”开始成为主要的权衡;那时将不得不使用其他架构技巧,这将对软件产生一些影响。问题是,如果您不能开枪让每个人在一夜之间更改软件,那么在什么时候,对于像 Adobe 这样的公司来说,说“好的,如果我知道我将不得不处理盒子里的集群,我必须慢慢开始移动我的基础,以便我能够做到这一点”在经济上变得有趣?
WILLIAMS 我怀疑我们最终会看到与 SMP 相同的进展。也就是说,将引入硬件和操作系统支持,以便这些平台能够运行多个程序,或程序的多个实例,但尚未修改为利用新架构。这已经证明对于 SMP 和 GPU 的使用是正确的。将会有一些少数应用程序绝对依赖于能够立即利用全新的功能——就像视频游戏和 3D 渲染应用程序及其尽快利用 GPU 的需求一样。然而,大多数应用程序直到以下情况才会开始显着利用新架构:(a)有装机基础;(b)有软件支持;以及(c)有明确的未来发展方向。
我假设 NUMA 和 MPI 的东西目前处于研究实验室级别。即使 MIC 芯片即将问世,除了 OpenGL 和 DirectX 之外,编程 API 将是什么仍然不清楚。
现在,只是想向您抛出一个问题:您认为编程 API 和操作系统支持在推出方面的进展是什么样的,因为像我这样的人如果想要利用这些架构创新,就需要这些支持?
COLE 随着我们开发专用硬件,无论是 GPU 还是网络引擎,机器本质上正在变成一个处理元素联邦,旨在处理特定任务。图形处理器经过高度调整,可以执行其显示像素和执行对图形人员和游戏玩家重要的某些数学功能的工作。然后其他人出现并说,“嘿,我想能够执行相同的函数。我可以访问它们吗?” 这就是像我这样的操作系统领域的工程师开始挠头并说,“是的,好吧,我想我们可以公开它。”
但我想知道这是否是您真正想要的。我的经验是,每次我们都有这样的专用引擎,并且我们试图将其提供给世界时,您可能已经能够像使用 Photoshop 一样编写一个应用程序库,该库能够调用某些图形卡,但这通常只存在几代人。也就是说,它被证明仅对该特定应用程序具有成本效益。因此,我认为 GPU 将继续变得越来越智能,但它将保持其作为图形引擎的重点不变。这真的是它最具成本效益的地方。
盒子的其余部分需要更通用,可能在其周围有很多专用执行引擎,您可以调用并轻松利用它们。然后问题就变成了:操作系统如何使所有这些引擎都可用?
作为早期的微内核人员之一,当我了解到您早期的微内核工作时,我笑了。我们操作系统社区中的许多人都认为这将是正确的方法。
WILLIAMS Elxsi 是一个基于消息的操作系统。它类似于 GNU Hurd 的独立进程,因为它通过消息进行通信。真正严重打击我们并且今天在 GPU 中以不同方式出现的——事实上,是我开始研究 NUMA 时想到的第一件事——是基于消息的操作相对于您所做的其他一切而言都非常昂贵。这也是视频应用程序也遇到的问题。他们沿着这条道路前进,进行渲染图来表示您在视频帧上拥有的效果堆栈,然后他们会向下走并开始渲染和合成这些东西。一旦他们获得可以在 GPU 上执行的任何操作,他们就会将其发送到那里进行处理,然后他们会继续处理它,直到他们遇到合成堆栈图中的一个无法在 GPU 上处理的节点,此时他们会将它吸回 CPU。
他们发现,即使使用最快带宽卡和最快带宽接口,也只能进行一次传输。超出此范围的任何操作都意味着您最好一开始就留在 CPU 上。当您开始移动数据时,与此相关的带宽消耗可能会迅速压倒计算成本。但 GPU 供应商正在不断改进这一点。
COLE 这部分就是我询问 MPI 的原因。我现在回到这一点仅仅是因为它似乎是当今流行的答案。我并不是说它就是答案;它只是一种尝试控制必须移动什么以及何时移动以及如何划分数据的方法,以便您可以编写能够利用这些执行单元的代码,而无需移动大量数据。
这就是英特尔等公司正在探索 RDMA(远程直接内存访问)等接口的原因,您可以在 IB(InfiniBand)内部找到它。大约一年前,英特尔收购了最初的 iWARP(互联网广域 RDMA 协议)供应商之一,并且该公司也坚定地致力于 OpenFabrics Alliance 的 OFED(OpenFabrics 企业发行版)实现,因此我们现在公开了您在以太网形式和 IB 中发现的相同 RDMA 接口。我当然认为这种硬件将开始出现在基本 CPU 内部,并且当您尝试移动那些对象时,它将对您可用。因此,您将拥有计算资源和数据移动资源,而处理能力将成为所有底层事物的联邦。
这意味着操作系统必须改变。我认为您是对的:接下来会发生的是应用程序将变得更丰富,并且将能够利用硬件基础中的某些部分,前提是操作系统公开它。那时,您在 Adobe 的人们也会想要开始利用它,因为您将拥有已经内置了这些功能的机器的客户。
WILLIAMS 当我们开始研究 NUMA 时,我们遇到了一些可预测性问题。理想情况下,在一个大型 NUMA 系统上,您会希望您的图像均匀地分布在所有节点上,这样当您执行操作时,您就能够启动每个 CPU 来处理图像的各自部分,而无需来回移动数据。
然而,实际情况是,您有一组图像,有人从中选择,并且可能从图像的中心部分选择一个圆形或区域,其中像素分布在 10 个节点中的 3 个上。为了分布用户随后对该选择调用的计算,您现在必须将这些数据复制到所有 10 个节点。您很快就会陷入数据碎片化严重的情况,并且任何特定的选择或操作都不太可能在 NUMA 节点上得到良好分布。然后,您需要付出巨大的代价来重新分布所有数据,作为启动操作的一部分。这,连同更普遍的带宽管理问题,将被证明是一个比人们已经充分记录的正确锁定数据结构的问题更困难的并行性问题。
COLE 是的,我们在这点上强烈同意。锁定数据结构确实只是开始。新的调优问题将完全就是您刚才描述的噩梦。
问
喜欢还是讨厌?请告诉我们
© 2010 1542-7730/10/0900 $10.00
最初发表于 Queue vol. 8, no. 9—
在 数字图书馆 中评论这篇文章