对 Java JIT 编译之父 Cliff Click 的精彩采访

对 Java JIT 编译之父 Cliff Click 的精彩采访悬崖点击 — Cratus(用于流程改进的物联网传感器)的首席技术官,多家初创公司(包括 Rocket Realtime School、Neurensic 和 H2O.ai)的创始人和联合创始人,并多次成功退出。 Cliff 在 15 岁时编写了他的第一个编译器(Pascal for the TRS Z-80)! 他最出名的是他在 Java 中的 C2(节点之海 IR)方面的工作。 这个编译器向世界展示了 JIT 可以生成高质量的代码,这是 Java 成为主要现代软件平台之一的因素之一。 随后 Cliff 帮助 Azul Systems 使用纯 Java 软件构建了一个 864 核大型机,支持 500 毫秒内在 10 GB 堆上进行 GC 暂停。 总的来说,Cliff 成功地研究了 JVM 的各个方面。

 
这篇 habrapost 是对 Cliff 的一次精彩采访。 我们将讨论以下主题:

  • 过渡到低级优化
  • 如何进行大重构
  • 成本模型
  • 低级优化训练
  • 绩效改进的实际例子
  • 为什么要创建自己的编程语言
  • 性能工程师职业生涯
  • 技术挑战
  • 关于寄存器分配和多核的一些知识
  • 人生最大的挑战

采访由以下人员进行:

  • 安德烈·萨塔林 来自亚马逊网络服务。 在他的职业生涯中,他成功地从事过完全不同的项目:他测试了 Yandex 中的 NewSQL 分布式数据库、卡巴斯基实验室中的云检测系统、Mail.ru 中的多人游戏以及德意志银行中计算外汇价格的服务。 对测试大规模后端和分布式系统感兴趣。
  • 弗拉基米尔·西特尼科夫 来自网络黑客。 十年来致力于 NetCracker OS 的性能和可扩展性,该软件是电信运营商用来自动化网络和网络设备管理流程的软件。 对 Java 和 Oracle 数据库性能问题感兴趣。 官方 PostgreSQL JDBC 驱动程序中十几项性能改进的作者。

过渡到低级优化

安德鲁:您是 JIT 编译、Java 和一般性能工作领域的知名人士,对吗? 

悬崖: 就像那样!

安德鲁:让我们从一些有关绩效工作的一般性问题开始。 您如何看待高级优化和低级优化(例如在 CPU 级别工作)之间的选择?

悬崖: 是的,这里一切都很简单。 最快的代码是永远不会运行的代码。 因此,你总是需要从高层次开始,研究算法。 更好的 O 表示法将击败更差的 O 表示法,除非有一些足够大的常数介入。 低级的事情放在最后。 通常,如果您已经充分优化了堆栈的其余部分,并且仍然剩下一些有趣的东西,那么这就是一个低水平。 但如何从高层次开始呢? 您如何知道已经完成了足够的高水平工作? 嗯……没办法。 没有现成的食谱。 您需要了解问题,决定要做什么(以免将来采取不必要的步骤),然后您可以发现探查器,它可以说出一些有用的信息。 在某些时候,你自己意识到你已经摆脱了不必要的东西,是时候进行一些低级的微调了。 这绝对是一种特殊的艺术。 有很多人在做不必要的事情,但行动如此之快,以至于他们没有时间担心生产力。 但这是在问题直白出现之前。 通常99%的时间没人关心我在做什么,直到一件重要的事情出现在没人关心的关键路径上的那一刻。 在这里,每个人都开始向你唠叨“为什么它从一开始就不完美”。 一般来说,性能总是有需要改进的地方。 但 99% 的情况下你都没有线索! 你只是想把事情做好,在这个过程中你会发现什么是重要的。 你永远不可能提前知道这件作品需要完美,所以,事实上,你必须在一切方面都做到完美。 但这是不可能的,你不这样做。 总是有很多事情需要解决——这是完全正常的。

如何进行大重构

安德鲁: 你是如何进行表演的? 这是一个交叉问题。 例如,您是否曾经不得不解决因许多现有功能的交叉而产生的问题?

悬崖: 我尽量避免。 如果我知道性能会成为一个问题,我会在开始编码之前考虑它,尤其是数据结构。 但通常你很晚才发现这一切。 然后你必须采取极端措施,做我所说的“重写和征服”:你需要抓住足够大的一块。 由于性能问题或其他原因,一些代码仍然需要重写。 无论重写代码的原因是什么,重写较大的代码几乎总是比重写较小的代码更好。 这一刻,每个人都开始害怕地发抖:“天哪,你不能碰这么多代码!” 但事实上,这种方法几乎总是效果更好。 你需要立即解决一个大问题,在它周围画一个大圆圈并说:我将重写圆圈内的所有内容。 边框比其内部需要替换的内容小得多。 如果这样的界限划分可以让你完美地完成里面的工作,那么你的双手就得到了自由,可以做你想做的事。 一旦你理解了问题,重写过程就会容易得多,所以咬紧牙关吧!
同时,当您进行大量重写并意识到性能将成为一个问题时,您可以立即开始担心它。 这通常会变成简单的事情,例如“不要复制数据,尽可能简单地管理数据,使其变小”。 在大型重写中,有一些标准方法可以提高性能。 它们几乎总是围绕数据展开。

成本模型

安德鲁:在一个播客中,您谈到了生产力背景下的成本模型。 你能解释一下你这是什么意思吗?

