Featured image of post ISPC 的故事

ISPC 的故事

The story of ispc.

封面来源:@sesmkun

原文来自 The story of ispc: all the links ,叙述深入浅出,写得很有意思。 简述提取了笔者觉得比较核心的观点。

简述

起源

Larrabee(LRB)的失败。

自动向量化不是一种编程模型

自动向量化可能而且确实会失败,使用它的程序员需要深入理解自动向量化编译器,需要去关心为什么代码没有成功向量化,而编译器版本一更新,生成的代码又是不可预测的了。

只考虑外层循环向量化,忽略内层的会存在的程序实例之间通信的情况,决定了注定做不好。

SPMD 编程模型

编程模型的存在,是为了更好地把程序映射到硬件上。

volta 诞生 & 全力投入 volta

主要其实是在指出 Intel 存在的弊病,大公司高度政治化的环境,抨击技术上贡献寥寥,但在政治上投入很多的蛀虫。

C 语言的影响及在 SIMD 上实现 SPMD

volta / ispc 遵循了 C 的设计哲学。

编译器优化与转换

为 SIMD 硬件编译 SPMD 程序是一种编译器转换,这与编译器优化完全是两回事。

增加几步掩码的操作,可能会带来性能的损耗,但是它的结果是确定的,它的方法是通用的,这种转化,使得它的可用性提高。

初步基准测试结果

volta 结合来自 GPU 的编程模型与 LLVM 得到的初版,丝毫不逊色于其他团队专家反复优化的编译器。

又继续抨击内部团队政治斗争。

首批用户与现代 CPU 的到来

在内部并行编程模型比拼中,对于性能上的领先,作者更加专注于开始将它用于更复杂的、其他模型无法处理的程序

有分叉控制流,当时还不被支持为向量指令的操作等,带来的开销,但能更快?

乱序执行掩盖了大量的瑕疵。英特尔 CPU 非常擅长运行糟糕的代码。

构建 AVX 后端及回馈 LLVM

自动向量化器处理的代码,由于 AVX 的出现,会需要把用 SSE 内部函数编写的代码用 AVX 的内部函数重写,这样的反复显然吃力不讨好。

完善 volta 支持 AVX 的时候,反哺 LLVM。

关于优化和性能的更多内容

GPU 能够在运行时获取如 gather 或 scatter 的相干性情况,而 CPU 需要在编译的时候尽量搞清楚。

对显著影响性能的情况,引入适当的情况判别优化,尽量不伤害程序的可预测性。

开源发布与 volta 的终结

作者全然是为了 volta 才留在 Intel,不同意开源后,立刻决定提交辞呈。最终,RIP volta, ispc 长存,Intel SPMD Program Compiler,参半的结局。

保留提交信息,虽然会把有些尴尬的探索公之于众,但能留下更多历史细节吧。

传播理念与离开英特尔

InPar 与英伟达的 GTC 大会同期举行,这意味着会议重点 heavily 偏向 GPU。说到"重点 heavily 偏向 GPU",我的意思是我们的论文是唯一一篇关于 CPU 的。然而,在听众的大力支持下,我们赢得了最佳论文奖。我们的奖品是一块顶级的英伟达 GPU。

显得幽默了。

在演讲后的问答环节中,一位研究生坚持认为,我在结果中报告的在一台 40 核机器上实现的 180 倍加速纯粹归功于多线程,我怎么确定 SIMD 起了任何作用?而且,据他说,现在没有一个有趣的工作负载不是大规模并行且能在 GPU 上运行良好的,因此让东西在 CPU 上跑得快并没有什么意义。

我其实当时了解到 ISPC 出来比 CUDA 晚的时候,有类似的疑惑,能在 GPU / CUDA 上做得更加彻底,所以这意义到底有多大呢?

继续提到为了防止 ISPC 死于职场政治斗争的个人努力,不进一步正规化。

于是,我辞职了,那次是认真的。当我解释原因时——正是他批准了我最初请求的人员编制,让我意识到是时候离开了——Geoff 有点惊讶,但他表现得非常冷静,令人佩服。

这看起来非常反直觉,但又是自然的。

回顾与反思

设计一个东西来解决你自己的问题可能很危险:最坏的情况是,它对其他任何人都没用。但这总比设计一个对你没用、但你想象别人会想要的东西要好。

然后是作者认为的 ISPC 的不足:侧重于 32 位数据类型每个源文件固定一个 SIMD 向量宽度unmasked 关键字显式向量与 SPMD嵌入 C++