悬崖: 当然。 我出生在一个处理器性能极其重要的时代。 而这个时代再次回归——命运不无讽刺。 我开始生活在 256 位机器的时代;我的第一台计算机使用 XNUMX 字节。 确切地说是字节。 一切都非常小。 必须对指令进行计数,并且随着我们开始在编程语言堆栈中向上移动,语言的数量也越来越多。 先是汇编器,然后是 Basic,然后是 C,C 负责处理很多细节,例如寄存器分配和指令选择。 但那里的一切都很清楚,如果我创建一个指向变量实例的指针,那么我就会得到负载,并且该指令的成本是已知的。 硬件产生一定数量的机器周期,因此只需将要运行的所有指令相加即可计算出不同事物的执行速度。 每个比较/测试/分支/调用/加载/存储都可以相加并表示:这就是您的执行时间。 在致力于提高性能时,您肯定会注意哪些数字对应于小热循环。 
但一旦你转向 Java、Python 和类似的东西,你很快就会脱离低级硬件。 在 Java 中调用 getter 的成本是多少? HotSpot 中的 JIT 是否正确 内联的,它会加载,但如果它没有这样做,它将是一个函数调用。 由于调用处于热循环中,因此它将覆盖该循环中的所有其他优化。 因此,实际成本会高很多。 您立即无法查看一段代码并了解我们应该根据所使用的处理器时钟速度、内存和缓存来执行它。 只有真正投入到表演中,这一切才会变得有趣。
现在我们发现自己处于处理器速度十年来几乎没有提高的境地。 旧时光又回来了! 您不能再指望良好的单线程性能。 但如果你突然进入并行计算,那就非常困难了,每个人都像詹姆斯·邦德一样看着你。 这里的十倍加速通常发生在有人搞砸了事情的地方。 并发需要做很多工作。 要获得 XNUMX 倍的加速,您需要了解成本模型。 费用是多少? 为此,您需要了解舌头如何安装在底层硬件上。
马丁·汤普森为他的博客选择了一个很棒的词 机械同情! 您需要了解硬件将要做什么、它到底如何做以及为什么它首先要做它所做的事情。 使用它,可以很容易地开始计算指令数并计算出执行时间的去向。 如果你没有接受过适当的培训,你就只是在黑暗的房间里寻找一只黑猫。 我看到人们一直在优化性能,但他们不知道自己到底在做什么。 他们遭受了很多痛苦,却没有取得多大进展。 当我采用同一段代码,加入一些小技巧并获得五倍或十倍的加速时,他们会说:好吧,这不公平,我们已经知道你更好了。 惊人的。 我在说什么......成本模型是关于您编写​​的代码类型以及它在总体上的平均运行速度。

安德鲁:你怎么能在脑子里记住这么大的容量呢? 这是通过更多的经验实现的,还是? 这样的经验从何而来?

悬崖:嗯,我没有以最简单的方式获得经验。 在你可以理解每一条指令的时代,我用汇编语言进行编程。 这听起来很愚蠢,但从那时起,Z80指令集就一直留在我的脑海里、我的记忆里。 我一说话就记不住别人的名字,但我记得 40 年前写的代码。 很有趣,看起来像是一种综合症”白痴科学家“。

低级优化训练

安德鲁: 有没有更方便的方法?

悬崖: 是也不是。 随着时间的推移,我们使用的硬件并没有发生太大变化。 除了 Arm 智能手机之外,每个人都使用 x86。 如果您没有进行某种硬核嵌入,那么您就会做同样的事情。 好的,接下来。 这些说明几个世纪以来也没有改变。 你需要去汇编中写一些东西。 虽然不多,但足以开始理解。 你在微笑,但我却在认真地说。 你需要了解语言和硬件之间的对应关系。 之后,您需要编写一些代码,为一种小玩具语言制作一个小玩具编译器。 像玩具一样意味着它需要在合理的时间内制作完成。 它可以非常简单,但它必须生成指令。 生成指令的行为将帮助您了解每个人编写的高级代码与在硬件上运行的机器代码之间的桥梁的成本模型。 这种对应关系会在编译器编写的时候就被烙进大脑里。 即使是最简单的编译器。 之后,您可以开始研究 Java,事实上它的语义鸿沟要深得多,并且在其上架起桥梁要困难得多。 在 Java 中,要理解我们的桥是好还是坏、什么会导致它崩溃、什么不会导致它崩溃要困难得多。 但是您需要某种起点,让您查看代码并理解:“是的,这个 getter 每次都应该内联。” 然后事实证明,有时会发生这种情况,除了方法变得太大并且 JIT 开始内联所有内容的情况。 这些地方的表现可以立即预测。 通常 getter 工作得很好,但是当你查看大型热循环时,你会意识到那里有一些函数调用,它们不知道它们在做什么。 这就是 getter 广泛使用的问题,之所以没有内联,是因为不清楚它们是否是 getter。 如果你的代码库非常小,你可以简单地记住它,然后说:这是一个 getter,这是一个 setter。 在大型代码库中,每个函数都有自己的历史,一般来说,任何人都不知道。 探查器显示,我们在某个循环上损失了 24% 的时间,要了解该循环在做什么,我们需要查看内部的每个函数。 如果不研究函数就不可能理解这一点,这严重减慢了理解的进程。 这就是为什么我不使用 getter 和 setter,我已经达到了一个新的水平!
哪里可以获得成本模型? 嗯,当然,你可以读点东西……但我认为最好的方法是采取行动。 制作一个小型编译器将是理解成本模型并将其融入您自己的头脑的最佳方式。 一个适合微波炉编程的小型编译器是初学者的任务。 嗯,我的意思是,如果您已经具备编程技能,那么这就足够了。 所有这些事情,例如将您拥有的字符串解析为某种代数表达式,以正确的顺序从那里提取数学运算指令,从寄存器中获取正确的值 - 所有这些都是一次性完成的。 当你这样做的时候,它就会印在你的大脑里。 我想每个人都知道编译器的作用。 这将有助于理解成本模型。

绩效改进的实际例子

安德鲁:在提高生产力的同时,还需要注意什么?

悬崖: 数据结构。 顺便说一句,是的,我已经很久没有教过这些课程了…… 火箭学校。 很有趣,但是需要付出很多努力,而且我也有生活! 好的。 因此,在一门大而有趣的课程“你的表现在哪里”中,我给学生们举了一个例子:从 CSV 文件中读取了 70 GB 的金融科技数据,然后他们必须计算销售的产品数量。 定期报价市场数据。 自 1 年代以来,UDP 数据包转换为文本格式。 芝加哥商业交易所——各种各样的东西,比如黄油、玉米、大豆等等。 需要统计这些产品、交易笔数、资金和货物的平均流动量等。 这是非常简单的交易数学:找到产品代码(哈希表中的 2-XNUMX 个字符),获取金额,将其添加到交易集之一,增加交易量,增加价值,以及其他一些事情。 非常简单的数学。 这个玩具实现非常简单:所有内容都在一个文件中,我读取该文件并浏览它,将各个记录划分为 Java 字符串,在其中查找必要的内容并根据上述数学将它们相加。 而且它在低速下工作。

通过这种方法,发生的事情很明显,并行计算也无济于事,对吧? 事实证明,只需选择正确的数据结构即可将性能提高五倍。 这甚至让经验丰富的程序员感到惊讶! 在我的特定情况下,诀窍是您不应该在热循环中进行内存分配。 好吧,这不是全部事实,但总的来说 - 当 X 足够大时,您不应该突出显示“once in X”。 当 X 是两个半千兆字节时,您不应该分配任何“每个字母一次”、“每行一次”或“每个字段一次”或类似的内容。 这就是时间花费的地方。 这是如何运作的? 想象一下我打电话 String.split() или BufferedReader.readLine(). Readline 从通过网络传输的一组字节生成一个字符串,对于数亿行中的每一行,每行一次。 我拿起这条线,解析它并把它扔掉。 为什么我要把它扔掉——好吧,我已经处理过了,仅此而已。 因此,对于从这 2.7G 读取的每个字节,将在该行中写入两个字符,即已经是 5.4G,并且我不再需要它们,因此将它们丢弃。 如果你看一下内存带宽,我们会加载 2.7G,通过处理器中的内存和内存总线,然后将两倍的内存发送到位于内存中的线路,而当创建每个新线路时,所有这些都会磨损。 但我需要读取它,硬件读取它,即使后来一切都磨损了。 我必须把它写下来,因为我创建了一行并且缓存已满 - 缓存无法容纳 2.7G。 因此,对于我读取的每个字节,我会再读取两个字节并再写入两个字节,最终它们的比率为 4:1 - 在这个比率中,我们浪费了内存带宽。 然后事实证明如果我这样做 String.split() – 这不是我最后一次这样做,里面可能还有另外 6-7 个字段。 因此,读取 CSV 然后解析字符串的经典代码会导致内存带宽浪费,与您实际想要的相比大约为 14:1。 如果你放弃这些选择,你可以获得五倍的加速。

这并不那么困难。 如果你从正确的角度看代码,一旦你意识到问题,一切都会变得非常简单。 你不应该完全停止分配内存:唯一的问题是你分配了一些东西,它会立即死亡,并且在此过程中它会消耗重要的资源,在本例中是内存带宽。 所有这些都会导致生产力下降。 在 x86 上,您通常需要主动消耗处理器周期,但在这里您会更早地消耗掉所有内存。 解决办法是减少排放量。 
问题的另一部分是,如果您在内存条带耗尽时运行探查器,那么您通常会等待缓存返回,因为它充满了您刚刚产生的垃圾,所有这些行。 因此,每个加载或存储操作都会变得很慢,因为它们会导致缓存未命中 - 整个缓存变得很慢,等待垃圾离开它。 因此,分析器只会显示整个循环中涂抹的温暖随机噪声 - 代码中不会有单独的热指令或位置。 只有噪音。 如果您查看 GC 周期,您会发现它们都是年轻代并且超快 - 最多微秒或毫秒。 毕竟,所有这些记忆都会立即消失。 你分配了数十亿字节,他把它们削减、削减、再削减。 这一切发生得非常快。 事实证明,GC 周期很便宜,整个周期都有温暖的噪音,但我们希望获得 5 倍的加速。 这时,你的脑海里应该有什么东西关闭并响起:“这是为什么?!” 经典调试器中不会显示内存条溢出;您需要运行硬件性能计数器调试器并亲自直接查看。 但这并不能从这三个症状直接怀疑。 第三个症状是,当您查看突出显示的内容时,询问分析器,他回答:“您创建了 XNUMX 亿行,但 GC 是免费工作的。” 一旦发生这种情况,您就会意识到您创建了太多对象并烧毁了整个内存通道。 有一种方法可以解决这个问题,但并不明显。 