最后是变扭的,由于公司利益被拒绝的 PR。(虽然最后 Jean-Luc 以他的提交权限为代价,合入了

后记

Excellence withers without an adversary: the time for us to see how great it is, how much its force, is when it displays its power through endurance. I assure you, good men should do the same: they should not be afraid to face hardships and difficulties, or complain of fate; whatever happens, good men should take it on good part, and turn it to a good end; it is not what you endure that matters, but how you endure it.

— Seneca, On Providence

我是否会在一个没有几个我一心想要证明他们是错的、且颇具影响力的混蛋的环境中写出它?

我并不认为混蛋是进步的必要因素,但我忍不住去想,最终他们是否以其特有的方式为 ispc 做出了"贡献"。

这里就只是作者的一些哲思了。

附录(原文 AI 翻译)

以下中文版由 DeepSeek 翻译。

ispc 的故事:起源(第一部分)

我决定写下一些关于 ispc 的历史,这是我在英特尔时写的一个编译器。要说的东西很多,所以会在接下来几周里分成一系列文章发布。虽然我尽力确保所有细节准确并恰当地归功于相关人员,但这都是凭我的记忆所写。如果当时在场的任何人发现任何事实错误,请发邮件告知。

Larrabee 的挽歌
要理解 ispc 的起源,了解一点关于 Larrabee 的知识会有所帮助。Larrabee(LRB)是英特尔尝试构建高端 GPU 的项目。这个项目大致时间跨度是从 2005 年到 2010 年。在多年来图形处理器一直只能使用落后的半导体工艺线和极小的芯片面积之后,英特尔打算用 Larrabee 大干一场:推出基于 PCI-Express 卡的 GPU,采用领先的半导体工艺,真正在高端市场参与竞争,目标是能够与 AMD 和 NVIDIA 抗衡。

英特尔的高管们爱上了 Larrabee,因为它基于 x86 架构。“看,x86 什么都能做!我们不需要构建某种奇怪的 GPU 架构就能在图形领域取得成功。“我敢肯定他们都是这么告诉自己的。这是一个诱人的提议,表面上看也似乎合理。只需在每个核心上增加一个大的向量单元,增加一些纹理单元,让某个程序员写点代码,然后接下来你就知道,你就在销售更多高利润的芯片,同时还能打击 NVIDIA 和他们的 GPU 计算野心。(而且 LRB 的想法在相当长一段时间里对我来说也似乎很合理,尽管我对指令集和 CPU 架构的文化性依恋不像公司里其他人那么深。)

Larrabee 未能成功的原因有很多,也许我以后会写点东西谈谈我对此的看法。(与此同时,Tom Forsyth 就他对此主题的看法写了一篇不错的文章,值得一读。)

其中一个主要问题是,每个核心上都有一个 16 宽的向量单元,但除了专门为 DX 和 OpenGL 编写的着色器编译器之外,没有其他好的方法来编写实际使用该向量单元的代码。如果你没有点亮向量单元,那么你只发挥了 Larrabee 潜在性能的 1/16;在那种情况下,你还不如在数量更少、但主频更高、具有乱序执行、更大缓存等特性的常规 CPU 核心上运行。

我曾多次看到一位 LRB 硬件架构师出去告诉开发人员,LRB 非常棒,因为他们可以像往常一样用 C 语言编程,只需重新编译他们已有的代码,就能获得数 TFLOP 的性能。

我们都会试图向那些相信只需重新编译就行的硬件架构师解释,事情没那么简单,没错,尽管多线程编程已被程序员们很好地理解,但你仍然需要为向量单元做点什么,老实说,在这方面当时一无所有。通常的回应是对方略带茫然地点头同意——好吧,也许没那么简单,但实际能有多难呢?这与许多软件人员开始感到的恐慌形成了鲜明的对比。

总的来说,英特尔的硬件架构师对编程的了解少得惊人(Forsyth 那家伙除外),而且我确信那些持这种想法的人真的相信如此。(公平地说,我对实际做硬件架构也知之甚少,不过我想我不会出去对硬件架构师胡扯如何最好地实现分支预测器。)

英特尔的编译器团队向硬件架构师保证,一切尽在掌握。他们拥有业界最好的循环向量化器——一旦他们为 LRB 写好新的后端,我们就万事大吉了。C、C++,甚至 Fortran 程序员将能够轻松点亮那 16 个向量通道,甚至无需思考。(只是为了校准一下:英特尔拥有业界最好的 Fortran 编译器也是他们引以为傲的一点。)

也有少数人对编写内部函数(intrinsics)感到兴奋——Mike Abrash 和 RAD 的其他优秀程序员正在编写光栅化器,他们几乎只想要这个,而 Tim Sweeney 也对这种可能性垂涎欲滴。我想,他们如此青睐内部函数选项这一事实,让硬件架构师觉得我们这些敲警钟的人只是水平不高的程序员,因此不值得担心。(澄清一下,与 Mike Abrash 和 Tim Sweeney 相比,我是个蹩脚的程序员。)但我谦卑地建议,构建一个世界上只有 5 个人能编程的可编程硬件,可不是一个制胜策略。

最终,并非向量单元编译器的缺失注定了 LRB 的失败:硬件延期了,软件光栅化器也延期了,而且整个项目遭遇了市场转向,即能效比几年前重要得多——消费者需要移动和电池供电的计算设备,而 LRB 架构的能效低于传统的 GPU 架构。

所以 LRB 走到了尽头,但至少我们现在在(部分)CPU 上有了 AVX-512。不过,从 LRB 的经历中,我们当时在场的很多人都清楚地认识到,这个向量单元的问题是一个需要解决的重要问题,即使只是为了 CPU,因为通过 SIMD 可用的处理能力越来越多。

让我们与编译器团队一起解决这个问题!
长期以来,由于专注于为执行密集矩阵数学的常规循环生成优秀代码,英特尔编译器团队的大多数人否认除了他们的自动向量化器之外还需要任何其他东西来处理向量单元的利用问题。我们很快陷入了一个循环:

  1. 他们会通知图形部门的人,他们已经根据我们的要求改进了自动向量化器,并且它实现了我们要求的所有功能。

  2. 我们尝试使用,发现虽然有所改善,但天哪,很容易写出实际上没有被编译成向量代码的代码——它会不可预测地失败。

  3. 我们给他们提供失败的案例,几个月后,他们会通知我们最新版本已经解决了问题。

  4. 如此周而复始。

很容易就会偏离向量化的路径。他们起初试图修补,但最终他们举手投降,提出了 #pragma simd,这个指令会禁用自动向量化器中"向量化此循环是否安全"的检查,无论如何都会对后面的循环进行向量化。(一旦提出用 #pragma 来解决难题,你就知道情况不妙了。)

于是有了 #pragma simd,它算是有点用,除非你调用了外部函数;那个问题从未得到解决。他们始终不理解为什么会有人想要编写完全使用所有向量通道运行的大型系统,并且无法想象这是一个重要的用例。(细心的读者可能会意识到,这种执行模型精确地描述了 GPU。)

自动向量化不是一种编程模型
我认为,编译器团队试图使其方法奏效的根本缺陷,最好由 T. Foley 诊断出来,他对此类问题充满了深刻的见解:自动向量化不是一种编程模型

自动向量化器的问题在于,只要向量化可能失败(而且它确实会失败),那么如果你是一个真正关心编译器为你的程序生成什么代码的程序员,你就必须深入理解这个自动向量化器。然后,当它未能将你希望向量化的代码向量化时,你可以要么用正确的方式"戳"它,要么以正确的方式修改你的程序,让它重新为你工作。这是一种糟糕的编程方式;这完全是炼金术和猜测,你需要对单个编译器实现的细微之处变得非常专业——而这本来是你完全不需要关心的事情。

当编译器新版本发布并更改了自动向量化器的实现时,愿上帝保佑你。

而有了合适的编程模型,程序员学习这个模型(希望它相当清晰),一个或多个编译器实现它,生成的代码是可预测的(没有性能悬崖),大家皆大欢喜。

在这个过程中,英特尔的许多图形人员试图向编译器团队的人解释,GPU 编程模型中有一些有趣的东西,他们最好去理解一下,而且这些想法不仅可以有益地应用于 LRB,也可以应用于通用的 CPU 向量编程。

这些有趣的东西归结为 SPMD 编程模型,GPU 程序员通过着色器和像 CUDA 这样的语言对此很熟悉:你编写的代码看起来 mostly 是串行的,只是描述了对单个数据元素(顶点、像素等)的计算。反过来,该代码在硬件上并行运行,处理许多不同的输入——许多顶点同时被变换,许多像素一起被着色,等等。

在这个模型中,并行性是隐式的。在大多数情况下,程序员只需要考虑对一块数据进行操作,而不需要担心他们的程序如何映射到硬件。(在 CUDA 以及更新版本的 DirectX 和 OpenGL 中,情况并不总是那么简单,但大体上是这样。)并行执行是自动处理的,只要你给 GPU 提供足够多的独立工作去做,你就能获得很高的并行利用率。

正如图形程序员所了解到的,SPMD 是编写高性能并行代码的一种非常好的方式。当然,它不像自动向量化器所处理的代码那样具有串行语义,而串行语义只要不抑制性能就很好,但就并行编程模型而言,SPMD 概念清晰,并且相对容易编译到 SIMD 硬件。(关于这一点后面还会详谈。)大多数编写着色器的程序员完全不需要考虑他们的程序是并行的。

你看,这其实不是向量化问题……
回顾过去,我认为英特尔的编译器人员对这个问题思考错了,而我们图形部门的人未能弥合分歧,让他们像我们一样看待这个问题。(但我们确实努力尝试过。)对他们来说,这是一个外层循环向量化问题:你不是在向量化内层循环,你只是在向量化程序的最外层循环。虽然这在某种意义上是对问题的准确描述,但在我看来,这总是一种奇怪的思考方式。(例如,它忽略了在某些 SPMD 模型中可以表达的多个运行中的程序实例之间通信的概念。)

这种思维方式的缺陷从他们的一位首席架构师在这些讨论中反复提到的一个细节变得清晰起来:“当 CUDA 编译器向量化失败时会发生什么?“他对 CUDA 中如何处理这个问题感到困惑。给人的感觉是,他觉得只要他能理解这一点,那将是修复英特尔自动向量化器并让我们闭嘴的关键。

当然,CUDA 根本不进行向量化,因此 CUDA 从不"向量化失败”;这个问题没有意义。你编写你的程序,虽然它看起来 mostly 是串行的,但它可以也将会在 GPU 上并行运行,因为这就是编程模型,而且它能很好地映射到硬件。就这样,完成了。

我们真的努力解释过很多次,但这些解释从未被接受。

不久之后的一次会议上,同一个人愤怒地告诉我们:“我不会告诉丰田如何设计汽车;我可能会请求功能,但如何设计是他们的工作。“他和其他人厌倦了图形部门的人试图告诉他们如何改进向量编程模型,以及他们当前的模型不足以满足我们想要编写的那类程序。我们也厌倦了一遍又一遍地说同样的话而毫无进展;在那个时候,似乎不可能说服他们为此做点什么。

敬请期待下一部分,内容包括:在瑞典度过的一个夏天,以及一些开始变得有趣的 LLVM 捣鼓经历。

ispc 的故事:volta 诞生(第二部分)

和之前一样,这都是凭记忆所写,但我已尽力确保准确。如果你当时在场并发现我写错的地方,请发邮件给我。

我一直非常喜欢理查德·汉明在贝尔实验室发表的演讲《你与你的研究》。我试着每年重读一次讲稿;里面充满了宝贵的建议。其中一个让我印象深刻的部分是关于赢得诺贝尔奖的诅咒——许多诺贝尔奖得主在获奖后最终都没有再做出任何有趣的工作。

汉明的诊断是:

当你成名后,就很难再研究小问题了。这就是香农所遭遇的。在信息论之后,你还能拿出什么更精彩的作品呢?伟大的科学家常常犯这个错误。他们未能继续播种那些能长出参天橡树的小橡子。他们总想一下子就搞出大成果。但事情不是这样发展的。

我既没有诺贝尔奖的负担,也不是什么伟大的科学家,但我真的很喜欢这个见解。最好是到处摸索、探索事物,不要一开始就制定宏伟计划,但要准备好在那次探索给你指明一个有趣的方向时集中精力。

在瑞典编程
2010 年夏天,我在瑞典度过,与 Tomas Akenine-Möller 和他召集的杰出智囊团一起工作。那五个人加起来对光栅化和实时渲染的了解比世界上几乎任何人都多;当时,他们正在进行各种关于高效高维光栅化以处理运动模糊和散焦模糊的有趣工作。

他们送了我一份小礼物欢迎我,如下图。(还有:土豆和新鲜莳萝。)夏天结束时,我坦白说我没有吃腌鲱鱼,不过很高兴地喝完了阿夸维特酒。我的记忆是,他们中的大多数人都同意腌鲱鱼没那么好吃,而且他们也没指望我真的吃。

在瑞典期间,我开始捣鼓 LLVM,以为这只是件像小橡子一样的事情,很可能不会有什么结果。深入研究 LLVM 是另一件得益于 T. Foley 的事情,他对 LLVM 的设计和能力非常热情。

至少,学习如何使用 LLVM 也让我觉得这将为我的程序员技能带添加一个有用的新工具。那年夏天,Steve Parker 等人关于 OptiX 的论文发表了;他们通过即时编译生成专门的高性能光线追踪器——这是思考该问题的一种非常有趣的方式,通过巧妙运用编译器技术得以实现。正是这类事情让我对代码生成感到兴奋。

LLVM 将中级程序 IR 作为输入,并从那里开始进行优化和生成原生指令。这样的想法很吸引人:如果我编写高级编译器部分和早期编译通道,那么我就可以让 LLVM 完成剩下的工作,直到生成优化的汇编代码。这种可能性使所有这些编译器相关的东西对我来说有趣得多,尽管我对它将走向何方并没有明确的计划。

不幸的是,那个夏天我最终没能如我所愿地与 Tomas 和其他人进行那么多深入的技术工作——至今仍有些许遗憾。部分原因是英特尔会议的开销;每天下午我都会提早回家,打上几个小时的电话,参加在美国那边早上开始的会议,另一部分原因是我自己花时间捣鼓编译器去了。

当时我没有对他们多说我的编译器黑客行为;我仍然不知道它会变成什么样子,而且我最初拥有的东西看起来并不那么有趣。老实说,在最初的几个月里,它是如此缺乏创新性,让我有点尴尬,尤其与周围发生的所有真正聪明的光栅化东西相比。

psl 的短暂生命
当我开始捣鼓 LLVM 时,我需要为我未来的编译器起个名字并找一个初始的用例。我的第一次尝试是 “psl”,代表"便携式着色语言”。可移植性本身从来不是我这个项目的最大目标;回想起来,我不确定当初为什么选择这个名字。

我从 Geoff Berry 和 T. Foley 为一种基于 C 的语言编写的解析器开始;反过来,我认为(但不完全确定)那是基于 Jeff Lee 为 ANSI C 编写的 lex 文件和 yacc 语法。有了这个基础,我开始编写一个处理 C 语言子集的基本编译器,构建抽象语法树(AST),对其进行类型检查等通道处理,所有这些都是编译器 101 的内容。我编写了将 AST 转换为 LLVM IR 的代码(没有为我自己添加额外的中间表示!),然后呼哧呼哧地开始看到看起来不错的(标量)x86 代码。

我以前从未写过编译器,所以所有这些都充满了乐趣;与我之前觉得它对我想自己编写的程序用处不大时相比,我现在更有动力去学习所有那些编译器知识。

我的想法是,这个着色语言可能会走向某个有趣的方向;我可能会从 C 语言演变成一个简洁的、用于着色的小型领域特定语言,并能做一些有趣的事情。也许未来版本的《基于物理的渲染》会有一章关于这个东西——谁知道呢?我尽量不去过分担心它会走向何方;这有帮助,因为我玩得非常开心,享受着 LLVM 最终吐出的优化良好的指令,即使我的编译器在功能上没什么特别之处。

在某个时刻,我好奇 LLVM 是否会生成良好的 SIMD 代码。我真希望我记得我尝试这个实验的确切原因;可能当时觉得用着色语言结合 SIMD 一次着色多个点会很有趣,但老实说我不记得了。

无论如何,我修改了 psl,将标量变量视为 4 宽向量,并将一个小程序编译到 LLVM 的 SSE4 目标平台。我很确定那个程序是:

1
float foo(float a, float b) { return a + b; }

然后,砰,我得到了:

1
2
	addps	%xmm1, %xmm0
	retq

这真是令人无比激动——我不能再要求更好的结果了。

从那里开始,添加支持更多算术操作——乘法、除法等——变得很容易,当我编写更长的(直线型)程序时,我发现编译器生成的指令看起来仍然很棒——就像我手写的一样。psl 还不能处理通用的控制流,但事情正迅速变得有趣起来。

volta 登场
随着编写一个以 CPU SIMD 指令为目标的通用 SPMD 语言的想法开始吸引我,着色语言的构想逐渐淡去:也许我可以尝试解决这个问题,因为我们与之交谈的英特尔编译器团队的人肯定不会去做。当他们没有对我们这些图形部门的麻烦制造者说"我们已经做到了”(#pragma simd)时,他们就会说"这行不通"或"这是不可能的”,这些立场之间的逻辑不一致显然不是他们担心的问题。

他们是编译器专家,所以在那个时候,我认为完全有可能我沿着这条路走下去,会发现他们一直是对的,并且这个问题比我理解的更复杂。再次说明,我以前从未真正写过编译器。那样的结果也可以接受,能学到关于计算的新东西,并对他们的立场产生新的尊重。

回到汉明的小橡子,我绝不会从一开始就决定承担"为 CPU SIMD 编写 SPMD 编译器"这个问题,但现在我发现,我已经通过黑客行为把自己带到了一个似乎可以设想它的位置。我已经积累了足够的基础设施,可以设想一连串的小步骤,如果它们都成功的话,就能把我带到某个有趣的地方,而且我对 LLVM 不会让我失望有足够的信心,愿意在代码生成部分继续押注于它。

一旦我有了更具体的目标,最紧迫的问题自然是为这个东西重新命名;“psl” 不再合适了。像通常做法一样,我向比约克寻求灵感。肯定有某个专辑标题或歌曲名字我可以拿来用——有点古怪,有点异国情调,但暗示着非常酷的东西。

放弃了《吃掉菜单》之后,我选定了 “volta”。我喜欢它所蕴含的电力和能量的感觉,而且,更妙的是,我找到了比约克解释她为何选择这个名字作为专辑名的精彩引述。我在英特尔介绍它时,会用这张幻灯片开始:

好了,这很合适。听起来也挺适合一个编译器。

下次,我们将涵盖至关重要的早期管理支持,以及一点关于 SIMD 上的 SPMD 的基本思想。然后,当我分享早期成果时,与英特尔编译器团队又一次激动人心的互动!

下一篇:全力投入 volta

注释

  1. 事实证明,这就是我目前探索用于渲染的机器学习所处的状态。我只是在学习如何用好 TensorFlow,并尝试重现别人写的关于去噪的几篇论文。有时我觉得我反而应该提出一个关于 ML 用于渲染的宏伟愿景,充满详细的计划和深刻的见解。幸运的是,谷歌一直非常支持我所采取的方法,我相信最终会取得好结果,即使目前我感觉自己的产出看起来并不特别显著。 ↩
  2. 如果我的记忆有误,错误地表述了 Tomas, Jacob, Robert, Jon, 和 Petrik 对瑞典国粹的感受,我在此道歉。 ↩
  3. 趣闻:2012 年,在听我提到 ispc 的原名后,Dave Luebke 随口问我那个名字是从哪里来的。当时,我怀疑 “volta” 可能是英伟达未来某款 GPU 的代号。(他们已经推出了 Fermi 和 Tesla,所以选择 Volta 并非不可想象,因为那里似乎有一条研究电力和能量的科学家的共同主线。)果然,在 2013 年,Volta 出现在他们的路线图上。它于 2017 年底上市。 ↩
  4. 另一个趣闻:在我离开时(2012 年),幻灯片套件在英特尔内部仍然被称为 “foils”,这个名字自使用 overhead projectors 做演示的时代起就一直沿用。我猜想这个命名法现在仍在用。 ↩

ispc 的故事:全力投入 volta(第三部分)

从瑞典回来后,我在英特尔的日常工作并不涉及编写编译器;我当时是高级渲染组的技术负责人。那时,我向 Elliot Garbus 汇报,他当时是负责图形软件的副总裁。

Elliot 是我遇到过的最好的经理。老实说,起初我并没有这种期望:虽然他职业生涯早期是技术出身,但他已经多年没有亲自动手做任何具体技术工作了,而且他的背景也不在图形领域。我不太确定我们是否有足够的共同点来建立良好的关系,但至少他看起来人很不错。

结果证明,Elliot 有着令人印象深刻的知识好奇心;当我与他谈论我和渲染组正在做的事情时,他总是能提出有见地的问题。随着时间的推移,我还了解到,你可以完全信任他会支持你;在英特尔高度政治化的环境中,这真的很有帮助。这些都是很好的基础。

最重要的是,我逐渐了解到,他非常善于了解为他工作的员工,然后有效地指导和辅导他们。有时别人能以你自己未曾理解的方式理解你,而 Elliot 非常擅长这一点。你会觉得他真正关心如何帮助你个人成长,推动你走向那些有点不舒服但值得尝试的方向。我从未在另一位经理那里有过这种经历。

我原本计划在瑞典之行之后的那个秋天离开英特尔。在经历了 Larrabee 的戏剧性事件后,我对那个地方已经感到筋疲力尽,并且已经和 Elliot 在安排过渡事宜了。就在我们处理细节的过程中,Elliot 对 volta 陆续取得的每一个新成果都持续表现出浓厚的兴趣。我当时仍在埋头苦干。他亲眼目睹了无法有效利用 Larrabee 的向量单元是多么成问题,并鼓励我留下来,看看 volta 能发展到什么地步。他提出,如果我离开,我们将永远无法知道这种方法是否真的有效。

幸运的是,他最终说服我留了下来,继续在英特尔作为一名个人贡献者,只专注于 volta 的工作。

生存下去
如果我打算留下,那么我有信心确信 volta 不会被扼杀,这对我很重要。在英特尔的政治环境中,这种情况很有可能在某个阶段发生。

例如,如果我真的在编译器团队里,很可能在某个时候会有一些人介入并说:“太好了,现在我们明白了。非常感谢!这个我们接手了——我们从这里接手,并在商业编译器中实现这个功能。哦,你不需要再继续做 volta 了,因为那只会是浪费精力。” 而如果他们用这个想法说服了管理层(这很有可能),那么无论他们是否真的履行了承诺,volta 都将会终结。

在一个理性的世界里,那种事情不会发生:为什么他们不希望自己的编译器尽可能做到最好,无论想法来自哪里呢?可能原因在于,一些在该领域确立了自己专家地位的人,更关心的是保持自己精通该主题的形象,而不是其他。也可能是因为他们仍然不相信 volta 所针对的用例——HPC 社区显然对自动向量化非常满意,而这似乎才是最重要的。

无论如何,很有可能在未来,他们中的一些人会乐于看到这个眼中钉消失,所以我必须小心。

Elliot 是让我相信我的工作不会白费的关键。我知道他会保护这个项目,而且他同意一旦编译器准备就绪,我可以将其开源。这对我至关重要;一旦 volta 开源,就不可能通过英特尔的政治手段将其扼杀。在英特尔开源软件,只需要副总裁批准(并通过一些直接明了的流程),所以有了他的同意,我可以放心地开展工作。

关于混蛋及其在机构内的被接纳度
再多说几句来解释我为什么有这些担忧。

首先,绝对明确的是,英特尔有很多优秀的人,特别是在英特尔的编译器团队里。有很多优秀的工程师在做着出色的工作,他们是完全友善、想做正确事情的人。从数量上看,他们占了绝大多数。

问题在于,只需要少数几个混蛋,尤其是在拥有权力或影响力的位置上,就足以把你搞得一团糟。

英特尔这样的人格外多,因此,在英特尔的每个人都得在技术工作和政治斡旋之间取得某种平衡。你不得不这样。政治斡旋不仅仅是标准的"为自己争取"那种事;至少,它是周期性地防御来自他人的攻击,那些人想要你的地盘,会试图让你的项目下马,以便他们可以接手。

那里的一些人对待工作的方式是,技术上贡献寥寥,但在政治上投入很多。结果证明,这完全可以成为一种成功的职业策略——在必要时诋毁他人以维持和提升自己的地位,而自己却从未真正交付多少实质性的东西。这些人就是混蛋。

让他们更容易得逞的一个事实是,英特尔软件职业发展路径全是关于尽快远离编码——写代码是给新毕业生和国外成本较低的工程师干的。荣耀在于成为架构师,自己从不编码,但设定方向。在那个角色上,一个人可以仅仅依靠幻灯片(我是指 foils)就走得很远,而无需产出更多东西。

因为那里有这些混蛋,你总是必须提防他们。即使你不想在自己的职业生涯中采用那种模式,你也必须防御他们,否则你就会被淘汰。

我一直不明白为什么英特尔的上级管理层似乎对他们存在无动于衷。我猜想,一旦那种成功模式扎根,它就会像癌症一样侵蚀组织,并且难以根除。也许他们认为英特尔作为一家公司做得相当不错,所以为什么要去修理没坏的东西呢?也许他们认为这是一种良性的进取心,并且对大家相互争斗、像角斗士在竞技场中搏斗以赢得胜利、一切为了英特尔的荣耀这种想法感到满意。

有时,那些纵容混蛋的管理者会鼓励你在与他们互动时"假定对方意图是好的”。如果你这样做,你很快就会被算计;他们不玩那种游戏,并且知道如何利用你给他们的任何空子。

Elliot 从未告诉我要对他们"假定对方意图是好的”。

当他帮助我弄清楚如何与那些混蛋周旋时,我相当肯定他用了一句轻蔑的脏话来表达他对他们的看法。

下次真的会谈谈 SIMD 上的 SPMD 和早期设计影响;会比这篇结果写成的样子更愉快一些。再下一篇我们才会讲到首次向编译器团队展示成果。

下一篇:C 语言的影响及在 SIMD 上实现 SPMD

注释

  1. 和往常一样,总有例外。有一小部分资深人士仍然编程;非常尊敬他们。 ↩

ispc 的故事:C 语言的影响及在 SIMD 上实现 SPMD(第四部分)

注意: 在这些文章中,我不会在技术层面全面详细介绍 ispc/volta 的工作原理、其前身是什么,或者所有关键设计目标是什么。我会在叙述中提及其中一些相关内容,但要了解全面讨论,请参阅我与 Bill Mark 合写的关于 ispc 的论文。如果你花时间阅读本文,那篇论文也值得一读(恕我直言)。(Bill 稍后会在本故事中出现。)

防御阵线建立好后,我就出发了。要将 volta 变成普遍有用的东西,还有很长的路要走。许多基本的语言功能尚未实现——例如,我很确定像结构体这样的东西在那个时候甚至还不能用。

在 volta 的设计和实现过程中,我思考了很多关于 C 语言的事情。我一路第 N 次重读 K&R 以寻找灵感。

让我用 C 语言开始 psl 的并不仅仅是因为有可用的 C 语法:我非常喜欢 C 语言;它是一种非常简洁明快的语言。对我来说,很明显我会继续以 C 语言作为 volta 的基础,尽可能少地偏离它。不仅 Kernighan 和 Ritchie 显然把很多事情都做对了,而且这种语言广为人知,如果 volta 有朝一日完成,熟悉的语法将使其更容易被人们采用。

我从 C 语言中汲取的远不止语法——其设计原则更为重要。C 语言与当时的硬件有紧密的映射关系,一个好的程序员可以查看 C 代码,并相当准确地猜出编译器会为该代码生成哪些指令。没有神秘感,没有编译器魔术,没有看起来无害却可能爆炸成一堆指令的语句(我说的是你,C++)。我希望 volta 能保持这一点。

我想象 Kernighan 和 Ritchie 在当今世界设计 C 语言。如果 C 语言是为今天的 CPU 设计的,它会有什么不同?CPU 架构发生了两大变化:多核处理和 SIMD 向量单元。

对于多核,有很多好的想法可以借鉴。Andrew Lauritzen 了解所有这些想法,并且对此思考了很多,他是 Cilk 多线程方法的忠实粉丝——函数可以异步调用其他函数,这些函数可以在单独的线程中运行,编译器在原始函数返回之前等待它们全部完成。这很好地实现了并行组合。

所以我给 volta 添加了一个 launch 关键字;它使用了 Cilk 的语义。把它放在函数调用之前,该函数就会被送到线程池中:

1
launch foo(a, 6.3);

尽管这在语法上并不比调用 TBB 或类似的东西简洁多少(尤其是在现在有了 C++11 lambda 表达式之后),但把它作为语言的一等公民感觉很好。这基本上是零摩擦的多线程,似乎很适合这个时代。

对于 SIMD,一个明显的选择是使用显式向量数据类型来暴露 CPU 的该能力——这基本上就是很多人手动做的事情,用 vec4f 类等包装内部函数。将其作为语言的一等特性当然很有用,并且对于某些类型的计算,显式向量最终是表达它们的一种更清晰的方式。

正如现在应该已经清楚的那样,我真的很想编写具有复杂控制流但仍然在 SIMD 硬件上运行的程序;对于这种情况,显式向量不是很方便,所以选择了在 SIMD 上实现 SPMD。我认为这也非常符合 C 语言的哲学:直接和可预测,背后没有深奥的编译器魔术。

也许 K&R 会决定让这两种选项都可用,而且两者兼有通常很好;例如,那样的话,人们可以编写一个针对 16 宽 AVX-512 的程序,运行 4 个 SPMD 程序实例,每个实例的每条指令都可以执行一个 4 宽的向量操作。我们将在回顾中回到这个话题。

实现在 SIMD 上的 SPMD
正如我在瑞典所体验到的,向量化直线型代码很容易——如果你不先试图证明向量化是安全的,那就没什么难的。更棘手的部分是为 SPMD 程序实现通用的控制流。我们希望不同的 SPMD 程序实例在程序中走不同的路径,并且仍然计算出正确的结果。

使用内部函数的程序员知道这是如何完成的:当有条件地处理向量中的值时,你需要维护一个额外的掩码变量,记录其中哪些应该被修改。如果你在逻辑上有类似这样的东西:

1
2
if (a < b)
  c = 0;

对向量值的 a、b 和 c 进行操作,那么你存储一个记录 a < b 的向量结果的掩码,然后用它来有条件地将零赋值给 c 向量。如果有一个 else 语句,那么你对掩码取反,然后执行其代码,并注意掩码。Kayvon Fatahalian 有一套很棒的幻灯片讨论了这个以及它在 GPU 上是如何处理的;所有这些都非常相似,只是硬件提供了多一点帮助。

更一般地说,循环、break 和 continue 语句,甚至通过指针的间接函数调用——所有这些都可以通过使用相同的思路以向量形式执行:

  1. 维护一个执行掩码,记录哪些程序实例(SIMD 通道)是活动的。
  2. 根据通过程序的保守控制流路径执行向量指令。换句话说,如果任何通道需要,就执行一条指令。
  3. 确保非活动程序实例不会产生可见的副作用——意外的内存写入等。

这些中的每一项要正确实现都可能有点繁琐,但它们在概念上是直接的原则。

维护循环执行掩码的规则只比 if 语句的规则稍微复杂一点。循环测试的值给出了循环体的执行掩码,你运行循环直到所有活动程序实例的该掩码都为假。循环中的 break 只是禁用执行 break 语句时掩码为活动的任何元素的活动掩码;它们的活动掩码在循环结束后恢复。continue 会禁用某个实例的掩码直到当前迭代结束,此时掩码恢复。等等。

在 volta 中正确实现这个掩码维护功能花了一些时间。一旦这一切都真正稳固下来,真是令人激动,尤其是 LLVM 持续可靠,只要我给它好的向量化 IR,它就能给我好的 x86 汇编。

这里有一个小的 volta/ispc 程序示例,它使用一种低效的算法计算一个浮点数的整数次幂。(注意这个程序也是有效的 C 代码。)

1
2
3
4
5
6
float powi(float a, int b) {
    float r = 1;
    while (b--)
        r *= a;
    return r;
}

这是如今编译器输出的汇编代码。(注意:AT&T 语法,目标操作数是最后一个参数。)这里我使用了 AVX2,因为它比 SSE4 更清晰,尽管 SSE4 是 volta 最初唯一支持的指令集。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
LBB0_3:
	vpaddd    %ymm5, %ymm1, %ymm8
	vblendvps %ymm7, %ymm8, %ymm1, %ymm1
	vmulps    %ymm0, %ymm3, %ymm7
	vblendvps %ymm6, %ymm7, %ymm3, %ymm3
	vpcmpeqd  %ymm4, %ymm1, %ymm8
	vmovaps   %ymm6, %ymm7
	vpandn    %ymm6, %ymm8, %ymm6
	vpand     %ymm2, %ymm6, %ymm8
	vmovmskps %ymm8, %eax
	testl     %eax, %eax
	jne       LBB0_3

前两条指令递减 b,使用活动的向量掩码仅对活动的通道执行赋值。接下来的两条将 r 乘以 a,同样使用掩码。然后将 b 与零进行相等比较,结果用于更新执行掩码。(由于 powi() 可能在调用时并非所有通道都启用,所以更新掩码需要一条额外的指令,因此我们必须在 powi() 入口处对掩码进行 AND 操作。在这种情况下,如果我们跳过这一步并为禁用的通道计算错误的结果也没问题,但通常我们需要一个准确的掩码,以防有像内存写入这样的操作需要为非活动通道抑制。)最后,用一个快速的 movmsk 检查是否还有任何通道是活动的,然后再跳转到循环开始。

就是这样。我认为除了在执行掩码维护上不必要的精确之外,这是最优的。我很乐意偶尔接受一些零星的额外指令,而不是必须手动编写内部函数,特别是对于非平凡的程序。

编译器优化与转换
看过这个例子后,就更容易理解 T. Foley 另一个超级有洞察力的观点:为 SIMD 硬件编译 SPMD 程序是一种编译器转换,这与编译器优化完全是两回事。

这个见解回到了自动向量化的问题:它是一个复杂的优化,充满了启发式方法,你无法确定它最终会走向何方。编译器试图推理循环向量化的安全性——是否存在任何循环携带的依赖?用计算机程序对任意程序进行推理只能做到这一步(记得那个麻烦的停机问题吧),所以自动向量器注定是脆弱的,并且对用户来说是不可预测的。

在 SIMD 上实现 SPMD?那是一种转换。我们刚刚看到了如何做到这一点。它是机械的。如果你已经将其自动化,没有理由它不会一直有效。

这样做的好处是它符合 C 语言的哲学:任何理解这个概念的程序员都可以准确预测编译器将生成什么代码,而且这几乎就是他们自己会手写的代码;对于有性能意识的程序员来说,第一个属性与第二个属性同样重要。

最终,volta 是一种有点"笨"的编译器;它的实现中并没有什么真正深奥巧妙的东西。除了大量的工程工作之外,关键首先在于以正确的方式处理问题。

下次,我们终于要向编译器团队分享初步成果了。

下一篇:初步基准测试结果

注释

  1. 当然,CPU 微架构发生了很多变化——乱序执行、分支预测、缓存,所有那些好东西。但这些都没有真正影响编程模型应该是什么样子。 ↩
  2. 我已经好几年没有用锐利的目光阅读 x86 汇编了,所以我期待收到邮件告诉我我漏掉了什么,并且那个说法是错的。:-) ↩
  3. Tim 也应该因发明"SPMD on SIMD"这个短语而受到赞誉。 ↩
  4. 至少,那些从被"足够智能的编译器"坑过一两次中吸取了教训的程序员是这样。 ↩