问题出在数据结构上:所有发生的事情背后的裸结构,它太大了,在磁盘上有2.7G,所以制作这个东西的副本是非常不可取的——你想立即从网络字节缓冲区加载它写入寄存器,以免对该行来回读写五次。 不幸的是,默认情况下,Java 并没有为您提供这样的库作为 JDK 的一部分。 但这是微不足道的,对吧? 本质上,这些 5-10 行代码将用于实现您自己的缓冲字符串加载器,它重复字符串类的行为,同时作为底层字节缓冲区的包装器。 结果,事实证明,您几乎就像处理字符串一样,但实际上指向缓冲区的指针正在移动到那里,并且原始字节不会复制到任何地方,因此相同的缓冲区会被一遍又一遍地重用,并且操作系统很乐意承担其设计目的,例如这些字节缓冲区的隐藏双缓冲,并且您不再需要处理无休止的不必要数据流。 顺便问一下,您是否明白,在使用 GC 时,可以保证在最后一个 GC 周期之后每次内存分配对处理器来说都是不可见的? 因此,所有这些都不可能在缓存中,然后就会发生 100% 保证未命中的情况。 当使用指针时,在 x86 上,从内存中减去寄存器需要 1-2 个时钟周期,一旦发生这种情况,你就得付费,付费,付费,因为内存全部打开了 九个缓存 – 这就是内存分配的成本。 真正的价值。

换句话说,数据结构是最难改变的。 一旦你意识到你选择了错误的数据结构,这会影响以后的性能,通常还有很多工作要做,但如果你不这样做,事情会变得更糟。 首先,你需要考虑数据结构,这很重要。 这里的主要成本落在胖数据结构上,它们开始以“我将数据结构 X 复制到数据结构 Y 中,因为我更喜欢 Y 的形状”的方式使用。 但复制操作(看起来很便宜)实际上浪费了内存带宽,这就是所有浪费的执行时间被埋葬的地方。 如果我有一个巨大的 JSON 字符串,并且想将其转换为 POJO 的结构化 DOM 树或其他内容,则解析该字符串并构建 POJO,然后稍后再次访问 POJO 的操作将导致不必要的成本 - 这是不便宜。 除非您在 POJO 上运行的次数比在字符串上运行的次数多得多。 您可以临时尝试解密该字符串并仅从其中提取您需要的内容,而不将其转换为任何 POJO。 如果所有这一切都发生在需要最大性能的路径上,没有 POJO 适合您,您需要以某种方式直接深入该行。

为什么要创建自己的编程语言

安德鲁:你说为了理解成本模型,你需要编写自己的小语言......

悬崖:不是一种语言,而是一个编译器。 语言和编译器是两个不同的东西。 最重要的区别在于你的头脑。 

安德鲁:顺便说一句,据我所知,您正在尝试创建自己的语言。 为了什么?

悬崖: 因为我可以! 我处于半退休状态,所以这是我的爱好。 我一生都在实现别人的语言。 我还在我的编码风格上做了很多工作。 还因为我看到其他语言的问题。 我发现有更好的方法来做熟悉的事情。 我会使用它们。 我只是厌倦了在自己、Java、Python、任何其他语言中看到问题。 我现在用 React Native、JavaScript 和 Elm 写作作为一种爱好,这不是为了退休,而是为了积极的工作。 我还使用 Python 进行编写,并且很可能会继续从事 Java 后端的机器学习工作。 有许多流行的语言,它们都有有趣的功能。 每个人都有自己的优点,你可以尝试将所有这些功能结合在一起。 所以,我正在研究我感兴趣的东西,语言的行为,试图提出合理的语义。 到目前为止我成功了! 目前我正在努力解决内存语义问题,因为我希望像 C 和 Java 一样拥有它,并获得强大的内存模型以及用于加载和存储的内存语义。 同时,具有像 Haskell 中那样的自动类型推断。 在这里,我尝试将类似 Haskell 的类型推断与 C 和 Java 中的内存工作混合起来。 例如,这就是我过去 2-3 个月一直在做的事情。

安德鲁:如果你构建一种吸收其他语言更好的方面的语言,你认为有人会做相反的事情:采纳你的想法并使用它们吗?

悬崖:新语言就是这样出现的! 为什么Java与C相似? 因为C有一个很好的语法,每个人都能理解,而Java受到了这个语法的启发,添加了类型安全、数组边界检查、GC,他们还改进了C的一些东西。他们添加了自己的东西。 但他们受到了很多启发,对吗? 每个人都站在前人的肩膀上——这就是进步的方式。

安德鲁:据我了解,您的语言将是内存安全的。 您是否考虑过实现 Rust 的借用检查器之类的东西? 你看过他吗,你对他有什么看法?

悬崖:嗯,我已经编写 C 语言很多年了,使用所有这些 malloc 和 free,并手动管理生命周期。 要知道,90-95%的手动控制寿命都具有相同的结构。 而且手动操作非常非常痛苦。 我希望编译器简单地告诉您那里发生了什么以及您通过操作取得了什么成果。 对于某些事情,借用检查器可以开箱即用地执行此操作。 它应该自动显示信息,理解一切,甚至不会给我带来呈现这种理解的负担。 它必须至少进行本地转义分析,并且只有在失败时,才需要添加描述生命周期的类型注释 - 这种方案比借用检查器或实际上任何现有的内存检查器要复杂得多。 “一切都很好”和“我什么都不懂”之间的选择——不,一定有更好的东西。 
因此,作为一个用 C 编写了大量代码的人,我认为支持自动生命周期控制是最重要的。 我也厌倦了 Java 使用多少内存,主要抱怨的是 GC。 当你在Java中分配内存时,你不会取回上一次GC周期时本地的内存。 在内存管理更精确的语言中情况并非如此。 如果调用 malloc,您会立即获得通常刚刚使用的内存。 通常你会用记忆做一些临时的事情,然后立即将其返回。 并且它立即返回malloc池,下一个malloc循环又将其拉出。 因此,实际内存使用量减少为给定时间的活动对象集加上泄漏。 如果一切都没有以完全不雅的方式泄漏,那么大部分内存最终都会进入缓存和处理器,并且运行速度很快。 但需要在正确的位置以正确的顺序调用 malloc 和 free 进行大量手动内存管理。 Rust 可以自行正确处理这个问题,并且在许多情况下可以提供更好的性能,因为内存消耗缩小到仅当前计算 - 而不是等待下一个 GC 周期来释放内存。 结果,我们得到了一种非常有趣的方法来提高性能。 而且相当强大——我的意思是,我在处理金融科技数据时做了这样的事情,这让我获得了大约五倍的加速。 这是一个相当大的提升,特别是在处理器没有变得更快并且我们仍在等待改进的世界中。