ispc 的故事:初步基准测试结果(第五部分)

和之前一样,这是凭记忆所写。我尽力确保细节准确,但如果有任何错误,请联系我。

编译器团队当时使用一小套基准测试来评估并行编程模型——比如布莱克-斯科尔斯期权定价、曼德博集求值、小型模板计算等。它们大部分都只有几十行代码。

最复杂的是 aobench,大约有 300 行代码。如果我没记错的话,图形部门的人把 aobench 推给他们,作为至少能模糊代表图形工作负载典型不规则性的事物。任何更复杂或更贴近实际的东西都根本不被考虑:对于当时手头的许多并行编程模型来说,处理起来都太困难了。

aobench,通过现代 ispc 渲染。在配备 AVX2 的双核笔记本电脑上,比串行代码快 15.6 倍。

英特尔拥有各种各样的并行编程模型;有些只针对多核,有些只针对 SIMD。英特尔 C 编译器中有用于多核的 Cilk,有自动向量化和 #pragma simd 的东西,有 OpenCL 编译器,有来自 RapidMind 的元编程技术(后来与英特尔的 Ct 合并,最终成为英特尔 Array Building Blocks),还有 Thread Building Blocks 以及英特尔 Concurrent Collections。可能还有其他我忘记的东西。

总的来说,只针对多核的模型在线程数上表现出线性扩展,而针对 SIMD 的模型在 SIMD 宽度上对没有控制流的计算表现出线性扩展,但对于有控制流的计算(如计算曼德博集和 aobench)则完全不起作用。不同的向量通道想要走不同的执行路径,这对它们来说太难处理了。

总之,一旦 volta 变得相当完善,并且我对它生成的代码质量感到满意后,我就用 volta 编写了其中一些基准测试并测量了性能。我相当惊讶:对于其中许多测试,volta 击败了 #pragma simd(最接近的竞争者),而在其余的测试中也相当接近。除了那个模板计算,我想是这样。

而且不仅仅是它在像 aobench 这样能真正处理控制流的测试中获胜,即使对于一些简单的基准测试,它也更快。虽然只快了几个百分点,但它赢了。我反复运行测试,只是为了确认自己没有搞错什么。

英特尔有数百名员工致力于编译器工作,并自豪于能生成比任何其他编译器都更好的 x86 代码。可以说,volta 能取得这样的成果(尽管是针对一组简单的基准测试),是相当令人震惊的。

我不记得我是如何首次向编译器团队传达这些结果的了,但这对他们来说同样令人惊讶——volta 结合了来自图形领域人士的奇怪编程模型,以及他们只是略有耳闻的 LLVM,这两者结合在一起效果如此之好,几乎是不可想象的。

不久后就安排了一个会议,与编译器团队的十来人讨论所有这些。

除了集体的惊讶之外,反应是分化的。大多数人对这个结果很感兴趣——LLVM 在当时还不是像今天这样众所周知的强大工具,一个程序员利用它就能击败 icc 编译器……这当中肯定有值得学习的趣事。我们对结果进行了很好、很健康的讨论,并深入研究了生成代码的一些差异。我可能又解释了一遍什么是"在 SIMD 上实现 SPMD"。

对结果的另一种解读
他们中有一两个人得出了另一个结论:只有一种方式可以解释——我肯定作弊了。我猜他们想象我一定是特化了编译器,设置了特殊 case 来检测这些基准测试程序,然后当识别出这些程序时,就直接吐出完美且预先准备好的代码,根本没有任何编译过程。这当然是解释击败 icc 的最可能的方式。

根据典型的博弈论来推测那些混蛋的想法,他们的思路是这样的:我也是个混蛋,我在这里的真正目标并不是真正解决问题,而是想利用 SIMD 要么篡夺他们在编译器组里并行编程模型方面的角色,要么推进某些其他邪恶的计划。

在那种情况下,我自然会严守秘密,对编译器源代码保密,并且只勉强让他们试用二进制文件。也许我甚至会试图推迟几个月再提供,声称我想先做更多改进。如果我作弊了,我会尽量拖延他们发现真相的时间,希望我的邪恶计划能先成功。

或者,如果我确实有个好主意,我应该做的是对细节保密,防止他们拿走并声称是他们自己的,以维持他们的地位。

而我呢,我仍然只是想说服专业人士来编写这个编译器,这样我就不用自己做了。我做的正是我们之前多次告诉过他们的事情。所以我在会议后通过电子邮件发了一个源代码的 tar 包。

请原谅我,但我很确定我在邮件里附了道歉:这是我第一次写编译器,所以如果部分实现得不是很好,请见谅。这是从那些混蛋身上学到的教训:有时候稍微补一刀也挺有意思的。

结果证明我并没有作弊,不过他们指出 volta 的超越函数精度不如其他编译器。我修改了 volta 以使用英特尔的短向量数学库内部函数(SVML),其他编译器用的也是这个。在期权定价测试上,性能差距缩小了,但 volta 仍然领先。在其他不使用超越函数的测试上,结果没有变化。

对源代码保密——真的吗?
对源代码保密的想法可能看起来很奇怪。毕竟,我们都在同一家公司工作,对吧?

事实证明,有些团队会小心翼翼地守护他们的源代码,只向英特尔内部的其他团队提供二进制版本,并且只在明确规定的交付点提供。这是防御混蛋的一种手段。

情况是这样的:如果你正在做的东西是别人想要攻击的,有时他们会拿走你进行中的系统版本,把它拆解剖析,找出一堆它目前还运行不好的例子,然后拼凑出一个论据,说你的东西状况糟糕、无法工作,因此应该被取消。

有时这种策略真的奏效;管理层对这种危言耸听的接受程度令人震惊。也许是因为他们离技术太远,无法根据其优劣来评估这些论点,或者也许又是他们欣赏这种角斗士般的争斗作为决策过程。

最好的情况是你的团队必须花费大量时间说服管理层,让他们相信你们实际上在正轨上,一切正常。更简单的办法就是一开始就不分享你的代码。

真是"美好"的时光啊。

下次我们将讨论并行编程模型的比拼,以及 volta 最初内部用户的使用情况。

下一篇:首批用户与现代 CPU 的到来

ispc 的故事:首批用户与现代 CPU 的到来(第六部分)

volta 早期令人印象深刻的结果带来的其中一件事,是受邀参加编译器团队内部正在进行的一系列并行编程模型比拼。