性能工程师职业生涯

安德鲁: 我也想问一下一般的职业。 您凭借在 HotSpot 的 JIT 工作而崭露头角,然后转到 Azul,这也是一家 JVM 公司。 但我们在硬件上的投入已经多于软件。 然后他们突然转向大数据和机器学习,然后转向欺诈检测。 这怎么发生的? 这些是非常不同的发展领域。

悬崖:我已经编程很长时间了,并且已经成功地参加了很多不同的课程。 当人们说:“哦,你是为 Java 进行 JIT 的人!”时,这总是很有趣。 但在此之前,我正在研究 PostScript 的克隆——Apple 曾将这种语言用于其激光打印机。 在此之前我做了一个 Forth 语言的实现。 我认为对我来说共同的主题是工具开发。 我一生都在制作工具,让其他人可以编写很酷的程序。 但我也参与了操作系统、驱动程序、内核级调试器、操作系统开发语言的开发,这些一开始很简单,但随着时间的推移变得越来越复杂。 但主要话题仍然是工具的开发。 我生命中的很大一部分时间是在 Azul 和 Sun 之间度过的,而且都是关于 Java 的。 但当我进入大数据和机器学习领域时,我重新戴上我的奇特帽子并说道:“哦,现在我们遇到了一个不平凡的问题,并且有很多有趣的事情正在发生,人们也在做事情。” 这是一条伟大的发展道路。

是的,我真的很喜欢分布式计算。 我的第一份工作是 C 专业的学生,​​从事广告项目。 这是 Zilog Z80 芯片上的分布式计算,收集由真实模拟分析仪生成的模拟 OCR 数据。 这是一个很酷且完全疯狂的话题。 但是存在问题,某些部分没有被正确识别,所以你必须拿出一张图片并将其展示给一个已经可以用眼睛阅读并报告其内容的人,因此出现了带有数据的工作,这些工作有自己的语言。 有一个后端处理所有这些 - Z80s 与 vt100 终端并行运行 - 每人一个,并且 Z80 上有一个并行编程模型。 星型配置中所有 Z80 共享的一些通用内存; 背板也是共享的,一半的 RAM 在网络内共享,另一半是私有的或用于其他用途。 具有共享...半共享内存的有意义的复杂并行分布式系统。 这是什么时候的事……我什至记不清了,大概是八十年代中期吧。 很久以前了。 
是的,假设30年已经是很久以前的事了,分布式计算的问题已经存在了很长一段时间,人们长期处于战争状态。 贝奥武夫-簇。 这样的集群看起来像......例如:有以太网,你的快速x86连接到这个以太网,现在你想要获得假共享内存,因为当时没有人可以做分布式计算编码,这太困难了,因此有是 x86 上带有保护内存页面的假共享内存,如果您写入此页面,那么我们告诉其他处理器,如果它们访问相同的共享内存,则需要从您那里加载它,因此类似于支持协议缓存一致性和相关软件出现了。 有趣的概念。 当然,真正的问题是别的。 所有这些都有效,但您很快就遇到了性能问题,因为没有人在足够好的级别上理解性能模型 - 存在哪些内存访问模式,如何确保节点不会无休止地相互 ping,等等。

我在 H2O 中想到的是,开发人员自己负责确定并行性在哪里隐藏和不隐藏。 我提出了一种编码模型,使编写高性能代码变得轻松简单。 但编写运行缓慢的代码很困难,看起来很糟糕。 你需要认真尝试编写缓慢的代码,你将不得不使用非标准的方法。 刹车代码一目了然。 因此,您通常会编写运行速度很快的代码,但您必须弄清楚在共享内存的情况下该怎么做。 所有这些都与大型数组相关,其行为类似于并行 Java 中的非易失性大型数组。 我的意思是,假设两个线程写入一个并行数组,其中一个线程获胜,而另一个线程相应地失败,并且您不知道哪个线程是哪个线程。 如果它们不是不稳定的,那么顺序可以是任何你想要的 - 这非常有效。 人们真正关心操作的顺序,他们将易失性放在正确的位置,并且他们期望在正确的位置出现与内存相关的性能问题。 否则,他们会简单地以从 1 到 N 的循环形式编写代码,其中 N 约为数万亿,希望所有复杂的情况都会自动变得并行 - 但这在那里行不通。 但在 H2O 中,这既不是 Java 也不是 Scala;如果您愿意,您可以将其视为“Java minus minus”。 这是一种非常清晰的编程风格,类似于使用循环和数组编写简单的 C 或 Java 代码。 但与此同时,内存可以以 TB 为单位进行处理。 我仍然使用H2O。 我不时在不同的项目中使用它——它仍然是最快的东西,比竞争对手快几十倍。 如果您使用柱状数据处理大数据,那么 H2O 很难被击败。

技术挑战

安德鲁:您整个职业生涯中最大的挑战是什么?