过程是这样的:每隔几个月,会组建一个小组,成员来自英特尔各个并行编程项目(TBB、#pragma simd、Cilk、OpenCL 等等),每个项目派一两名代表。这个过程会持续几个月,首先是大家共同商定评估方法,然后协商纳入哪些工作负载。(我向您保证,绝对没有参与者会推动那些特别适合自己模型的工作负载,或者试图边缘化那些不适合的工作负载。)接着是测量性能,调整优化器,最后,我们会准备一份演示文稿,提交给编译器部门的副总裁。

能参与其中,至少在传统的英特尔职业生涯看来,是一项荣誉。我的工作重要到足以向副总裁级别汇报,这种事情是可以写进个人的"自夸表"里的——就是每年为绩效评估准备的自评报告。有些人全年都在打磨这份文件,从上一次评估周期结束就立刻开始。报告长度没有限制,长达二十页的也并不罕见。

最终,演示不仅会展示性能结果,还会强调每种编程模型的优势。人人都有奖杯,而这些比拼似乎从未影响过英特尔的战略。

我想我被邀请参加这个"派对"是高兴的,但我并没有在这些活动上投入太多精力。我对小心翼翼地守护 volta 在这些基准测试上的性能领先优势不感兴趣,尤其是在其他模型的实现者努力缩小差距的时候;更有趣的是开始将它用于更复杂的、其他模型无法处理的程序,并开始与英特尔内部的早期采用者合作。

早期用户体验
英特尔的许多图形部门人员对 volta 感到兴奋并予以支持。就像我说过的,这或多或少是他们许多人想要的那种工具。

他们中有一些人已经使用内部函数实现了有趣的图形程序。这形成了一个绝佳的组合:我们既有编写良好的内部函数实现可以作为 volta 结果的对比基准,又有聪明的程序员,他们知道自己希望从编译器得到什么,并且不惧怕阅读和批评汇编代码。到 2010 年 12 月左右,编译器已经足够健壮,开始有其他人使用它。

最早的用户之一是 Doug McNabb,他曾经用内部函数编写了一个粒子光栅化器。他将其移植到 volta 然后……性能糟透了,汇编代码一团乱。这个结果起初让我有点害怕——面对一个新的工作负载完全失败了。也许这整个事情终究不会像我希望的那样成功。

结果发现,他在他的 volta 代码中恰好全程使用了无符号整数,但这并非出于任何特定需求。而事实上,SSE4 指令集中并没有在浮点数向量和无符号整数向量之间进行转换的指令,因此,每次需要转换时(这种情况很频繁),都会变成一大段标量代码,逐个转换每个向量元素。

我在编译器中添加了一个关于在 SSE4 下使用无符号整数的警告,而 Doug 迅速修复了 volta 代码,改用常规的整型。成功了!干净的汇编代码,性能与他手写的内部函数代码相差无几。呼,松了一口气。

另一位早期用户是 Andrew Lauritzen,他有一个集群延迟着色工作负载,用来评估将该计算映射到不同并行硬件(从 Larrabee 到 GPU)的各种方法。他有一个内部函数实现,并且很乐意编写一个 volta 实现,这个实现现在成了 ispc 的示例之一。

ispc 中的延迟着色:在单核上使用 SSE4 比标量代码快 4.15 倍,并且在多核上呈线性缩放。

Andrew 的延迟着色示例是当时用 volta 编写的较长的程序之一,所以我又一次紧张起来。令人欣慰的是,它运行良好,几乎可以说是开箱即用。我手头没有当时的性能数据,但如今在单核上,使用 SSE4(我们当时的测试目标)运行比串行代码快 4.15 倍,并且随着核心数量的增加几乎呈线性扩展。

对于 Doug、Andrew 和其他早期采用者来说,从串行 C 实现转到 volta 确实相当容易,这起到了很大帮助。首先你戴上 SPMD 的"思考帽"。然后你决定将 SPMD 程序实例映射到什么——像素、三角形,或者任何适合循环遍历的对象,然后基本上就这些了。你的大部分 C 代码可以保持不变或只需最少量的修改。

当然,这正是基于 C 语言在 SIMD 上实现 SPMD 的核心理念,但让我惊讶的是,它在实践中如此清晰。例如,比较 ispc 示例中一个小型光线追踪器的串行 C++ 实现和 ispc 实现;大部分代码几乎完全相同。

真的,用你喜欢的图形化差异比较工具看一下;没有多少行代码是不同的,但 ispc 展现出的性能在 CPU 核心数量和 SIMD 宽度上都能线性扩展。

早期采用者们深入使用过程中发现了不少 bug,我真的很感谢他们总体上没有因此感到困扰,并且乐于投入时间和见解来让这个东西变得更好。他们的反馈和热情真的很有帮助;能够逐渐确信这个东西或许也能解决他们关心的问题,这太棒了。

赞颂现代 CPU
关于 Andrew 的延迟着色工作负载实现的 4.15 倍加速:这个改进实际上略高于 SSE4 可能提供的理想 4 倍加速。有时 volta 确实会发生这种情况;这有点诡异,让人怀疑"我是不是测错了?"。延迟着色工作负载涉及一些 gather 操作和一些分叉的控制流;它本身也不是完全规整的。

这种结果尤其令人惊讶,因为在 AVX-512 之前,英特尔的向量指令集架构(ISA)完全不是为 SPMD 执行设计的。在 AVX 之前,它们尤其非正交且古怪(参见 SSE4 中无符号整数向量和浮点数向量之间的转换)。给人的感觉是,它们的设计初衷并非作为编译器目标,而是架构师们发现手写一些重要内核代码所必需的操作的大杂烩。

当我最初在 volta 中实现 SPMD 控制流时,我并不确定它在实践中效果到底会多好。我或许能正确地在 CPU SIMD 硬件上运行 SPMD 程序,但如果性能很差,那也没什么意思。分叉控制流是一个已知的风险:就像在 GPU 上一样,分叉执行会带来性能损失:如果一些程序实例走 if 语句的一个分支,而另一些走另一个分支,那么你将不可避免地执行两边代码,每一部分都只有部分通道是活跃的。

一个更令人担忧的问题是,有很多操作不被支持为向量指令,必须通过分解成相当于在 SIMD 通道上循环的代码,用标量代码处理每个通道。在 SSE4 时代,这类情况非常多,经过 AVX、AVX2 到 AVX-512,逐渐减少。

举个例子,这里有一个简短的 volta/ispc 函数,执行一次 scatter 操作:index 的值在每个 SPMD 程序实例中是唯一的;因此,每个实例通常写入完全不同的(并且可能是不连续的)内存位置:

1
2
3
void scatter(uniform float ptr[], int index, float val) {
    ptr[index] = val;
}

对于现代 AVX-512,情况很理想,有一条原生的 scatter 指令 vscatterdps 可以完全处理这个问题。而在 AVX-512 之前的所有指令集上,都必须生成基本上是对向量通道进行循环的代码,检查每个通道的执行掩码是否启用,然后仅在启用时才将该通道的值写入内存。

最终对于 SSE4 总共需要 23 条指令;对于 AVX 则需要更多指令,因为向量通道数翻倍了。这是为 SSE4 生成代码的开头部分,处理第一个向量通道:

1
2
3
4
5
6
7
	movmskps %xmm2, %eax
	testb   $1, %al
	je      LBB0_2
	movd    %xmm0, %ecx
	movslq  %ecx, %rcx
	movss   %xmm1, (%rdi,%rcx)
LBB0_2:

testbje 在当前通道不活跃时跳转到下一个通道,而那三条 mov 指令则在通道活跃时整理数据并写入内存。然后,或多或少相同的事情,再重复三次。除了所有这些指令,还有一系列不一定非常可预测的分支短指令序列;这对性能也不是什么好消息。

那么,那个延迟着色工作负载以及其他偶尔有 gather 或 scatter 之类操作但仍然高效运行的程序,是怎么回事呢?我能想到的最好答案是:乱序执行掩盖了大量的瑕疵

英特尔 CPU 非常擅长运行糟糕的代码。这么写有点滑稽,但我的意思是这是一种高度的赞美;我认为这是他们真正的竞争优势之一。构建一个能高效运行完美规整代码的处理器,比构建一个能体面运行任何扔给它的垃圾代码(虚函数调用、缓存不友好代码、分支众多的代码等)的处理器要容易。我认为英特尔的伟大才能之一就是能够很好地运行所有这些玩意儿——比竞争对手好得多。

另一个更具挑战性的工作负载例子:ispc 发行版中包含一个小型体渲染器。(同样,ispc 实现看起来非常像 C++ 实现。)它生成这样的图像:

当我第一次编写它时,我不知道它在 volta 中运行是否会比在标量代码中更快;它本身并不非常 SIMD 友好。计算的核心部分让光线穿过一个规则网格的体密度值,并对 8 个邻居进行三线性插值以计算密度,并在每个点计算光照。因此,沿着每条光线的每个点都需要 8 次 gather 操作,因为每个向量通道可能读取不同的内存位置来获取其密度值。

它持续向前步进穿过体积,直到不透明度足够高,以至于光线更远点的光照不会产生影响。因此,还存在不规则的控制流:在一组跨越 SIMD 通道的光线中,必须持续进行,直到所有光线都决定终止。

在运行 SSE4 版本(我最初测试的目标)的 2 核笔记本电脑上,ispc 实现比串行代码快 5.2 倍。请注意,这仅是最佳情况下可能期望加速比的 65%——多线程带来 2 倍加速,4 宽 SSE 带来 4 倍加速,总共 8 倍。在相同的 2 核系统上使用 AVX2,ispc 版本比串行代码快 7.7 倍。整体更好,但仅为理想情况的 48%。推测 AVX2 中的原生 gather 指令起到了一些作用,尽管分叉控制流的效率损失更高了,现在是 8 宽运行。

无论如何,我认为这个性能出奇地好:我原本半预期这个工作负载在映射到 SIMD 硬件上时根本看不到任何好处;对于如此不规则的东西能有这种加速,我已经非常满意了。

下次,我们将介绍构建 AVX 后端的经验以及与 LLVM 团队的互动。

下一篇:构建 AVX 后端及回馈 LLVM

注释

  1. 一些谷歌员工对谷歌的绩效流程开销感到非常焦虑;不用说,我见过你们难以想象的事情。 ↩
  2. 那个 uniform 限定符表示该值在所有程序实例中是相同的;这里意味着基指针是相同的。 ↩

ispc 的故事:构建 AVX 后端及回馈 LLVM(第七部分)

时间到了 2011 年底,我对 AVX 即将在 Sandy Bridge CPU 上亮相感到非常兴奋:在经历了多年 SSE 的 4 宽向量(针对 32 位数据类型)之后,AVX 将其翻倍,使得执行 8 宽 32 位向量操作成为可能。这大概是自 1999 年 SSE 问世以来,英特尔 SIMD 指令集架构中最令人兴奋的事情了。AVX 的到来对 volta 来说尤其令人兴奋——在最佳情况下,得益于向量通道数翻倍,许多东西的运行速度会大致快两倍;而在最坏情况下,这整个"在 SIMD 上实现 SPMD"的想法可能最终被证明并不那么吸引人。

速度快两倍是巨大的提升:上世纪 90 年代是你最后一次在单代 CPU 中看到接近"快两倍"性能提升的时候。如今,单核 CPU 性能每代可能提升 10-20%,这得益于更好的半导体工艺、略快的时钟频率以及微架构改进,但也就这样了。

有趣的是,英特尔即将发布 AVX,但在某些情况下,AVX 要让程序运行得更快会有延迟,有时甚至长达数年。对于自动向量化器能处理的代码,只需重新编译即可。但对于所有用 SSE 内部函数编写的代码,嗯,必须有人去用 AVX 内部函数重写它,它才会变快。对于世界上所有不使用 SIMD 的标量代码,AVX 不会带来任何好处。如果几年后你又得全部重写一遍,那么当初费尽心思写所有这些内部函数的动机是什么呢?

用内部函数编码有很多问题——不仅仅是那些永恒难题,比如什么前面加单下划线,什么加双下划线,更在于它完全将你绑定在特定的指令集架构及其能力上。这种情况与 GPU 完全不同,GPU 厂商能够每代进行重大的架构更改,通过提供更多核心和更多向量通道来提升速度,而程序员无需修改他们的代码。

在很大程度上,英特尔内部的人似乎并不太在意事情是这样;我从未真正理解这一点。嗯,有些人在意,但我不明白为什么领导层没有为此积极抓狂——你即将推出一款计算能力比一年前产品翻倍的 CPU,但几乎没人能享受到这个好处?

我唯一的猜测是,这是多年来 C(和 Fortran)能完美映射到英特尔 CPU 架构所遗留的观念;在多核和 SIMD 变得重要之前,他们无需担心编程模型本身,所以我猜他们已经习惯了这不关他们的事。

尽管英特尔的编译器团队内部关于并行编程模型的讨论很多,但它并没有那种"公司的未来取决于此"的感觉,例如,不像英伟达对待 CUDA 那样。谁知道呢,也许公司的未来确实不取决于此;我猜英特尔现在还在经营。不过,在增加 SIMD 宽度的同时,却没有一个关于开发者如何真正有效利用它的计划,这仍然显得很奇怪。

无论如何,如果他们给我这些向量通道,我会欣然接受。一旦 LLVM 中开始出现对 AVX 的早期支持,我就开始为 volta 添加 AVX 支持。

为 volta 添加新后端
为 volta 添加新后端基本上包括启用相应的 LLVM 代码生成器,然后手动编写一堆 LLVM IR 来弥合编译器希望执行的基本操作与给定指令集架构的具体细节之间的差距。例如,volta 标准库提供了一个对各种类型进行操作的 min() 函数。

以下是针对 float 的实现(用 volta 编写):

1
2
3
static inline float min(float a, float b) {
    return __min_varying_float(a, b);
}

相应地,每个后端都需要提供 __min_varying_float() 的实现,该实现需手动用 LLVM IR 编写。对于 AVX,有一条对应的指令,LLVM 通过一个内部函数暴露它,我们可以直接调用它。

以下是 AVX 的定义:

1
2
3
4
define <8 x float> @__min_varying_float(<8 x float>, <8 x float>) {
  %call = call <8 x float> @llvm.x86.avx.min.ps.256(<8 x float> %0, <8 x float> %1)
  ret <8 x float> %call
}

LLVM 会将 volta 中对 min() 的调用转换为一条 vminps 指令。

如果 AVX 没有单条指令能完成此操作,那么 AVX 目标的 IR 就需要通过其他操作以最合理的方式来完成计算。(像 SSE4 的 scatter 和 gather 这类操作就是以这种方式实现的。)

大力测试 LLVM 中的 AVX 支持
如前所述,没有 LLVM,volta 绝无可能;如果开箱即用的 SSE4 代码生成质量没有那么好,我很可能早就结束了早期的实验,转而进行新项目了。我欠 LLVM 一个大人情,所以想做点有益的事情作为回报。

在 LLVM 的 AVX 后端甚至还没完成之前,我就开始尝试使用它了;我猜开发人员当时可能还没准备好让任何人去测试它。不过,我真的很想看看 AVX 对 volta 的效果如何,而且我也觉得我可以在测试他们的实现方面帮点忙。

结果证明,volta 在测试 LLVM 的向量代码生成方面相当有效。它不仅生成大量向量化的 LLVM IR,还直接发出大量 x86 向量内部函数(如 __min_varying_float());这两者的特征都与大多数其他基于 LLVM 的编译器通常生成的 IR 有很大不同。这使得在 LLVM 的那个早期 AVX 后端中很容易找到很多 bug。

为了让您对典型输出有个概念,这里半随机地选取了为延迟着色示例生成的一些代码,这里使用的是 AVX 和现代的 ispc。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
	vmovups   1856(%rsp), %ymm3
	vdivps    %ymm2, %ymm3, %ymm13
	vmulps    %ymm13, %ymm1, %ymm1
	vdivps    1792(%rsp), %ymm1, %ymm4
	vmulps    608(%rsp), %ymm13, %ymm1
	vdivps    1376(%rsp), %ymm1, %ymm11
	vmulps    %ymm4, %ymm4, %ymm1
	vmulps    %ymm11, %ymm11, %ymm2
	vaddps    %ymm2, %ymm1, %ymm1
	vmulps    %ymm13, %ymm13, %ymm2
	vaddps    %ymm1, %ymm2, %ymm1
	vrsqrtps  %ymm1, %ymm2

这是该示例的所有汇编代码:deferred.S。

当我开始尝试使用初期的 AVX 后端时,我遇到的第一个 LLVM bug 通常是崩溃和断言失败;LLVM 中各种对于新目标尚未经过测试的部分,或者那些对其输入有假设但新目标不再成立的部分。我会使用 LLVM 的 bugpoint(一个很棒的工具,它能进行自动二分搜索以找到最小的测试用例)来精简出一个小的测试用例,然后发送出去。

当代码能编译之后,下一步就是正确性。我在开发过程中使用了一个包含几百个 volta 程序的测试套件;每个程序都是一个简短的函数,执行一个小计算,然后验证结果是否与期望值匹配。这些测试不仅在我开发 volta 时对于验证其自身的正确性很有用,而且也能很好地发现 LLVM 向量代码生成的正确性 bug。每当这些测试在新目标上失败时,我就会深入排查;有时是我自己的 bug,例如在我为后端编写的 IR 中,有时是 LLVM 的代码生成 bug。一旦所有这些测试都通过,我就可以自信地开始编译更大的程序了。

随着 LLVM 对于给定后端的向量代码正确性变得稳定,我花了很多时间查看生成的汇编代码(volta 的用户也是如此);这导致了我观察到许多 LLVM 向量代码质量可以改进的情况。

在 volta/ispc 的开发过程中,我似乎总共提交了 144 个 LLVM bug。LLVM 开发人员通常修复得非常迅速。这让整个过程充满乐趣——感觉我们在一起取得良好进展,随着他们修复了早期的 bug,我可以继续寻找 progressively 更冷门的 bug。最终,AVX 及更高版本的 LLVM 后端变得非常稳定;我愿意认为 volta 发现的问题对这个过程有所帮助。

在 LLVM 方面,非常感谢 Nadav Rotem,他在 LLVM 的向量选择方面做了很多关键工作;Bruno Cardoso Lopes,他在 AVX 代码生成方面做了大量工作并修复了大部分这些 bug;以及 Craig Topper,他为 AVX2 做了很多贡献。当然,还要万分感谢 Chris Lattner 最初启动了整个 LLVM 项目,以及 LLVM 团队的其他成员。

调查结果显示…
所有的汗水都是值得的。当 AVX 开始正常工作,我可以开始测量性能时,真的非常令人兴奋。通常,从 AVX 中获得 1.5 倍到 2 倍的性能提升是很典型的。而且只需要重新编译;现有的 volta 代码无需修改就能看到这些性能提升。再次松了一口气,没有出现意外的波折导致事情不如预期。

以下是用今天的 ispc 测量的一些结果,显示了在单核上相对于标量代码的加速比。

工作负载SSE4 加速比AVX1 加速比AVX1:SSE4 比率
Black-Scholes4.13x6.12x1.48x
光线追踪器2.60x5.42x2.08x
延迟着色4.15x5.00x1.20x
Aobench3.33x4.86x1.46x

几个工作负载的单核加速比,显示了 AVX 带来的性能优势(使用今天的 ispc 测量)。

我本来敢发誓 Black-Scholes 在 AVX 落地时基本上快了两倍。这点将来需要深入研究一下,但上面是现在的数字。

AVX2 也是一个巨大的进步,因为它也提供了 8 宽 32 位整数操作:

工作负载SSE4 加速比AVX2 加速比AVX2:SSE4 比率
Black-Scholes4.13x6.97x1.68x
光线追踪器2.60x6.56x2.52x
延迟着色4.15x6.38x1.54x
Aobench3.33x6.78x2.03x

不用说,看到这些加速比真实发生,真是太神奇了。将 SIMD 向量宽度翻倍,在晶体管和功耗方面是相对廉价的。我不知道实际数字,但就这些指标而言,将向量宽度翻倍比将 CPU 上的核心数量翻倍要便宜得多。而且事实证明,如果你有一个合理的编程模型、编译器以及合适的工作负载,你就能看到在亚线性硅成本下性能接近翻倍。胜利!

下次,将详细介绍一些关于如何让程序运行得更快的具体细节。

下一篇:关于优化和性能的更多内容

ispc 的故事:关于优化和性能的更多内容(第八部分)

之前将 volta 描述为一个"笨"编译器有点不太公平;我们今天将重新探讨这个话题。

不仅许多语言特性经过精心设计以很好地映射到 CPU 硬件,而且一些针对 LLVM IR 的自定义优化通道对于保持性能与内部函数代码竞争也至关重要。其中许多优化都受到了英特尔早期用户的影响,以及他们对 volta 汇编输出的仔细审查。

统一 (Uniform)
uniform 限定符是对性能最重要的语言特性之一。

uniform 是一个类型限定符,它描述一个在所有正在执行的 SPMD 程序实例中相同的值。它对应于一个标量值,并能很好地映射到 CPU(我听说 CPU 既支持标量计算也支持 SIMD)。这个概念对于程序员来说很容易掌握,并且直接映射到 CPU 内部函数程序员组织其代码的方式。

将变量声明为 uniform 可以带来两个好处:

  1. 任何基于 uniform 值的控制流就像常规程序中的控制流一样:所有 SPMD 程序实例遵循相同的路径,我们不需要担心更新执行掩码。
  2. uniform 值的内存访问易于处理且高效:例如,一个 uniform 读取对应于一个简单的标量加载。

我第一次接触 uniform 的总体思想是在 RenderMan 着色语言(RSL)中,它实际上是一种在要着色的点网格上操作的 SPMD 语言。它也有一个 uniform 关键字,表示对所有点都相同的值。据我所知,RSL 实现从未以 SIMD CPU 硬件为目标,但标量 CPU 实现维护了一个记录哪些点处于活动状态的掩码,并且可以应用 uniform 来在控制流方面获得类似的好处。兜了一圈,当皮克斯几年前发布了一份关于使用 ispc 编写着色器的好处的说明时,我觉得很有趣。

事实证明,RSL 最初是为 1980 年代的定制 SIMD 硬件设计的,而且那个时代针对多处理器的其他 SPMD 语言中也有 uniform 的前身;再次请参阅 ispc 论文以了解该领域先前工作的更多信息。

最小化掩码指令
处理掩码向量计算的所有细节可能会导致 x86 汇编代码相当臃肿。结果证明,设计 volta 的一些语言特性是值得的,以便能够促成编译器可以确定所有程序实例都处于活动状态的情况,并在代码生成中利用这一点。

其中一个例子是 volta 提供的一个专门的循环结构 foreach。它描述了一个遍历一个或多个维度的循环,其中 SPMD 程序实例被映射到给定的值范围。

我们将在下面使用这个简短的 volta 函数作为示例;想必它的功能是显而易见的:

1
2
3
4
5
void increment(uniform float ptr[], uniform int count) {
    foreach (i = 0 ... count) {
        ptr[i] += 1;
    }
}

现在考虑一个在 130 个值上进行 foreach 循环,目标是 8 宽 SIMD:将会有 16 次执行掩码全开的循环迭代,处理前 128 个值。然后,在最后会有一次迭代,其掩码是混合的,用于处理剩余的两个元素。ispc/volta 为循环体生成两个版本的代码,第一个专门针对全开掩码进行了优化。

现代的 ispc 为无掩码迭代生成以下 AVX2 汇编代码,外加几条额外的指令来检查是否需要进行下一次循环迭代,然后跳转到适当的位置:

1
2
	vaddps	(%rdi,%rdx), %ymm0, %ymm1
	vmovups	%ymm1, (%rdi,%rdx)

这正是你想要的,除非你是那种会为可能不必要的未对齐向量存储而烦恼的人。假设 count 很大,绝大多数迭代将只运行那段代码。

在一般情况下,还需要做更多的工作。以下是最后一次迭代的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
	vmovd        %eax, %xmm0
	vpbroadcastd %xmm0, %ymm0
	vpaddd       LCPI0_1(%rip), %ymm0, %ymm0
	vmovd        %esi, %xmm1
	vpbroadcastd %xmm1, %ymm1
	vpcmpgtd     %ymm0, %ymm1, %ymm0
	shll         $2, %eax
	cltq
	vmaskmovps   (%rdi,%rax), %ymm0, %ymm1
	vbroadcastss LCPI0_0(%rip), %ymm2
	vaddps       %ymm2, %ymm1, %ymm1
	vmaskmovps   %ymm1, %ymm0, (%rdi,%rax)

前几条指令确定执行掩码,禁用对应于数组中超过 count 的项的向量通道。然后,在从数组加载最后的值时,必须使用该掩码和一条掩码加载指令 vmaskmovps,以免意外读取数组末尾之后的内存。然后是加法运算。最后,在将结果写回时使用掩码存储,以免破坏数组之后的内存。

如果没有 foreach 以及它所启用的"全开"优化,我们每次循环都需要经历很多这样的操作。(而内部函数程序员会理所当然地翻个白眼然后走开。)

如果你知道要处理的项数总是 SIMD 宽度的倍数,你可以在循环前添加类似这样的代码:

1
count &= ~7;

这在实践中是无操作(no-op),但这足以让编译器推断出如果不需要,它就不必发出循环体的第二个版本。(这里的"编译器",我指的是 LLVM;ispc 会继续为两种情况发出 IR,但在 LLVM 的常规优化通道完成工作后,LLVM 的死代码消除通道会处理掉不需要的部分。)

除了 foreach 之外,还有其他地方我们可以做类似的事情。当应用程序第一次从 C/C++ 调用 volta 代码时,根据定义,所有程序实例都在运行,因此可以静态地确定执行掩码是全开的,直到 SPMD 控制流介入。在那之前,也可以应用相同类型的优化。

此外,当 SPMD 控制流开始发生时,并非全无希望:在运行时检查所有程序实例是否处于活动状态可能是值得的。volta/ispc 提供了独立的控制流关键字,允许程序员指示预期是相干的控制流:cifcfor 等等。

例如,当使用 cif 时,volta/ispc 生成的代码会在为 ‘if’ 条件更新执行掩码后立即测试它。如果它是全开的,那么我们可以跳转到一个专门的代码路径,该路径以"掩码全开"的假设开始。如果它是全关的,那么我们可以直接跳转到 ‘if’ 语句体之后。否则,我们必须像常规 if 那样,使用常规掩码执行 if 的代码。显然,cif 在代码重复方面是有成本的,但在所有程序实例实际上都处于活动状态的情况下,它可以带来有意义的性能好处。

我觉得将其作为一个显式的语言特性很重要,而不是不引入新的关键字并试图为优化器找出合理的启发式方法来决定何时添加该检查以及何时不添加。诚然,这对程序员来说增加了一点额外的脑力开销,而且我确信有些人在编写 ispc 代码时没有使用这些特性,但本可以从这些特性中受益。不过,这再次表明我倾向于编译器在生成的代码方面是直接和可预测的;这是另一个专注于以性能为导向的程序员作为最重要用户类型的案例。

高效的 SPMD 加载和存储
在 SIMD 硬件上运行的 SPMD 程序的加载和存储是"有趣"的。通常,向量中的每个程序实例可能正在读取或写入内存中完全不同的位置。此外,其中一些实例可能是非活动的,在这种情况下绝对不能发出读取或写入。(我们之前在了解实现 scatter 时已经看到了这个问题的一点端倪。)

通常,我们需要为 SPMD 读取发出一条 gather 指令,为写入发出一条 scatter 指令;这些指令允许在访问的内存位置方面具有完全的灵活性。即使这些可以作为原生指令使用(就像在 AVX-512 中一样),如果我们访问的内存位置是连续的,使用向量加载和存储的性能会好得多——最好只在真正需要时才使用 gather 和 scatter。

不幸的是,我们需要在编译时做出这个决定。对于 GPU(据我理解它们如今的工作方式),所有这些很大程度上是运行时的区别:编译器只是发出相当于 gather 或 scatter 的指令,然后运行时的实际性能取决于访问的位置是否以各种方式相干。(避免存储体冲突等等。)这样好得多,因为访问的位置通常是数据相关的,因此它们的相干性在编译时永远无法像在运行时那样清楚。

但 CPU 不是这样工作的,所以我们必须在编译器中尽力而为。

volta 前端完全不会在这方面耍小聪明;除了通过 uniform 进行的标量内存访问之外,它一开始只是为所有 SPMD 读写生成试探性的 gather 和 scatter。volta 在 LLVM IR 中声明了一大堆伪 gather 和 scatter 函数,例如:

1
2
declare <WIDTH x float> @__pseudo_gather64_float(<WIDTH x i64>,
    <WIDTH x MASK>) nounwind readonly

(WITDH 和 MASK 通过宏扩展步骤设置为具体值。)

然后,对于任何 SPMD 内存读取浮点数(例如,在上面那个 increment() 示例中加载一个 SIMD 向量容量的值),在 64 位目标上,volta 会发出一个对 __pseudo_gather64_float 的调用,为每个 SIMD 通道提供一个唯一的指针以及执行掩码。

这些伪函数在早期的 LLVM 优化通道中保持未定义状态。随后,自定义的 volta 优化通道开始尝试改进它们。有很多可以做得更好的地方:

  • 如果所有指针都具有相同的值,volta 会替换为标量加载和向量广播。
  • 如果可以确定 SPMD 实例正在读取连续的内存位置(如在 increment() 中),则使用向量加载。
  • 如果确实需要 gather,或者编译器无法确定,那么就使用 gather。(然后特定目标的 IR 将要么发出原生指令,要么用一系列指令等效实现。)

我希望所有这些复杂性都不是必要的,但如果是不必要地发出了 gather 或 scatter,性能会显著下降,所以花大力气解决这个问题是值得的。最终,这些优化通道得到了大量关注,并且在检测适当模式方面变得相当稳健,我认为这是所能期望的最好结果了。

后来,我花了一些时间实现了一些稍微更复杂的方法来避免 gather,将那些可以更好地表示为向量加载和混洗的读取进行优化。考虑这个函数:

1
2
3
4
5
void reduce(uniform float ptr[], uniform float result[], uniform int count) {
    foreach (i = 0 ... count) {
        result[i] = ptr[i/2];
    }
}

在 8 宽目标上,最好发出一个 4 宽加载并进行向量混洗——比 gather 快得多。对于像这样的函数:

1
2
3
4
5
6
7
void deinterleave(uniform float ptr[], uniform float a[],
                  uniform float b[], uniform int count) {
    foreach (i = 0 ... count) {
        a[i] = ptr[2*i];
        b[i] = ptr[2*i + 1];
    }
}

有经验的内部函数程序员会使用两次向量加载然后进行一些混洗。用现代的 ispc 尝试这个,会发出一个 gather;我发誓这些过去是由那个优化处理的。这是另一个以后需要深入研究的小问题。

总之,最后所有这些加起来在 ispc 中构成了大约 6k 行代码的自定义 LLVM 优化通道;所以也许这个编译器毕竟不是完全"笨"的。然而,所有这些中并没有太多深奥的编译器魔术,我认为这使得编译器的输出仍然相当可预测。

明天:激动人心的时刻,让 volta 开源的时刻。

下一篇:开源发布与 volta 的终结

注释

  1. volta 的 foreach 灵感来源于 Mark Lacey, T. Foley, Jefferson Montgomery, 和 Geoff Berry 正在构建的一种以 GPU 为目标的语言中的相关结构。 ↩
  2. 一个合理的批评是,我们正开始走向程序员需要做一些繁琐的事情来让优化器按他们意愿行事的状态。我认为有一个更广泛的有趣问题,关于程序员如何清晰直接地向编译器提供程序将要处理的数据的特征信息,以帮助优化。不过,这不是我最终在 volta 中解决的问题。 ↩

ispc 的故事:开源发布与 volta 的终结(第九部分)

照例声明: 这全是凭记忆所写,而且已经过去好几年了。如果你当时在场,发现我记错了什么,请发邮件给我,我很乐意更正。

2011年春天,公司进行了一次重组,大部分图形软件部门的人并入了硬件部门。对我来说,加入他们那边没有意义,所以我留在了编译器组,向英特尔院士、编译器组首席技术官 Geoff Lowney 汇报。我很遗憾不能再为 Elliot 工作,并且在组织上也离开了我的图形部门朋友们。我也有点担心:感觉有点像搬到了"敌占区"。

幸运的是,Geoff 非常棒:甚至在我加入他的团队之前,他对 volta 的态度就是开放和充满知识好奇心的——“它效果这么好,真的很有趣;我们能从中学到什么?” 这一点加上他在编译器方面的深厚造诣,真的很棒;我从他那里学到了很多。在我剩余的英特尔时光里,他都是这个项目的坚定支持者;我对他一路上的所有帮助表示万分感谢。

我继续开发 volta,大约在春末的时候,我开始觉得它已经准备好面向更广阔的世界了。内部已经有足够多的人使用过它并且体验良好,这让我有信心在更广泛的用户群中也能进展顺利。而且我很兴奋能够向英特尔外部的程序员传递这个信息:你的 CPU 拥有的计算能力可能远超你的想象;关键在于要有合适的编程模型。

我之前的经理 Elliot 已经同意让我将 volta 开源,而且我知道我可以完全信任他的承诺。但问题是,必须由你当前的副总裁批准,才能开源在其组织内开发的任何东西。我的新副总裁负责编译器团队。

他对开源这件事并不那么热心。

有些担忧是这会令客户感到困惑,同时拥有一个开源编译器和一个商业编译器;还有人担心如果我某天离开英特尔,谁来维护它——诸如此类的问题。可能还有更深层次的原因,但没人明说。

我们来回讨论了几次,但最终决定是:不,编译器不会被开源。(但我可以继续研究它,并继续遵循"影响"生产编译器这条崇高道路,尽管到目前为止这并未产生任何可见的效果。)这个决定显然让我非常沮丧,因为我一直以来都是在预期它最终会开源的前提下继续开发 volta 的。

至少,接下来该怎么做是显而易见的:如果 volta 将被永远锁在英特尔内部,那我就没有理由再继续开发它了,而且在那时,我在那里也没有其他感兴趣的工作可做了。

于是,我提交了辞呈。

态度立刻发生了转变,开源批准下来了。我赶紧尽快处理细节,并将代码推送到 GitHub,以免情况有变,我的授权被撤销。

RIP volta, ispc 长存
英特尔(理所当然地)对产品命名有非常严格的规定。其中包括,产品名称必须以"Intel"开头,并且必须精确描述产品的功能。没有多少发挥创造力的空间,一旦编译器开源,“volta” 作为其实际名称就立刻夭折了。

这非常符合英特尔的典型作风:他们害怕因商标侵权而被起诉,这种担忧压倒了为事物取个好名字的考量。(或者可以这样理解,审批名称的人极度渴望自保,以至于制定了确保永远不会发生商标诉讼的规则,这样他们就不会因为放行任何更大胆的名称而惹上麻烦。)总之,有机会可以用这个视角去看看英特尔的产品名称——“Intel® SSD 730 Series” 等等。

所以,必须是"Intel"并且精确描述其功能。好吧,那么它就是"Intel SPMD Program Compiler",简称 ispc。我仍然对"volta"被那个怪异的名称取代感到有点难过——“program compiler”,我的意思是,真的吗?

具有讽刺意味的是,这个新名字让编译器听起来比它实际的情况更"官方",更像是一个得到英特尔广泛支持的东西,而事实并非如此。

初始发布
在商标部门批准了名称之后,还有一些行政琐事,然后要获得批准将代码发布到 GitHub 上,这在当时是相当新奇和另类的,尤其是从英特尔的视角来看。

我花了很多时间打磨代码和文档。我希望源代码是干净且注释良好的,我希望文档是详尽的。我认为尽可能留下良好的第一印象,对于吸引人们的注意力和让更多人使用它来说是时间花得值得的。

现在让我后悔的是,我那时还从一个全新的 git 代码库开始。当时,我不想让我在确定编译器计划之前的所有摸索和探索公之于众,而且有一半的提交信息是"小修复"或"添加了待办事项"有点尴尬。现在我真希望能仔细翻阅所有这些,弄清楚早期历史的更多细节。

无论如何,代码于 2011 年 6 月 21 日在 GitHub 上线了。那差不多是我开始捣鼓 LLVM 一年之后。

那天晚上我发了一些邮件,并在推特上发布了公告:

我过去大约一年一直在忙活的事情…… Intel SPMD Program Compiler (ispc) 现在可以在 ispc.github.com 上获取了。

还有

原生的、高性能的 {SPMD, SIMT, map/kernel, 着色器风格} CPU 编程。ispc.github.com。唤醒你沉睡的 SIMD 单元!

(请注意,那还是在 140 字符推文的"远古时代",所以需要两条推文才能说完。)

这就是所有的"市场营销"了。人们开始尝试使用它,并看到了好的结果;一切继续像宣传的那样工作。呼,松了一口气。

下次: 作为开源项目继续开发 ispc,向学术界介绍 ispc 的经历,以及我离开英特尔。

下一篇:传播理念与离开英特尔

注释

  1. 谷歌图片搜索能响应"Bjork crying"并提供这张她小时候伤心或者可能困倦的照片,是不是很棒? ↩

ispc 的故事:传播理念与离开英特尔(第十部分)

首次推送到 GitHub 之后,出现了一些 bug 修复(幸好没有太尴尬的)和拉取请求;一切似乎进展顺利。对 AVX2 的初步支持于 2011 年 12 月进入 ispc 代码库;看起来它在 2012 年 1 月被启用,但对 AVX2 的 gather 和 FMA 指令的支持直到那年夏天才完成。(我想可能是在等待 LLVM 对这些功能的支持,但不完全确定。)

2012 年夏天,Jean-Luc Duprat 开始致力于 ispc 对 Knight’s Corner(KNC)的支持,这是一个基于 Larrabee、面向 HPC 的架构,也是至强融核系列的第一个产品。Jean-Luc 曾是图形部门的人员,非常了解 SPMD,后来成为 KNC 的架构师,并希望 ispc 能在该平台上运行。由于缺乏 KNC 的 LLVM 后端,他实现了一种巧妙的方法,基于使用 LLVM 的 C++ 后端来生成 C++ 内部函数代码。只要有正确的头文件,这些代码就可以被编译成汇编。这是一种取巧的办法,但非常高明。

C++ 与方案撰写
Bill Mark 开始深入研究为 C++ 标准提出 SPMD 计算扩展需要涉及哪些细节;他是一位出色的系统设计师,非常擅长深入思考细节。在接下来的许多个月里,我们就语言设计及其与 C++ 的关系进行了多次长谈;最终,他提出了一个相当全面的 C++ 扩展设计,并称之为"Sierra"。关于 ispc 中指针的正确设计就是这些讨论的成果;事实证明这有点微妙。

一位实习生用 Clang 实现了这些想法的一个原型,并取得了良好的初步结果;Clang 清晰的设计使得实现相对简单直接。看到像 lambda 表达式和模板这样的特性直接在 SIMD 上的 SPMD 代码中工作,真的很棒。Bill 设计中的许多想法后来出现在这篇论文中。

Bill 和我在 2012 年合写了一篇关于 ispc 的论文。我认为它很好地捕捉了系统的设计和实现考量,并且深入讨论了先前与 ispc 有很多共同点的 SPMD 语言。我们在当年的一个新并行计算会议 InPar 上发表了它。

InPar 与英伟达的 GTC 大会同期举行,这意味着会议重点 heavily 偏向 GPU。说到"重点 heavily 偏向 GPU",我的意思是我们的论文是唯一一篇关于 CPU 的。然而,在听众的大力支持下,我们赢得了最佳论文奖。我们的奖品是一块顶级的英伟达 GPU。

与学术界交流
Geoff Lowney 提供的巨大帮助之一,是安排我向学术研究人员就 ispc 做几次外部演讲。其中一次促成了我对伊利诺伊大学香槟分校为期两天的访问,并在那里做了一次讲座。

第一天上午,我与英特尔香槟-厄巴纳办公室的一群人度过,非常棒——他们聪明、思想开放且有趣。然后我有幸和 David Kuck 共进午餐,这也非常棒。事实证明,他对并行编程略知一二。

不过有个小插曲:显然午餐的鸡肉沙拉里的鸡肉出了问题;结果导致了食物中毒,我在酒店房间里度过了当天下午和晚上的剩余时间,状态很不好,并且非常担心第二天在大学里的演讲会怎么样。要在观众面前站立一个多小时,同时还要条理清晰地演讲,看起来相当悬。

即使在不生病的时候,我也总是担心向编译器研究人员谈论 ispc;编译器不是我的领域,我担心自己对先前工作的了解不完整。我想象自己向一位教授解释这个想法,然后对方说:“哦,那是 Hazenburger 变换,最早在 1975 年就被描述了。我的本科编译器课程上周刚把它作为作业实现了。你做的事情还有什么新东西吗?”

呃,没有——就这些。(到现在我已经很放心了,毕竟根本没有什么 Hazenburger 变换。)

我对 UIUC 的演讲格外紧张,因为 Vikram Adve 是那里的教员,并且会出席。他不仅是著名的编译器研究员,还是 Chris Lattner 的博士导师;LLVM 就是在 UIUC 起步的。所以,在我设想的最坏情况下,当众出丑的可能性更大了,现在还要加上担心自己是否能从食物中毒中完全恢复。就在演讲前,我侦察了最近的洗手间位置,以便知道紧急情况下该往哪里跑。

令我欣慰的是,演讲进行得很顺利。Vikram 人真的很好,我们之后愉快地聊了聊;他似乎觉得这些想法很有趣。演讲被录了下来,但链接似乎失效了。这样可能也好;我可以避免看自己视频的尴尬。幻灯片仍然在线;它们展示了当时项目的状况和传达的主要信息。

几周后,在另一所大学的并行计算实验室进行的演讲就不那么顺利了。一个不好的预兆是,本该介绍我的那位教员直到原定开始时间 20 分钟后才出现。在尴尬地站了 10 分钟等待有人来开场之后,我最终只好自己做了介绍并开始演讲。

在演讲后的问答环节中,一位研究生坚持认为,我在结果中报告的在一台 40 核机器上实现的 180 倍加速纯粹归功于多线程,我怎么确定 SIMD 起了任何作用?而且,据他说,现在没有一个有趣的工作负载不是大规模并行且能在 GPU 上运行良好的,因此让东西在 CPU 上跑得快并没有什么意义。

当邀请我做演讲的那位教员告诉我,他没有安排演讲后与实验室研究人员的任何会议(这原是邀请的一部分)时,我反而有点松了一口气。

离开英特尔
这一切的结局有点讽刺。

很长一段时间里,我都极力避免组建一个团队来开发 ispc;一路上有很多其他人参与进来,投入其中,并做出了关键贡献——T. Foley、Bill Mark、Jean-Luc 以及许多其他人。他们都在不同的组织,自愿贡献他们能够且愿意投入的时间。

不试图在此基础上进一步正规化,是一种防御策略。一个有组织的 ispc 工作小组会成为一个更好的攻击目标:如果我获得了人员编制并雇人来组建一个专注于 ispc 的团队,我们可能会高效工作一段时间。然而,随着时间的推移,那些讨厌鬼很可能会施展他们娴熟的伎俩,说服管理层这些人可以更好地用于其他更重要的事情上。如果成功,那么噗的一声,所有人都会被调去加入其他小组,项目也就分崩离析——这正是他们的实际目标。

只有我一个人的话,就没有什么明显的目标了。

2012 年秋天,我还是去找了 Geoff Lowney,请求仅仅增加一个人的人员编制来帮助我进行 ispc 开发。目标不算太大,而且那时正是开始认真支持 AVX-512 的好时机;在这方面有很多工作要做。他爽快地答应去促成此事。几天后,当他告诉我没问题时,我感到的是……恐惧。

尤其是在 ispc 开源之后,我一直能够比较无忧无虑:编译器已经存在于世,运行良好,人们喜欢它。我可以基本上按部就班地继续开发它。如果英特尔内部情况变得怪异——办公室政治、糟糕的重组,无论什么——我知道我可以一走了之,而不会留下太多未竟之事。我从未计划在英特尔度过我的整个职业生涯,所以我打算只要在这里比离开更有趣就待着,并在合适的时机离开。

但是,让一个人加入这个项目?那我就要对他负责,必须尽我所能保护他免受政治影响。更糟的是,我将不再能随时离开英特尔——那样对那个人不公平,尤其因为如果我离开,他很可能会被重组到其他项目中去。我意识到,增加人手实际上等于承诺自己至少再待一两年。

考虑到之前所有的起起落落,我还没有准备好做出那样的承诺。进一步思考后,似乎这可能是时候离开了;ispc 状态良好,没有什么重大的缺失。继续按部就班地工作并没有太大的吸引力。

于是,我辞职了,那次是认真的。当我解释原因时——正是他批准了我最初请求的人员编制,让我意识到是时候离开了——Geoff 有点惊讶,但他表现得非常冷静,令人佩服。我用英特尔邮箱地址进行的最后一次提交是在 2012 年 9 月 14 日。

接下来, 将介绍一些用 ispc 编写的大型系统、设计回顾,以及一点基于 ARM 的兴奋点。

下一篇:回顾与反思

注释

  1. 与此相关,Ingo Wald 写了一个 SPMD on SIMD 语言原型,IVL,它直接将抽象语法树转换为 C++ 内部函数代码。 ↩

ispc 的故事:回顾与反思(第十一部分)

随着开源发布,我曾希望 ispc 能播下使其自身被遗忘的种子。我希望有一天它能被一个更好的、在 SIMD 上实现 SPMD 的编译器所超越,理想情况下,这个编译器能成为像 Clang、GCC 或 MSVC 这样被广泛使用的编译器的一部分。我非常喜欢 ispc,直到今天仍然享受用它来编写代码——我仍然认为它是一个很棒的工具。真正的成功应该是有人采纳了这个想法并做得更好,使得这种方法无处不在。

至少 ispc 存活了下来,并且似乎有满意的用户;我对此感到非常兴奋。我也很高兴英特尔有一些人在继续维护 ispc。英特尔的同事们在对 AVX-512 的良好支持和修复用户发现的 bug 方面做得非常出色。

如今的 ispc 能生成非常漂亮的 AVX-512 代码;这里是 aobench 的一小段代码,展示了那些可爱的 zmm 寄存器和一些 AVX-512 掩码管理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
        vsubps    %zmm6, %zmm16, %zmm0
        vsqrtps   %zmm7, %zmm6
        vsubps    %zmm6, %zmm0, %zmm0
        vmovaps   2368(%rsp), %zmm6
        vcmpnleps %zmm0, %zmm6, %k1
        vcmpnleps %zmm16, %zmm7, %k1 { %k1 }
        vcmpnleps %zmm16, %zmm0, %k0 { %k1 }
        kmovw     %k0, %ecx
        testw     %cx, %cx
        je        LBB1_32

我或许应该找点时间,在支持 AVX-512 的 CPU 上享受一下编写和运行 ispc 程序的乐趣。

应用情况
至少有几个相当大的系统是用 ispc 编写的;它似乎经受住了考验。

我曾用 ispc 写过一个 Reyes 渲染器,可惜最终没能纳入 ispc 发行版的示例中——我始终没有彻底完成它。那有近 1 万行 ispc 代码。我觉得 ispc 很好地证明了其价值:对于渲染器需要做的几乎所有事情,我都能生成良好的 SIMD 代码:细分时的贝塞尔曲线求值、着色、纹理过滤、光栅化、遮挡剔除等等。任何人都不可能用内部函数手写所有这些代码。

翻找我在英特尔时的旧推文,我找到了它生成的一张图像:

这个场景有 140 万个双三次曲面片;地平面应用了纹理和置换贴图。在一个 4 核 AVX1.1 系统上,以 720p 分辨率、每像素 16 个采样渲染该场景耗时 634 毫秒。在我看来这相当快了。

Embree,英特尔的高性能光线追踪库,广泛使用了 ispc。他们使用 ispc 让我非常激动——那个团队里的一些人是极其出色的内部函数程序员;他们的标准很高。

梦工厂更是用 ispc 编写了他们新的生产渲染器 MoonRay。他们为此写了一篇论文,其中包含关于向量化影响的广泛测量。看到向量化在如此复杂的系统中也能运行良好,真是太好了;事实证明——想想看——这种 SIMD 技术不仅仅适用于局部内核。

批评与反思
总的来说,我对这门语言最终呈现的样子相当满意。有具体的程序我想用 volta 来编写,这对一路上的设计决策提供了坚实的依据。一个小例子:我自然想用它写一个光线追踪器,但也许起初我只想在 volta 中做光线遍历。因此,让从 C/C++ 调用 volta 以及在不同语言间共享基于指针的数据结构变得容易,就成了设计的核心部分。

设计一个东西来解决你自己的问题可能很危险:最坏的情况是,它对其他任何人都没用。但这总比设计一个对你没用、但你想象别人会想要的东西要好。我当时相当确定,我正在考虑的那些用例不仅符合图形领域其他人想做的事情,而且也可能适用于其他领域。

通过这种方式构建,我认为 volta 在很多方面都做得不错,但随着经验的积累和视角的拓宽,很明显在设计和实现上仍存在一些粗糙之处和需要改进的地方。

  • 侧重于 32 位数据类型: 我个人感兴趣的大多数计算主要基于 32 位浮点数。在我编写优化通道和查看编译器汇编输出时,这些得到了最多的关注,这在某种程度上损害了 64 位浮点数的代码质量,并且肯定也损害了 8 位和 16 位整数数据类型的代码质量。
  • 每个源文件固定一个 SIMD 向量宽度: 在 ispc 中,SIMD 向量宽度是在编译时按每个源文件固定的。然而,在计算的不同部分使用不同的 SIMD 宽度通常很有用,例如在处理不同大小的数据类型时。能够以更细的粒度来改变这一点会更好。
  • unmasked 关键字: ispc 提供了一个 unmasked 关键字,可以在定义函数或语句前使用;它让程序员向编译器指示,在此时应假设掩码为"全开"。对于那些希望在安全的情况下(即无需掩码即可进行计算时)削除每一个不必要指令的程序员来说,这是一个有用的工具,但它很危险,并且并不真正符合 SPMD 编程模型;它或多或少是一种为了解决硬件限制而泄露到语言中的变通方法。
    • 补遗: 在重新查阅文档后,我想起 unmasked 使得在 ispc 中表达嵌套并行成为可能,我想这毕竟不是坏事,但可能有更好的方法来实现。
  • 显式向量与 SPMD: 如果能支持映射到 SIMD 通道的显式向量,并能在 SPMD 和这些显式向量之间分割 SIMD 通道,那将会很好。这不仅可以通过语言提供显式向量计算,还能表达兼具向量并行性和数据并行性的计算。
  • 嵌入 C++: 如前所述,如果能将 SPMD 功能在 C++ 中可用,那将很好;这将能实现与应用程序代码更轻松的互操作,并且能够使用模板、lambda 表达式,甚至可能是虚函数的全部功能,这将非常棒。

不受欢迎的拉取请求
接近尾声时,我应该为给英特尔的一些同事带来的尴尬处境而道歉。

离开英特尔后,我来到谷歌,最终从事在 ARM CPU 上运行的工作。我觉得为 ARM 的向量指令集 NEON 编写一个后端会很有趣。我在 2013 年 SIGGRAPH 会议期间,在酒店的闲暇时间里完成了这项工作。只花了几天时间,遵循了前面描述的相同路径。

我发现并提交了 LLVM NEON 后端的一些 bug。在修复之后,面向 NEON 的 ispc 可以工作了,但加速效果相当平淡。在 4 宽英特尔向量单元上,ispc 能可靠地为 SPMD 程序带来 3-4 倍的加速,而在当时我使用的 ARM CPU 上,2 倍加速更常见。虽然也有提升,但远不如在英特尔 CPU 上那样令人惊喜。不过,我认为让它对其他开发者可用仍然是有用的;之前已经有一些开发者请求过这个功能。

尽管我仍然拥有对 GitHub 代码库的提交权限,我还是将这些更改打包成了一个拉取请求。我认为 ispc 在那时已经归英特尔维护,应该由他们决定是否接受这些更改。

我忍不住在 2013 年 7 月 20 日格林威治标准时间 15:02 发了两条推文:

完成了 ispc NEON 后端:github.com/mmp/ispc/tree/… 测试通过,示例工作正常等等。(附上在 a15 上运行的 aobench 结果。)

现在等着看当前的维护者会如何处理这个拉取请求。:-)

我绝对低估了这种情况的敏感性。后来我被告知,内部对此进行了激烈的讨论。我认为没有人愿意成为那个接受拉取请求的人;对于一个英特尔员工来说,允许在一个由英特尔分发和冠名的编译器中添加 ARM 支持,没有任何好处,却可能带来一大堆负面影响。

我特别感到遗憾的是,被我置于这种棘手境地的那些人,正是那些一直支持 ispc 并维持项目运行的人。其中一位给我发了邮件,说他们不会接受这个拉取请求。

在格林威治标准时间 22:15,即我的第一条推文 7 小时后,我发了推文:

令人印象深刻的快速拉取请求拒绝。

我决定直接分叉代码库;这似乎是一个合理的选择。

推送了带有针对 int8 和 int16 计算特化的 NEON 分支的 ispc:github.com/mmp/ispc/tree/…。假设这个分支会长期存在。

然而,之前曾致力于为 ispc 添加 Knight’s Ferry 支持的 Jean-Luc Duprat 认为,将其纳入代码库是正确的——对用户如此,甚至对英特尔也是如此。他当时已不在英特尔,但仍然拥有向 GitHub 代码库提交的权限,于是他继续操作并接受了这个拉取请求。就这样:NEON 目标进入了官方代码库。撤销它可能会更加尴尬,所以它就留在了那里。Jean-Luc 不久后失去了他的提交权限。我很确定他觉得这是值得的。