悬崖:我们正在讨论问题的技术部分还是非技术部分? 我想说最大的挑战不是技术挑战。 
至于技术挑战。 我简直打败了他们。 我什至不知道最大的一个是什么,但有一些非常有趣的,花了相当多的时间和精神斗争。 当我去Sun的时候,我确信我会做出一个快速的编译器,而一群前辈回应说我永远不会成功。 但我沿着这条路,编写了一个编译器到寄存器分配器,而且速度相当快。 它和现代 C1 一样快,但当时的分配器要慢得多,事后看来这是一个很大的数据结构问题。 我需要它来编写一个图形寄存器分配器,但我不理解代码表现力和速度之间的困境,这在那个时代存在并且非常重要。 事实证明,数据结构通常超过当时 x86 上的缓存大小,因此,如果我最初假设寄存器分配器将计算出总抖动时间的 5-10%,那么实际上结果是50%。

随着时间的推移,编译器变得更干净、更高效,在更多情况下不再生成糟糕的代码,并且性能越来越开始类似于 C 编译器产生的结果。当然,除非你写了一些连 C 都无法加速的废话。 如果您编写像 C 一样的代码,那么在更多情况下您将获得像 C 一样的性能。 你走得越远,你就越经常得到渐近与 C 级一致的代码,寄存器分配器开始看起来像是完整的东西......无论你的代码运行得快还是慢。 我继续研究分配器以使其做出更好的选择。 他变得越来越慢,但在其他人无法应付的情况下,他的表现却越来越好。 我可以深入研究寄存器分配器,在那里埋下一个月的工作,突然间整个代码的执行速度会加快 5%。 这种情况一次又一次地发生,寄存器分配器变成了一件艺术品——每个人都喜欢它或讨厌它,学院的人就“为什么一切都是这样做”这个主题提出问题,为什么不呢? 线扫描,有什么区别。 答案仍然是相同的:基于图形着色的分配器加上非常仔细地处理缓冲区代码等于胜利的武器,是无人能击败的最佳组合。 这是一件相当不明显的事情。 编译器所做的所有其他事情都经过了相当深入的研究,尽管它们也已达到艺术水平。 我总是做一些应该将编译器变成艺术品的事情。 但这些都没什么特别的——除了寄存器分配器。 诀窍是要小心 降低 在负载下,如果发生这种情况(如果有兴趣,我可以更详细地解释),这意味着您可以更积极地内联,而不会有在性能计划中陷入困境的风险。 在那些日子里,有一堆全规模的编译器,挂着小玩意和口哨,有寄存器分配器,但没有其他人能做到。

问题是,如果你添加需要内联的方法,增加和增加内联区域,使用的值集立即超过寄存器的数量,你必须削减它们。 当分配者放弃时,通常会出现临界水平,并且一​​个好的溢出候选者值得另一个,你将出售一些通常疯狂的东西。 这里内联的价值在于你损失了一部分开销,调用和保存的开销,你可以看到里面的值并可以进一步优化它们。 内联的代价是形成大量的实时值,如果你的寄存器分配器消耗的过多,你就会立即失败。 因此,大多数分配器都会遇到一个问题:当内联跨过某条线时,世界上的一切都开始被削减,生产力就会被冲进马桶。 那些实现编译器的人添加了一些启发式方法:例如,停止内联,从足够大的大小开始,因为分配会毁掉一切。 这就是性能图中的一个扭结是如何形成的——你内联,内联,性能慢慢增长——然后繁荣! – 它像一个快速的千斤顶一样掉下来,因为你的线太多了。 这就是 Java 出现之前一切的运作方式。 Java 需要更多的内联,所以我必须让我的分配器更加积极,这样它才能平稳而不是崩溃,如果内联太多,它就会开始溢出,但“不再溢出”的时刻仍然会到来。 这是一个有趣的观察,我突然想到了这一点,并不明显,但它得到了很好的回报。 我采用了积极的内联,它把我带到了 Java 和 C 性能并存的地方。 它们非常接近——我可以编写比 C 代码和类似代码快得多的 Java 代码,但平均而言,从整体来看,它们大致相当。 我认为这个优点的一部分是寄存器分配器,它允许我尽可能愚蠢地内联。 我只是内联我看到的所有内容。 这里的问题是分配器是否工作良好,结果是否是智能工作的代码。 这是一个巨大的挑战:理解这一切并使其发挥作用。

关于寄存器分配和多核的一些知识

弗拉基米尔:像寄存器分配这样的问题似乎是某种永恒的、无尽的话题。 我想知道是否有过一个看起来很有前途但在实践中却失败了的想法?

悬崖: 当然! 寄存器分配是您尝试寻找一些启发式方法来解决 NP 完全问题的领域。 而且你永远无法实现完美的解决方案,对吧? 这根本不可能。 看,提前编译 - 它的效果也很差。 这里的对话是关于一些普通情况的。 关于典型性能,因此您可以去测量您认为良好的典型性能 - 毕竟,您正在努力改进它! 寄存器分配是一个与性能有关的主题。 一旦你有了第一个原型,它就会工作并绘制出所需的内容,性能工作就开始了。 你需要学会很好地衡量。 它为什么如此重要? 如果你有明确的数据,你可以查看不同的区域并看到:是的,它在这里有所帮助,但这就是一切都崩溃的地方! 一些好的想法出现了,你添加了新的启发式方法,突然之间,平均而言,一切都开始变得更好了。 或者它没有启动。 我遇到过很多案例,我们正在为 XNUMX% 的性能而奋斗,这使得我们的开发与之前的分配器有所不同。 每次看起来都是这样:在某个地方你赢了,在某个地方你输了。 如果您拥有良好的绩效分析工具,您可以找到失败的想法并了解它们失败的原因。 也许值得让一切保持原样,或者采取更认真的方法进行微调,或者出去修复其他问题。 这是一大堆东西! 我做了这个很酷的黑客,但我还需要这个,这个,还有这个 - 他们的总组合提供了一些改进。 孤独的人可能会失败。 这就是 NP 完全问题的性能工作的本质。