ARM 支持仍然在那里,但在英特尔官方的二进制发行版中并未启用。这似乎是一种不错的折中方式。

我们还没完全结束。明天还有一篇简短的帖子,内容会比较哲学化。

下一篇:后记

ispc 的故事:后记(第十二部分)

卓越若无对手相伴,亦会凋零:当我们看到它何其伟大,力量何其磅礴之时,正是它通过坚忍展现其威力之际。我向您保证,善良之人亦应如此:他们不应畏惧面对艰难困苦,亦不应抱怨命运;无论发生何事,善良之人都应坦然接受,并努力将其转化为善果;重要的不是你承受了什么,而是你如何承受。

——塞内加,《论天命》

在英特尔的时光里,有很多不愉快的时刻。尽管我很高兴如今已不在那里,但那段时期却成为了技术创造力和构建至今仍引以为傲的事物的时期。

尽管经历了那些荒唐事,我不能说今天我完全后悔那段时光。也许部分原因是时间的流逝,冲淡了关于压力的记忆,忘记了政治斗争涌动时的无力感。部分原因是知道最终,一切实际上都还不错。

谷歌以其"Googliness"的理念为傲,即认为那里的每个人都是友善、快乐的好人,大家互相帮助,朝着同一个方向努力。基本上确实如此:五年里,我只遇到过一回需要应付那种咄咄逼人的办公室政治,而那一次也很快被管理层制止了。绝对没有同事 actively 破坏你的事情。据我所见,这类伎俩会迅速被谷歌的文化抗体所排斥。

谷歌是一个令人愉快的地方。我不会希望它是别的样子。但有时我会纠结于一个问题:这是否伴随着某些代价。

我是否会在一个更具"Googliness"的环境中写出 volta?

更切题地说:我是否会在一个没有几个我一心想要证明他们是错的、且颇具影响力的混蛋的环境中写出它?

需要明确的是,我在谷歌也做成了一些我认为不错的事情,并且没有那种对抗性的动机——这是一个绝佳的工作环境。我并不认为混蛋是进步的必要因素,但我忍不住去想,最终他们是否以其特有的方式为 ispc 做出了"贡献"。

也许塞内加确实道出了一些真谛。

感谢您阅读至此。明天我们将开始我的 Larrabee 回忆录。开个玩笑,只是玩笑。我们到此为止了。我绝不会去写那个,而且我也该放个博客假期了。

使用 Hugo 构建
主题 StackJimmy 设计