弗拉基米尔:人们会感觉分配器中的绘画之类的问题已经解决了。 好吧,从你所说的来看,这已经为你决定了,那么这还值得吗……

悬崖: 没有这样解决。 你必须把它变成“解决”。 有困难的问题需要解决。 完成此操作后,就该提高生产力了。 您需要相应地处理这项工作 - 进行基准测试,收集指标,解释当您回滚到以前的版本时,您的旧黑客再次开始工作(反之亦然,停止)的情况。 并且在取得某些成就之前不要放弃。 正如我已经说过的,如果有一些很酷的想法不起作用,但在想法寄存器分配领域,它几乎是无穷无尽的。 例如,您可以阅读科学出版物。 尽管现在这个区域已经开始比年轻时移动得慢得多,并且变得更加清晰。 然而,有无数的人在这个领域工作,他们的所有想法都值得尝试,他们都在等待。 除非您亲自尝试,否则您无法判断它们有多好。 它们与分配器中的其他所有内容集成得有多好,因为分配器可以做很多事情,并且有些想法在您的特定分配器中不起作用,但在另一个分配器中它们很容易。 分配器获胜的主要方法是将慢速内容拉到主路径之外,并迫使其沿着慢速路径的边界分裂。 因此,如果您想运行 GC,请采取慢速路径、去优化、抛出异常,所有这些东西 - 您知道这些事情相对较少。 我查了一下,它们确实很罕见。 你做了额外的工作,它消除了这些慢速路径的许多限制,但这并不重要,因为它们很慢并且很少有人行走。 例如,空指针 - 它永远不会发生,对吧? 对于不同的事情,你需要有几条路径,但它们不应该干扰主要路径。 

弗拉基米尔:当同时有数千个核心时,您如何看待多核? 这是一个有用的东西吗?

悬崖:GPU的成功说明它相当有用!

弗拉基米尔: 他们很专业。 通用处理器怎么样?

悬崖:嗯,这就是 Azul 的商业模式。 答案出现在人们真正喜欢可预测性能的时代。 那时编写并行代码很困难。 H2O 编码模型具有高度可扩展性,但它不是通用模型。 也许比使用 GPU 更通用一些。 我们是在谈论开发这样一个东西的复杂性还是使用它的复杂性? 例如,Azul 教给我一个有趣的教训,一个相当不明显的教训:小缓存是正常的。 

人生最大的挑战

弗拉基米尔:非技术挑战呢?

悬崖:最大的挑战是不……对人友善和友善。 结果,我经常发现自己处于极度冲突的境地。 那些我知道事情出了问题,但不知道如何解决这些问题并且无法处理它们的人。 许多持续数十年的长期问题就是这样产生的。 Java 拥有 C1 和 C2 编译器的事实就是这一点的直接结果。 Java连续十年没有多级编译也是一个直接后果。 显然我们需要这样一个系统,但它不存在的原因尚不清楚。 我与一名工程师或一组工程师之间存在问题。 曾几何时,当我开始在 Sun 工作时,我是……好吧,不仅如此,我通常对所有事情都有自己的看法。 我认为你确实可以接受你的这个真相并正面讲述它。 尤其是因为我大多数时候都是正确的。 如果你不喜欢这种方式……特别是如果你明显错了并且胡言乱语……一般来说,很少有人能容忍这种形式的沟通。 虽然有些人可以,比如我。 我的一生都建立在精英原则之上。 如果你给我看有什么不对的地方,我会立即转身说:你胡说八道。 当然,我同时表示歉意,如果有的话,我会记下优点,并采取其他正确的行动。 另一方面,我对总时间中很大一部分时间的预测是正确的。 而且在人与人的关系上也不太管用。 我并不是想表现得友善,但我只是直率地问这个问题。 “这永远行不通,因为一、二、三。” 他们就像,“哦!” 还有其他后果可能最好忽略:例如,那些导致我与妻子离婚以及此后十年抑郁症的后果。

挑战是与人们的斗争,与他们对你能做什么或不能做什么、什么是重要的和什么不重要的看法的斗争。 编码风格面临许多挑战。 我仍然写了很多代码,在那些日子里我什至不得不放慢速度,因为我做了太多并行任务并且做得很糟糕,而不是专注于一项任务。 回想起来,我写了一半Java JIT命令的代码,即C2命令。 第二快的程序员写得慢一半,下一个慢一半,这是指数下降。 这排第七个人的速度非常非常慢——这种情况总是会发生! 我接触了很多代码。 我看着谁写了什么,无一例外,我盯着他们的代码,审查了他们每个人,并且仍然继续自己写的比他们中的任何人都多。 这种方法对人来说效果不太好。 有些人不喜欢这样。 当他们无法处理时,各种抱怨就开始了。 例如,我曾经被告知停止编码,因为我写了太多代码,这危及了团队,这对我来说听起来就像一个笑话:伙计,如果团队的其他成员消失了而我继续写代码,你只会损失一半球队。 另一方面,如果我继续编写代码,而你失去了一半的团队,这听起来像是非常糟糕的管理。 我从来没有真正想过它,也没有谈论过它,但它仍然在我脑海中的某个地方。 这个念头在我脑海中盘旋:“你们在开玩笑吗?” 所以,最大的问题是我和我与人的关系。 现在我更了解自己了,我长期担任程序员的团队领导,现在我直接告诉人们:你知道,我就是我,你将不得不对付我 - 如果我站起来可以吗?这里? 当他们开始处理这个问题时,一切都顺利了。 事实上,我不坏也不好,我没有任何恶意或自私的愿望,这只是我的本质,我需要以某种方式接受它。

安德鲁:就在最近,每个人都开始谈论内向者的自我意识和一般软技能。 对此您有什么想说的吗?

悬崖:是的,这就是我从与妻子离婚中学到的见解和教训。 我从离婚中学到的是了解自己。 这就是我开始理解别人的方式。 了解这种互动是如何运作的。 这导致了一个又一个的发现。 人们意识到我是谁以及我代表什么。 我在做什么:要么我全神贯注于任务,要么我在避免冲突,或者其他什么——这种程度的自我意识确实有助于保持自我控制。 在此之后一切都会变得容易得多。 我不仅在我自己身上发现了一件事,而且在其他程序员身上也发现了这一点:当你处于情绪压力状态时,无法用语言表达想法。 例如,你坐在那里编码,处于心流状态,然后他们跑向你,开始歇斯底里地尖叫,说有什么东西坏了,现在将对你采取极端措施。 而且你不能说一句话,因为你处于情绪紧张的状态。 所获得的知识可以让你为这一刻做好准备,度过它并继续执行撤退计划,之后你可以做一些事情。 所以,是的,当你开始意识到这一切是如何运作时,这是一个改变生活的巨大事件。 
我自己找不到合适的词语,但我记得动作的顺序。 关键是,这种反应既是身体上的反应,也是言语上的反应,而且你需要空间。 这样的空间,在禅宗意义上。 这正是需要解释的,然后立即退到一边——纯粹的身体退开。 当我保持口头沉默时,我可以在情感上处理这种情况。 当肾上腺素到达你的大脑,将你切换到战斗或逃跑模式时,你再也不能说什么,不 - 现在你是一个白痴,一个鞭打工程师,无法做出适当的反应,甚至无法停止攻击,而攻击者是自由的一次又一次地攻击。 你必须首先重新做回自己,重新获得控制,摆脱“战斗或逃跑”模式。

为此,我们需要言语空间。 只是自由空间。 如果你要说什么,那么你可以准确地说出来,然后去真正为自己找到“空间”:去公园散步,把自己锁在淋浴间里——这并不重要。 最重要的是暂时脱离这种情况。 一旦你关闭至少几秒钟,控制权就会恢复,你就会开始清醒地思考。 “好吧,我不是什么白痴,我不做蠢事,我是一个非常有用的人。” 一旦您能够说服自己,就该进入下一阶段:了解发生了什么。 你遭到攻击,攻击来自你意想不到的地方,这是一次不诚实、卑鄙的伏击。 这不好。 下一步是了解攻击者为什么需要这个。 真的,为什么? 也许是因为他自己很愤怒? 他为什么生气? 比如,因为他把自己搞砸了,无法承担责任? 这才是谨慎处理整个局面的方法。 但这需要回旋余地,即言语空间。 第一步是中断言语接触。 避免用言语讨论。 取消它,尽快走开。 如果是电话交谈,就挂掉——这是我从与前妻沟通中学到的技巧。 如果谈话进展不顺利,就说“再见”然后挂断电话。 电话那头:“等等等等”,你回答:“是的,再见!” 然后挂断电话。 你就结束谈话吧。 五分钟后,当你恢复理智思考的能力时,你已经冷静了一点,可以思考一切,发生了什么以及接下来会发生什么。 并开始制定深思熟虑的回应,而不是仅仅出于情绪做出反应。 对我来说,自我意识的突破正是在情绪紧张时无法说话。 摆脱这种状态,思考和计划如何应对和弥补问题——这些是当你无法说话时的正确步骤。 最简单的方法就是逃离出现情绪压力的情况,并停止参与这种压力。 之后你就变得能够思考,当你能够思考时,你就变得能够说话,等等。

顺便说一下,在法庭上,对方律师试图对你这样做——现在原因很清楚了。 因为他有能力把你压制到一个地步,比如你连名字都叫不出来。 从非常现实的意义上来说,你将无法说话。 如果这种情况发生在您身上,并且您知道您会发现自己处于一个激烈争论的地方,例如法庭,那么您可以与您的律师一起去。 律师会为你出面,停止言语攻击,并且会以完全合法的方式去做,失去的禅宗空间也会归还给你。 比如,我给家人打了几次电话,法官对此很友善,但对方律师却对我大喊大叫,我什至插不上话。 在这些情况下,使用调解员最适合我。 调解者停止了所有这些源源不断地倾注在你身上的压力,你找到了必要的禅宗空间,随之而来的是说话的能力回归。 这是一个完整的知识领域,其中有很多东西需要学习,有很多东西需要在你自己身上发现,所有这些都会变成对不同的人来说不同的高层战略决策。 有些人不会有上述问题;通常专业销售人员不会有这些问题。 所有这些以文字为生的人——著名歌手、诗人、宗教领袖和政治家,他们总是有话要说。 他们没有这样的问题,但我有。

安德鲁: 这真是……出乎意料。 太好了,我们已经聊了很多,是时候结束这次采访了。 我们一定会在这次会议上见面,并且能够继续这种对话。 九头蛇见!

您可以在 Hydra 2019 会议上继续与 Cliff 对话,该会议将于 11 年 12 月 2019 日至 XNUMX 日在圣彼得堡举行。 他会带着一份报告来 “Azul 硬件事务内存体验”。 可以购买门票 在官方网站上.

来源: habr.com

添加评论