整洁的代码,糟糕的性能¶
标题:Clean Code, Horrible Performance
日期:2023/04/03
作者:Casey Muratori
链接:https://www.youtube.com/watch?v=y8N5aP4EGBI
注意:此为 AI 翻译生成 的中文转录稿,详细说明请参阅仓库中的 README 文件。
备注一:这是 easyperf 频道的闲聊讨论,这个频道也很久没更新了。
备注二:Casey 写了一篇同标题文章,谷歌可以搜到。
Marko / 主持人 (Dennis): Marko,我们要不要直接开始?简单介绍一下这个话题的背景。我知道在座的很多人和我们一样都是开发者,所以他们可能非常渴望开始这个讨论。你知道,甚至我还记得当我刚入行时,我读过那些书,我是读着那些书长大的。好吧,也许随着我职业生涯的某种发展,我发现有些事情绝对不像书里写的那么简单。但是,是的,让我们开始吧。Marko?
Marko: 当然。我本来想让 Casey 来做概述,但我也可以试着讲讲。在那段视频中,主要的论点是:很多架构书籍中常见的建议并不一定有助于编写高性能的代码。Casey 举了几个例子,比如将经典的类层级结构(Class Hierarchy)转换为使用 switch 语句,以及其他一些优化措施。这些优化让代码运行速度——我不记得确切的数字了——但可以说是快了几个数量级。
显然,互联网上的很多人对此持不同意见,并争辩说,当初开发这些方法的理由基本上是为了提高开发人员的生产力,而不是为了优化机器周期。我想,这就是很多人产生分歧的地方,或者说大家对于什么更重要有不同的看法。我不确定这里能不能发链接,但 Casey 和 Bob Martin(也就是 Uncle Bob,他创造了“Clean Code”这个词并写了那本书)之间有一场很有趣的讨论,那是一场非常友好且漫长的讨论。但是是的,看起来……
主持人 (Dennis): Marko,抱歉,你能简单地概述一下吗?比如 Bob Martin 的主要防御点是什么?而 Casey 的攻击点——如果我们能称之为攻击或防御的话——又是什么?
Marko: 好的。我认为最终归结为……他们当时在讨论……我看看能不能找到确切的措辞。
主持人 (Dennis): 术语的问题。Casey 试图表达的观点是,“Clean Code,Horrible Performance(整洁代码,糟糕的性能)”,对吧?这基本上就是他试图……噢,Casey 终于加入进来了。那就让他来做概述吧。
Marko: 这就是那本书吗?是的,这正是我们正在谈论的那本书,《Clean Code》。Casey,你可以申请发言权,然后你可以先给我们概述一下你的视频的主要……主要观点。
Casey Muratori: 我刚才只听到你们说的一点点内容,因为我必须在手机上安装 Twitter 才能进入这个 Space。我先简单吐槽一下,因为我觉得这很好地概括了我经常谈论但不知为何总是被反驳的内容,这真的让我感到费解。
为了参与这个 Space,我本来坐在台式电脑前,用我用来做播客、录视频、做 Twitch 直播的麦克风。我可以用这套设备使用所有其他服务。但在 Twitter 上显然不行,因为不知什么原因,你不能在桌面版 Twitter 的 Space 里发言。所以我不得不去手机上安装它。我在手机上安装了应用并授权了,但这个应用甚至没有一个列表来显示我已经关注的 Space(哪怕我已经设置了提醒)。相反,你只能拉出一个其他 Space 的列表。所以我必须要把你们给的链接,因为 Twitter 应用里没地方输入这个链接,我得去网络浏览器输入链接,然后点击“在应用中打开”,跳回 Twitter 最终找到这个 Space。
我之所以提起这件荒谬的事,是因为人们总是告诉我,我们之所以需要所有这些导致性能变得非常慢的东西,是因为它应该让程序员变得非常高效,让产品更好、没有 Bug 或者拥有更多功能。但我今天使用的所有软件都 Bug 更多、功能更匮乏,而且用起来像刚才那个经历一样慢得要命。那么,这些所谓的“好处”到底体现在哪里?
当我们谈论性能并遭到反驳时,怎么可能解释你们今天仍在使用的应用程序,连像“在桌面上使用麦克风”或者“手动输入网址因为没有列表”这种基本功能都做不到?现在的 App 怎么还是这么烂?
所以,作为这次谈话的开场白,我想说这种事情在目前的软件行业是普遍存在的。因此,当我在谈论我们可以做些适度的改进来提高性能时,那些不断对我那套说辞的人,我想挑战你们:你们声称我们得到的那些好处在哪里?因为我今天使用的软件不仅慢,而且功能贫瘠。它甚至做不了基本的事情。我再指出一点。我不能在台式机上用麦克风,但在手机上可以。我总是被告知的另一个好处是“跨平台”。为什么这在网络浏览器里不行?“哦,因为我们只写一次代码。” 不,你们并没有。它在两个平台上的工作方式并不一样。所以这根本不是答案。
我觉得在谈论性能时,存在很多虚伪的争论。人们做出的很多声明显然是错误的。如果你使用软件哪怕只有15分钟,你就知道他们说的所有话都只是借口。因为事实胜于雄辩,而这个事实(布丁)的味道令人作呕。
吐槽完毕。现在我很乐意回答你们的其他问题。
主持人 (Dennis): 不,Casey,你刚才说的内容很多。我们要不要一步步来,提炼一下你所说的?因为,当你责怪现代软件功能匮乏、Bug 丛生时……好吧,我们不能把这怪在 Clean Code(整洁代码)头上,对吧?我们可以吗?
Casey: 我们可以。是的,绝对可以。
主持人 (Dennis): 抱歉,我们可以还是不可以?
Casey: 我们可以。之所以可以,我很乐意详细说明。在以前,编程——大概是 20 世纪 80 年代——还没有真正发生太大的变化。到了 90 年代,情况变了。以前,编程是关于如何让计算机做某事。所以当你谈论编程时,你通常讨论的是“我如何让计算机执行这个任务?”焦点在于,例如,我需要计算机访问麦克风。所以我现在思考的是“我如何让计算机访问麦克风?”
不幸的是,在那段时间兴起的许多软件运动——我想我明白它们为什么兴起,我们可以稍后再谈——并不是关于“如何让计算机做某事”,而是关于“什么是好程序”的抽象概念。Clean Code 正是这样一种方法论。在这些方法论中,很少有关于实际完成你设定的任务的内容。如果你制作了这些类(classes),做了所有额外的工作来隔离事物、遵守 SOLID 原则等等,它们并不会去分析这样做之后麦克风实际能工作的概率是多少。
从来没有人真正停下来分析过,这些东西实际上是提高了单位时间内的功能实现率,还是仅仅产生了更多的代码?根据我做过的所有分析——我实际上以两种方式都发布过软件——结果与他们声称的完全相反。当你关注的不是让代码工作,而是把全部焦点放在“什么是 Clean Code”的抽象概念上时——无论你是使用所谓的“Clean Code”方法论,还是你只是有一种未经衡量的抽象的“整洁”概念——最终结果是,程序员的大量思维空间和实际日常工作都被那些无法提高发布给客户的软件质量(特性和可靠性)的活动所占据。
这种例子不胜枚举。此时此刻在这个 Space 里听讲的人,有多少人能感同身受:你们不得不在代码审查(Code Review)中花费一个小时讨论一些根本不重要的事情?比如它根本不影响计算机最终的执行结果。那只是做代码审查的人认为对“Clean Code”很重要的语法问题。比如私有部分(private section)到底是什么样,或者那个东西应该是一个虚函数还是一个 if 语句?
当我们从一开始就教导程序员——从他们的计算机科学教育开始,然后通过博客文章和 Twitter 帖子等强化这一点——即存在一个“Clean Code”的抽象概念,它关联着所有这些奇怪的、从未被证明有效的事情,比如创建大量的类、把所有东西都分到各自的文件中、让所有函数都极小、函数不接受参数枚举、必须确保一切遵守里氏替换原则(Liskov substitution principles)等等。然后我们堆砌拥有成千上万特性的语言(比如现代 C++)来做这些事。这种巨大的认知负荷绝对减少了能够参与到“让计算机实际工作”这一任务中的人数。
还有一件事,如果你看看我们建立的流程,你会看到开发人员整天都在 GitHub Issue 上讨论/争论他们要对代码做什么。你会看到开发人员发布如此多的 GitHub Issue,反复争论如何做某事,以至于他们白天根本不可能做任何工作。这根本不可能发生,对吧?
所以我们肯定创造了一种环境,我们在其中重视所有其他被放置在那里的东西,因为我们声称它们能产生更好的软件。但我看到的所有证据都表明,它们产生了极其糟糕的软件,而没有人愿意承认这一点。这不仅仅是关于性能。性能只是这种我们养成并毫无减缓迹象的可怕习惯的受害者之一。
主持人 (Dennis): 你刚才说到在会议上与开发人员花费无数小时,而谈话内容与代码应该做什么毫无关系。我也曾经在一次会议上花了超过一个小时与开发人员争论“依赖注入(dependency injection)”。
Casey: 是的,是的,没错。
Marko: 这其实是一个很好的讨论点。这是一个症状吗?与其说是软件开发作为一种实践的问题,不如说是公司结构的问题?我的意思是,公司的结构及其分成的团队,是否随着时间的推移反映到了代码是如何开发的,这未必是人们想要开发代码的方式。因为你有所有这些不同的团队。尤其是现在的公司有大量的软件工程师在开发应用程序的不同部分,甚至不同的应用程序。他们必须亲自交流,也必须通过代码交流。但是为了简化流程,我们创造了这些必须信任的黑盒,创造了这些抽象和接口,据称这能带来更好的软件、更好的代码和更高的生产力。但实际上,情况并非如此。你认为这是一个公平的评估吗?
Casey: 不,我不这么认为。我不认为这是一个公平评估的原因是……显然,我是康威定律(Conway’s Law)的忠实信徒。我认为这实际上可能是一条定律。这是我们在架构中见过的最接近定律的东西。对于不知道康威定律的人,我简单提一下。康威定律说,组织总是发布其自身结构的副本。这有点简化了。我做过一个关于康威定律的完整视频,如果大家想了解更多信息,我可以指路。它比那更丰富,是一条比这更好的定律。但就本次谈话而言,这就是它的含义。
它之所以这么说,当然是因为就像你在问题中提到的,公司会有结构。而这种结构将会有基于公司结构而非产品结构划定的沟通界线。因为跨越这些界线进行创新或优化是非常困难的,所以该公司发布的所有产品至少都会带有这种结构。
针对你所说的,以及我绝对同意的部分,那就是公司的结构确实会影响他们发布的软件。所以如果有一个音频团队和一个视频团队,那么你基本上可以打赌我们发布的两个组件将是音频和视频。因为这就是我们组织公司的方式。我们不会发布一个合并的视听模块,因为团队是分开开发的,中间会有一个 API 或类似的东西。这就是它的运作方式。你可以看到这在各处上演。这几乎是普遍真实的。你可以看看微软的组织结构图,不出所料,同样的组织结构图会体现在 Windows 的设计中。
然而,这是我们可以做的最小代码划分。也就是说,有一个 DirectSound 团队(现在在 Windows 中可能是 XAudio 团队或其他什么),这只是意味着 XAudio 会有一个面向外部世界的 API。它并没有说明 XAudio 内部必须如何划分。
因此,即使我们下沉到单个程序员的行为,如今我们会看到单个程序员——显然没有任何康威定律强加在他们身上,因为他们在写他们自己的代码,不需要跨越慢速通道与自己交流,他们的大脑内部有快速通道——他们仍然经常因为被教导这样做(不一定是他们自己想出的主意),而将他们制作的东西微切成数百个(有时是不必要的)类、组件、容器以及这个那个东西。因为这是他们被告知要做的,以使代码“整洁”或“好”,或者为了通过代码审查等等。
所以我不认为这仅仅是康威定律在起作用。我认为你会触到一个极限,在那里你会撞上康威定律,无法再进一步优化软件开发。你可能无法进一步优化公司开发,但我不认为那是我们现在代码质量的瓶颈。那个极限离我们现在的代码质量还很远。康威定律永远都在生效,因为它是定律。为什么他们在 1973 年没有这个问题?答案是因为 1973 年没人叫他们做这种事。所以当他们坐下来编程,专注于让计算机运行程序时,他们并没有想:“哦,还有这 97 件其他的事情我可以做,我虽然无法衡量其功效,但我会将它们的优先级置于让 Twitter Space 的麦克风真正工作之上。”
主持人 (Dennis): 各位,我们是不是也许应该回到性能上来?既然我想我们……对。所以专注于主题,对吧?“Clean Code,糟糕的性能”。Casey,如果我们回到比如 1997 年,不知道,也就是那些 Clean Code 原则确立的时候。它们一开始就是错的吗?那些 Clean Code 原则从哪里开始出错的?
Casey: 是的,它们绝对是错的。我会这样描述它。基本上 C++ 和 Java 的原始设计,我不会把它追溯到 1997 年。而且我也不会说是 Clean Code 原则开启了这种错误。
我会说,这种特定的面向对象编程(OOP)实现背后的想法……顺便指出一点,当我们谈论 Clean Code 时,必须非常具体,因为有些人会说:“不不,我的代码性能很高,而且很整洁(Clean)。” 每个人在不同地方对这个词都有自己的定义。所以当我们谈论 Clean Code 时,必须小心说明我们指的是什么,因为可能有人认为的 Clean Code 是我也认为整洁的代码(如果让我定义这个术语的话)。
所以我们在谈论什么必须很谨慎,但让我把时钟再往前拨一点。C++ 或“带类的 C(C with Classes)”背后的想法,最初的版本全都是错的。我的理解是它们来自 Simula 66 或类似的语言,我对那门语言没有经验。但如果你回顾历史,那里有一个传承。并不是 Bjarne Stroustrup 坐下来说:“嘿,我不知道,我有这个主意,咱们试试吧。”
但是 C++ 和随后的 Java——当然 Java 背后有巨大的营销活动并被广泛采用——我认为与这些事情的关系比 Clean Code 更大。Clean Code 实际上包含了一些我同意的东西。比如我认为合理地命名标识符非常有意义。我不觉得这会伤害软件开发,尤其是现在我们有自动补全功能。很难争辩说把所有变量名都写成一个字母会有什么改进。所以并不是说 Clean Code 作为一个整体完全是坏的。
然而,目前软件设计中主要的糟糕之处也包含在 Clean Code 中。但如果我们想更专门地关注它们,我们会回到 Java 和 C++ 的设计。因为我认为那才是真正的开端,因为那些语言变得非常流行。两者都被大力营销,C++ 是非正式的,Java 是正式的且预算巨大。我认为这真的把行业推向了那个方向。
先打好基础:那些实现并没有解决问题。认为带有虚函数(virtual functions)和继承层级(inheritance hierarchies)的类是设计软件的好方法,依我之见,是完全错误的。它在当时是错的,今天依然是错的。这就不应该被这样做。在几乎每种情况下,几乎总是有其他你想做的事情。
请记住,我说的是一种特定的实现。显然,函数指针(function pointers)的想法并不坏。我用它,我认为它没问题。但是,通常情况下你创建这些东西,而这些东西里面包含了你看不见的东西。记住 Java 和 C++,这种机制对你是某种程度不可见的。尤其是 C++。它在某处放入了一个 Vtable(虚函数表)指针。你不知道它指向哪里,它指向一个你未必能控制格式的 Vtable,且位于一个你无法控制的地方。这就是你的程序应该工作的方式。然后我们有了这种继承,允许我们通过隐含的操作来“创建”那个 Vtable,这实际上与直接创建 Vtable 无关。这不是对 Vtable 的控制,而是通过创建这个层级结构并由此生成这个 Vtable 的间接活动。这就是 C++ 的设计方法论,并在很大程度上被推向了 Java。
这只是一个坏主意。我认为它一直都是坏的。我认为人们认为它可能是一个好主意,或者它流行的原因是……我该怎么说呢?它之所以流行并被认为解决了问题,是因为我确实认为在那个时候,我们还没有真正思考或编纂出那些不使用这种代码的良好架构。意思是普通人从未思考过我们要如何称呼今天比如“数据导向设计(Data-Oriented Design)”这样的概念。他们没有想过,你实际上可以在没有那些东西的情况下,制作出易于扩展、易于维护且易于阅读的代码。
所以有人过来说:“你做这些继承层级,你的代码就更可维护。” 大多数人从未衡量过那个事实。而他们的出发点是没有关于如何创建一个真正易于维护的程序的任何指导。所以如果你的选择——尤其是有意地,比如一家公司要做这个决定——是在一个什么都不说的人(因为没人推销 C 语言或其他替代方案,也没有告诉你它更可维护并提供不同选项供你评估)和只给你这个奇怪的继承层级东西但告诉你它更可维护、代码更好的人之间做选择,你可以理解为什么它被采用了,因为“某种方法论”通常比“没有方法论”要好。我也认为没有任何方法论是非常危险的。
主持人 (Dennis): 抱歉,让我打断你一下。举个简单的例子。假设你看到一段代码,一个巨大的 if-else 语句。你是更愿意看到一个虚函数调用,把所有东西都折叠在这个虚函数实现的底层细节里吗?我觉得那不是个坏主意吧?那是个好主意。你会想要隐藏那种复杂性,不要让阅读代码的人被特定虚函数实现的所有细枝末节淹没,对吧?
Casey: 你刚才说的正是我刚才说发生的事情的一个例子。你现在的说法是:只有两个选择,要么是一段包含 8000 个 if 语句的代码,要么是一个虚函数。但事实并非如此。
我可以把那段代码重构(factor)成更多的函数。如果你觉得这些 if 语句是多余的,或者不应该在一行内阅读,你可以把它们提取到只是一个普通的函数中,对吧?因为认为某种程度上把它们拉进虚函数……只要这样想:两种情况下的代码片段有多少?数量是一样的。在 if 语句的情况下,你有相同数量的基本块(basic blocks),在虚函数的情况下也是如此。只是在 if 语句的情况下,它们是有顺序的,我按顺序阅读它们。在虚函数的情况下,它们被放入了我必须去寻找的类中的几个单独的文件里,当我想知道它们做什么时。
所以我只是说,如果你发现那个东西不可读,或者你认为那些 if 语句被重复了——这是另一个问题——那不是你需要虚函数来解决的问题。只需将它们放入你调用的静态函数或普通函数中。只是不要让它们变成“虚(virtual)”的,因为那样会有这种额外的继承层级,这是一堆没有实际理由的打字工作。
主持人 (Dennis): 是的,这对我来说是有道理的。但你看,好吧,我有点从试图寻找中间立场的人的角度来说,因为我不一定认为所有那些 Clean Code 的东西都是坏的。但正如你展示的和许多其他人展示的那样,Clean Code 并不总是导致良好的性能。但是我们能不能在这里找到一个中间立场?我的意思是,只让那些不一定需要高性能的代码部分变得“整洁”。我的意思是……
Casey: 不行,因为它对功能(features)来说也更糟。我想这就是我开始这段吐槽时试图说的。我认为没有人去看看他们声称是 Clean Code 的东西是否真的更容易使用。去看看 LLVM 吧。那是继承层级的典范(poster child)。你永远不知道发生了什么。他们完全做了你刚才说的事。与其拥有你可以按行遵循的 if 语句,他们把一切都变成了虚函数,一切都是类。在那样的代码库里做任何事情都是不可能的。你必须在调试器里单步调试一个小时才能找出任何东西是如何工作的。而且,顺便说一下,它慢得离谱,这当然正是你所期望的。
所以你看这些东西。我的意思是:不,这不是中间立场的问题。如果你希望你的代码更容易维护,并且你发现自己在写所有这些 if 语句而且你不喜欢那样,除了虚函数和继承层级之外,还有其他选择。
主持人 (Dennis): 抱歉,回到 LLVM。好吧,我在 LLVM 代码库上花了几年时间。所以我……是的。我的意思是,LLVM 之所以慢,只是因为有一堆指针,你总是遇到缓存未命中(Cache Misses)。有一堆虚函数调用的间接层。好吧,也许那也是个问题。我没有测量过那个,但是,好吧,我只是想弄清楚你会怎么解决它?如果你把所有这些“整洁(cleanness)”从 LLVM 代码库中扔掉,它会变快吗?
Casey: 当然会变快,但我认为它也会更容易处理。我想说的是两件事。让我们都解决一下。
第一,虚函数调用只是问题的一部分。如果你像在我的视频里那样,实际上它必须进行虚函数调用的事实并不是它慢的真正原因。它慢的原因是因为无法跨虚函数调用进行优化。这才是慢的来源。每次你进行虚函数调用,你就划了一条编译器无法跨越优化的硬线。所以发生的情况是它不能合并所有的代码。
举个非常简单的例子。假设我有一个类,它有虚函数调用。一个叫 getSize,另一个叫 getCount。我所做的只是把它们乘在一起。好吧,做这两个虚函数调用的成本,也许很坏,可能确实如此,但可能没那么坏。实际的成本是它不知道它实际上只需要检查一次这个东西的类型(因为它要对它做两次虚调用)。但现在它不能把这两件事合并在一起了。
而 switch 语句,它可以合并。如果你有两个做同样事情的 switch 语句,一个接一个,或者两个检查相同条件的 if 语句,一个接一个,编译器会把它们合并在一起。现在它可以跨越这两件事进行优化。虚函数真正的问题在于,如果你有一个包含成吨成吨虚函数的代码库,这些东西都不会被合并。
主持人 (Dennis): 是的。首先,性能方面,我们做过那个。无论如何。是的。我的意思是,在这里我同意你的观点。虚函数缺乏内联(inlining)是一个巨大的问题。我这里绝对同意。好的。那么你问题的第二部分。
Casey: 我对你问题的回答的第二部分,我认为如果我理解正确的话,是说:当你这样做时更容易处理。让我们谈谈虚函数实际上做了什么。虚函数所能做的就是你写那个虚函数要做的那个操作。代码的使用者将使用一个不透明的类,必须使用这些虚函数,这意味着你推动了一个决策点。任何时候你设计任何一段代码,如果你是为一个特定的用例设计一个实际的 API,都有一个连续的决策点。
它是连续的,方式如下:我可以向将要使用这个东西的人暴露 100% 的细节,我可以暴露 0% 的细节,或者我可以做介于两者之间的事情,对吧?总是有那个连续统一体,那是你的选择。一般而言的 Clean Code,例如 LLVM 代码库的设计,是关于将那个指针推得很远,推向 100%(隐藏细节)那边。这就是为什么一切都是它自己的类,一切都是虚的,继承层级如此疯狂,以至于我只见过它的一小部分。意思是,如果你真的试图把整个东西画成图表,我都不想知道它会是什么样子。但即使只是 Operator 类,里面就有那么多东西,有那么多行,以至于在图表中阅读它们都要花几分钟,更不用说在代码中了。
现在,当你只用常规的可区分联合(discriminated unions)编写代码,比如标记联合(tagged union),就像来自范畴论或喜欢用联合编程的人会做的那样。当你做类似的事情时,你有很多选择,因为你向外层暴露了类型,你说你可以用这个做更复杂的事情。
例如,当你用这些选项编程时,它赋予了使用你提供的任何东西的人更多的能力去做事物的组合。你可以很容易地说,比如,“哦,如果这个东西有一个 X 而那个东西有一个 Y,那么我就做这个。” 读起来很容易。编译器也很容易理解发生了什么,因为没有虚函数。
而用继承版本模仿那种东西的唯一方法是让那些东西成为虚函数。比如我调用“你有 X 吗?”“你有 Y 吗?”“你支持 Z 吗?” 这不仅对人们来说通常更令人困惑(因为有更多的函数调用,那是做什么的?而不是仅仅看 type == this),而且这也意味着现在编译器无法优化任何这些东西。
所以我看不出有什么好处。我看不出他们通过引入所有这种人为的复杂性得到了什么,而实际上那里需要的只是说明你实际上想在代码中做什么。如果代码想知道这个东西是那个还是另一个东西是那个,只需放入一个字段说明那些东西是什么,让每个人都去检查它。我的意思是,无论如何这都在发生。我是说,LLVM 里有成吨的东西,我们有过这样的例子,他们说:“好吧,为什么我们不直接改这个?” 他们说:“我们不能。太难了。” 因为我们把它烘焙到了设计中。所以,你知道……
Marko: 抱歉,Dennis,在你继续之前。我认为有一点,我在这里扮演一下魔鬼代言人。我认为人们使用类,特别是 C++ 推动你使用类的原因之一是,他们试图实现(至少在某种程度上)类型安全(type safety)。意思是,与其将类型嵌入到基本上是 void* 或联合(union)中,不如有一个定义非常明确的类。同样,它可以被编译时检查。你可能有更好的保证它不会行为不端。我知道事实并非如此,但这有时是在谈论继承和多态等问题时提出的论点之一。比起 switch 和 if 语句的方法,你可能有更好的类型安全。
Casey: 我的意思是,这真的取决于你在谈论哪种语言。但是是的,在 C++ 中,我知道他们添加了 standard variant,我相信它是为了解决这个问题。但我想说的是,你要做的任何事情都有权衡。我的问题是,假设由于某种原因你的语言没有办法帮助你做到这一点。同样,只需将其包装在一个函数中。只需将其包装在一个普通函数中。因为这就是虚函数无论如何都在为你做的事情。如果你真正担心的是有人会不正确地访问这个联合的成员,把它包装在一个普通函数中。搞定。问题解决了。对吧?
主持人 (Dennis): Casey,我想说关于 LLVM 的是……对不起,但在我们继续之前,这是否回答了被问到的问题?是的,是的。好的。
主持人 (Dennis): 不,所以 Casey,你看,我想说的是有些问题是工作负载固有的。比如你不能……好吧,即使你摆脱了多态性,把所有东西都放进静态函数里。我的意思是,内存间接寻址(memory indirections)不会消失。因为,这只是编译的本质,对吧?当你编译函数 A 时,你需要知道关于函数 B 的一些信息,而你并没有提前拥有那个信息。
Casey: 你必须记住,如果你像 LLVM 那样用这些继承层级设计代码,事后就没有办法进来,然后说:“好吧,让我们看看典型编译上的实际工作负载是什么。让我们继续实际找出我们可以如何优化这些东西。”
我的意思是,看看像 JAI(Jonathan Blow 开发的语言)这样的东西的工作方式,它快得多,快得多。现在,我不知道是不是因为 Clang 中必须发生某些事情,而这在另一种语言中没有发生。所以我不能告诉你那就是真的。但是,我敢肯定,如果我真的去为一个 C++ 编译器写一些合理的东西,它会比 LLVM 快得多。我的意思是,这有点荒谬。它并没有做那么多工作。一百万行代码并不是那么多行代码。
主持人 (Dennis): 好吧,我不知道该对你说什么。你得去试着重新实现一个更好的。我的意思是,好吧,让我告诉你 LLVM 的瓶颈在哪里,好吗?首先它是一个巨大的代码库。所以首先,你实际上访问了许多代码页(code pages)。所以你有大量的 iTLB(指令转换后备缓冲区)未命中和 iCache(指令缓存)压力,因为代码到处乱跳,对吧?
Casey: 是的,但为什么代码库是巨大的?为什么会这样?
主持人 (Dennis): 嗯,这是因为你必须处理编译的所有棘手细节。
Casey: 但是,想一想……你玩过像《使命召唤》之类的游戏吗?
主持人 (Dennis): 也许是很久以前,但是是的。
Casey: 你玩过像《堡垒之夜》之类的吗?
主持人 (Dennis): 《堡垒之夜》,没有。我是个老派的人。
Casey: 好的。好吧,如果你看看现代视频游戏,你有潜在的数十万个实体,你在一帧里真的要绘制 2700 万个三角形,对吧?还有所有这些着色器图(shader graphs)和所有其他东西。我的意思是,想想那里发生了什么,对吧?怎么可能……怎么可能是这样一种情况:LLVM 代码库如此庞大,以至于它必须那样写?相比之下,有些东西做了所有这些事情,而且如果你看它,它在视觉上做了更复杂得多的事情,同时还有服务器后端、用户输入和所有其他东西。语音聊天。然而,有些东西只是输入一个文本文件并输出一个二进制文件。这种论点是说那只是一个无法解决的问题。就像,“不,为此代码必须是 100 兆字节。我们没办法把它减下来。”
主持人 (Dennis): 我的意思是,这有多大可能性?你知道有 C 编译器显然很微小。现在的论点是,好吧,如果我们想添加 LLVM 的特性,如果不把代码大小乘以接近 10 万倍就没办法做到。这有多大可能性?你觉得这真的有多大可能性?如果我们关注的仅仅是,“我们将做简单的代码来执行我们试图做的操作,我们不会引入其他东西。使用数百个类或数百个模板。我们不会有所有这些奇怪的继承层级。我们不会考虑任何那些。我们将只看我们如何生成代码并从那里反向工作?我们如何生成机器码,我们将从那里反向工作。” 你真的认为……我不不知道可执行文件有多少兆字节。30、40、50、100?LLVM?就机器码而言,整个东西总共有多少兆字节?
主持人 (Dennis): LLVM 的文本段(Text segment,代码段)大概是,我想 60 到 70 兆字节。
Casey: 70 兆字节。你认为没办法在少于 70 兆字节的情况下做到。而且,我们真的不应该说 70 兆字节,因为那不可能全部都被触及,可能是这样。但是,无论我们实际使用的量是多少。
主持人 (Dennis): 我的意思是,我不是说没有缓解措施。你可以把所有热函数(hot functions)分组。如果你有分析数据(profiling data),当然,对吧?所以如果你有分析数据,你可以做 PGO(基于分析的优化),最终摆脱这个问题。但是,有些事情还是编译过程固有的,再次强调。比如如果你看分支,你有 if-else,我的意思是那没那么容易。从 CPU 的角度来看,那些分支不是超级可预测的。我的意思是,那只是随机性,如果你愿意的话,是工作负载的熵。那不……那就在那里。我的意思是,即使你……甚至没有办法让编译器在这里帮忙。哦对不起,是 CPU 在这里帮忙。
Casey: 你确定吗?因为通常有很多方法我们可以做到这一点。例如,当我们做 SIMD(单指令多数据)编程时,我们一直都在处理这个问题。
主持人 (Dennis): 是的,你可以做预测(predication)。我同意,比如 CMOV(条件移动)和把分支反转为 CMOV。但是,在某些情况下是的,你可以做到。但是,人们通常对此非常小心,对吧?因为,再次强调,当你把分支转换为预测时,你实际上把代码流依赖转换成了数据流依赖,对吧?所以你有点……你这样做的时候真的应该三思。
Casey: 呃,这取决于你怎么做。但是,比如做 SIMD,那你几乎总是想这样做,因为你只会做一个“非分支(not branch)”,如果一切都走相同的路径。但是,当然,如果你做 CMOV,那是不同的。
主持人 (Dennis): LLVM 的代码中有零个版本(使用了这种优化)。
Casey: 是的,我知道。但那是因为他们是怎么写的。
Marko: 各位,各位,秩序问题。我们要让每个人都能提问。我们陷入了讨论。对不起各位。如果在电话会议上的人想提问,请申请麦克风,有人会给你们麦克风。我们有一个来自 Matt 的问题。他把问题发在聊天里了。所以我读给他听。是给你的,Casey。他说:“LLVM 有大量的用户,用例非常不同,这在某种程度上驱动了由此产生的必要复杂性。一个更接近的比较可能是虚幻引擎(Unreal Engine),它也以复杂性和大量使用运行时分发而闻名,无论好坏。所以我认为将像 LLVM 这样的通用编译器框架与像 UE 这样的通用游戏引擎进行比较,比与像《使命召唤》这样的单一游戏进行比较更公平。”
Casey: 我猜是这样,虽然这有点奇怪,因为虚幻引擎并不是因为可许可性(licenseability)才那样制作的。它实际上那样工作只是因为那就是它的工作方式。对于《使命召唤》也是如此。比如他们的引擎也将是同样类型的设计,对吧?
但在任何一种情况下,我试图说的是,如果你看看那些引擎,它们能够执行这些非常非常复杂的工作负载。通常你可以看到的是,如果你去看看它们实际上是如何保持代码库隔离的,你就不会在代码库中真正重要的部分看到所有这些疯狂的层级结构和类似的东西。
比如在他们必须做事情的地方,比如 Nanite(虚幻5的几何技术)或类似的东西,他们会确保那些部分没有这类动态多态的东西。他们想出了构建代码的方法,使这种情况不会发生。而在 LLVM 中这是根本不可能的,因为此时没有人做那个工作。没人让那些对性能来说很难的代码部分保持那种“整洁”。他们保持了另一种“整洁”(带引号的),那是关于除了交付正确的最终产品(即快速的产品)之外的其他事情。
现在,我不是试图声称虚幻引擎的架构是好的。我相信虚幻引擎架构中有大量的浪费。我的意思是你看它你会说,噢天哪,这里面有大量的浪费。那仍然是一个相当糟糕的案例。别误会我的意思。只是它在做更多得多的工作。而且,我不确定还能怎么说。当你看像 LLVM 这样的东西时,你会说,“好吧,我们不使用 SIMD,但为什么不呢?” 你几乎可以在任何地方使用向量完整(vector complete)的东西。
但为了使用它,你必须首先从某种批处理(batching)的角度思考你的问题。你必须把虚函数调用和类似的东西排除在你实际上要做那种批处理的代码部分之外。当你在你要尝试快速处理事物的地方时,你真的不能有所有这些你要被期望分发(dispatch)到你不能总是看到的每一段代码中的事情。
如果你看看像虚幻引擎这样的人是如何处理这个问题的,如果你想支持(例如提问者问的)其他人需要用它做东西,你会知道他们做了什么。像虚幻引擎发布了各种各样的东西,旨在防止你在用户添加的东西上遇到这个问题。他们做像 Blueprint(蓝图)或类似的东西,然后他们可以在底层优化。他们在 Nanite 中做的事情,当你提供着色器时,他们做一堆工作来在他们开始考虑着色器之前弄清楚将会发生的一切,以便他们可以同时做在着色器中任何给定路径上发生的所有事情。这是他们加速处理必须执行的实际多个代码路径的主要方式。而在 LLVM 中,你甚至无法想象你可以这样做,因为架构就是这样,你甚至没有那个知识。你仅仅必须在这个盲目类型的土地上操作,而你没办法现在朝着那个方向重组架构。
Marko: 你有什么问题吗?有人想申请麦克风吗?是的,我有一个请求。我要批准。哦不,抱歉。那是 Ivica。在那之前还有另一个人。我想是 Ginger Bill。你想重新申请吗?不。好的,Ivica,他加入了。欢迎。Ivica,你在静音状态。
Ivica: 是的。嗨,你们的谈话真的很棒。听起来真的很享受。我在关注 Marko 提出的论点,但我同意 Dennis,同意你,仅仅是虚幻引擎所做的工作负载类型与编译器通常做的事情相比。真的是不同种类的,我们这样说吧。你有两种类型的工作负载。一种是计算处理,一种是查找。所以一个是数据处理,一个是数据查找。游戏属于数据处理,编译器属于数据查找。而这些数据查找对 SIMD 并不友好。这真的很难实现。
请记住,LLVM 是一个非常复杂的软件,仅仅就它所做的事情而言。在它上面添加 SIMD,可能会变得复杂得多得多。
主持人 (Dennis): 是的,因为并行性不在代码中。它在数据中,对吧?
Ivica: 是的。
主持人 (Dennis): 所以并行性是你如何实际访问内存,以及那类东西。这不是关于,我的意思是,如果你在访问数据,比如……我该怎么说呢?如果不以时间友好(temporal friendly)的方式,那么你甚至不能在这里使用 SIMD,对吧?所以你必须有点像顺序访问。如果你做指针追踪(pointer chasing),对吧?
Ivica: 是的。你不能使用 SIMD。你可以使用 SIMD,但是以不同的方式,你不能像经典的指令依赖那样……你需要考虑大、大的东西,比如处理数据。
主持人 (Dennis): 等等。
Casey: 是的?我的意思是,只是为了明确,也许如果我们严格谈论 CPU,我同意 CPU 上 SIMD 中的指针追踪是困难的。但是,我的意思是,那是我们可以稍微讨论的一个单独话题——LLVM 是否真的需要它正在做的那么多指针追踪。但是,不,指针追踪是完全可以 SIMD 化的。那就是 GPU,对吧?GPU 就是一个巨大的用于追踪指针的机器。我的意思是,那是它的整个架构,对吧?它是一个 SIMD 指针追踪机器。
Ivica: 我确实这么认为。所以,用 std::transform 的术语来思考。图形引擎做的事情,处理的类型,你可以很容易地用 C++ 中的 std::transform 术语来表达它们。而编译器做的事情,这种树查找(tree lookup),这种树访问,你不能轻易地用 std::transform 的方式表达它们。
Casey: 不不不不。
Ivica: 所以 std::transform 的本质是它里面有无限的并行性,那种工作负载。它们可以很容易地并行化。而编译器做的事情,它们只有节点和链接从一个节点到另一个节点。所有节点都是不同类型的。那种方法,这种方法,你不能……代码本身具有非常低水平的可用指令级并行性(instruction level parallelism)。如果你需要添加它,那比这些只处理向量和处理数组的代码要复杂得多。
Casey: 只是为了明确,对吧?有两件事。指针追踪不是你刚才说的那样。GPU 绝对是为了指针追踪而制造的。这就是为什么它们有巨大的队列,对吧?因为通常你一直在做的事情之一就是你有依赖纹理查找(dependent texture lookups)和类似的事情,这正是那个东西。它是一个指针追踪。
现在我们做各种各样的事情,比如在着色器内部遍历空间分区。那些是什么?那些是指针追踪。所以这就是 GPU 现在的工作方式。有大量的指针追踪。你做这些请求去获取横跨某种你编码在 GPU 内存中的结构的一组分散的东西。然后,它只是继续到另一个超线程(hyperthread),当它等待那些回来的时候。这就是为什么它们有那些通往内存单元的超深队列。因为那就是 GPU 的工作方式。
现在,我同意如果你不提前考虑它,并且你不是为了性能……顺便说一下,这偏离了 Clean Code,因为现在我们只是在讨论优化。但是如果你真的坐下来问,LLVM 中到底有多少实际上不是代码级可并行的?你的论点是,好吧,没有一个是,对吧?而我只知道这一点,因为我看着这个代码库,我看到所有这些不同的类,所有这些不同的虚函数,它们显然都在做不同的事情。我绝对确定那根本不是真的。
任何时候我解决过像这样的问题——我们在游戏中一直这样做,因为我们的实体和游戏玩法代码看起来就像这样。所以实际上,如果你想去优化某些东西,比如:“哦,游戏设计师说,好吧,如果这个东西着火了并且它有这个其他东西,那么我们需要做这一部分。然后,如果不是,我们必须去做这个其他事情。” 我们解决了看起来与 LLVM 完全相同的问题。游戏玩法代码与 LLVM 完全一样。
所以如果你想去优化那段代码,你会惊讶地发现,当你实际上可以看清它们时,有多少相似的代码路径最终可以折叠成相同的代码路径。与当你永远无法发现那是怎么回事的巨大蔓延的继承层级相比,当它们清楚发生了什么时。因为(后者)被如此隔离和人为分离,以至于实际上本该是相同代码路径的两个东西永远不会被合并。它们甚至永远不会被注意到。
Ivica: 也许当 AI 到来时。所以对我来说的问题,我不给 GPU 编程。我不了解很多这些事情。但给你的问题是,有没有任何类型的编译器可以实际上运行,实际上执行 GPU,以显著的方式从 GPU 中获利?小型编译器,实验性编译器,任何东西。
Casey: 我不知道我是否试过那个,但这听起来有点意思。我想可能有某个研究人员做过。我不知道。因为……当我阅读研究论文时,我总是对人们试图放在 GPU 上的东西感到震惊。所以人们试图把所有东西都在某个时候放上去的事实通常意味着你可以找到做了某事的人。但我不知道是否有人试过在 GPU 上制作编译器。
Ivica: 我问这个是因为……我的意思是这对开发者来说是一件重要的事情。因为它将节省的不是数千,而是数百万小时的开发工作、电力和其他一切。所以我们肯定会从拥有一个快速且可以利用加速器的编译中获利。现在,LLVM 现在的编写方式,那只是不可能的。所以你不能把它移到不同种类的……你可能需要……如果你想采取数据导向的方法,你需要从一开始就考虑到这一点。也许……例如,我看了一些……是关于数据导向设计和实体组件系统(ECS)的讲座。但它是关于网络浏览器中的动画的。但实际上,当涉及到采取那种方法并实现一个完全不熟悉的编译器时,从来没有人做过。你知道吗?所以问题是,这可能吗?也许。但从来没有人做过。因为你需要一个既是编译器专家,又是那个(数据导向/GPU)专家的人。这是一个未知的术语。
Casey: 此外,你必须……所以对于像我这样的人来说,这并不重要。因为转移的成本……你知道,我就像保持我的编译时间极低。所以我们目前的全部构建编译时间在这里降到了,比如 1 点几秒,对于所有……是什么?12 个构建目标。我们显然并行运行它们。
但从像我这样希望一切都非常快的人的角度来看,这真的没多大关系。因为把你的代码转移到 GPU 上的时间可能太长了。现在,也许如果你想象一个假设的场景,IDE 也在 GPU 上或类似的东西。
问题是在 GPU 上运行编译器,是的,它有更好的分散……更好的分散-收集(scatter-gather),因为有更深的超线程和深队列。但它不太适合编译器,因为它们过度优化的东西是浮点单元(floating-point units)。所以你真的不会用浮点单元进行编译。我的意思是,它真的不做浮点数学。
所以我的假设是,将编译移至 GPU 并不是很好,因为你正在做的大多数操作将是实际 GPU 没有那么多的二进制操作,对吧?我的意思是,看看像 Skylake 这样的东西。我不记得了,但我想说它有,什么,比如 5 个可以在其上做二进制操作的东西或类似的东西,对吧?比如 5 个执行端口。这是非常高的。而它只有两个执行端口可以做像 FMA(融合乘加)这样的事情,对吧?
所以 CPU 有更多的二进制处理能力。所以你去 GPU 获得的 ALU(算术逻辑单元)乘数并不会那么大。所以编译器并没有得到很多优势。是的,如果你想做一些我们假设存在的很酷的 SIMD 编译,它可以利用那些深队列来做分散-收集之类的事情。这会比在 CPU 上好,因为 CPU 上的收集(gather)……但你没有得到主要的优势。你没有得到为什么 GPU 在处理科学工作负载时看起来快得多的东西,因为它们只有那么多更多的 FMA,对吧?
Marko: 那么,将此转向更多关于未来的话题,我们看到更多的视频出现,各种会议,无论是 CPPCon 还是 Media C++。你会看到更多关于数据导向设计的演讲在未来出现。你正在推出课程,你正在与 Practical Engineer 或类似的机构合作制作这些每周视频。是这样吗,Casey?Practical Engineer 是你在做的吗?什么是 Practical Engineer?抱歉,我想我不熟悉这个。我忘了。你一直在哪个平台上每周发布这些视频?我忘了它的名字。
Casey: 哦,是 Computer Enhanced。
Marko: Dot com。谢谢。Computer Enhanced。所以你有你在 Computer Enhanced 上做的事情。你有所有这些过去一直在进行的数据导向设计演讲。我想说它们在过去三四年里一直在增加。你是否乐观地认为,在为性能编码而不是如此严格地遵守 Clean Code 宗教方面,未来会变得更好?
Casey: 我想我会说很难衡量,因为你不知道有多少人在听。一直有一群以性能为导向的人。我想在这里区分一下“性能导向(performance-oriented)”和“性能意识(performance-aware)”,我稍后会做。一直有一部分程序员是性能导向的。这些人就是像虚幻引擎这样的东西仍然可以运行得很快的原因。对吧?因为如果你看看像虚幻引擎这样的东西,其中很多是用类似于 LLVM 的方式编写的。对吧?性能核心之外的所有东西。对吧?
所以一直有这种分类:有一部分编码者了解性能并在他们的代码中关注这一点,还有一部分不关注。不关注的比例非常高。所以我不知道像我正在做的事情是否触及了任何新人,或者它们只是下一代那同一部分本来就会是性能导向的人的培训,只是也许他们会使用一些稍微不同的资源来达到那个目标。对吧?
所以我不知道是否该对未来持乐观态度。但我会说,这种心态并不是很好。即使在这个谈话中,对吧?所以我们在看像 LLVM 这样的东西并思考它实际上必须做的任务时的默认反应应该是:显然,这个东西应该快得多。对吧?对我来说,这似乎是一个自然的假设。因为看着代码,它显然没有任何为性能设置的方式。它是巨大的和蔓延的,这有很多我认为不太合理的理由。
所以如果你说,“嘿 Casey,你未来 10 年的工作将是做一个 LLVM 的替代品,它真的很快,或者快两倍,或者快 10 倍,或者任何你想选的数字。” 我不会说,“好吧,那是不可能的。” 但这听起来有点像这个谈话中发生的事情。就像,“不,LLVM,那就那么快了,伙计。就像,什么?我的意思是,真的吗?它是怎么回事?”
所以,你知道,我认为那种态度现在无处不在。这使得这种非常糟糕的进展(指目前的软件状况)。所以我想如果人们只是更多地想,“是的,性能真的很糟糕,我们可能应该开始改变我们做事的方式,以便代码库不会进入这些状态,并且我们要更频繁地检查我们的性能并思考这些事情,并实际分析如果我们遇到这些问题,比如,好吧,你认为这里会发生很多指针。或者也许我们需要退后一步,说,好吧,这是我们现在需要开始实际做的事情吗?”
因为这样想。人们看像游戏这样的东西,它们运行得非常快。他们说的,以及在这个谈话中所说的,是,“好吧,它们只是不同。” 不,它们不是。它们不不同。它们看起来不同的原因是人们花了大量的时间弄清楚如何做那些本来看起来就像 LLVM 的事情,但我们想出了技术让那不发生,对吧?像 Nanite 这样的东西是跨越多年的巨大努力,以弄清楚如何不让代码运行缓慢,这就是为什么它看起来不同。
所以如果不看 LLVM 说,“好吧,就是这样,指针追踪,抱歉,我们回家吧。” 如果相反我们是,“不,这可以是即时的,让我们弄清楚如何做。” 是的,这会花我们五六年的时间做关于我们如何措辞的新实验吗?我们做不同于抽象语法树的新事情吗?我们需要改变我们做比较的方式吗?我们需要对这些我们向上游提交给人们的语言进行修改吗?我们会说,“你知道吗?我们需要这三样东西,因为那会让编译器快得多。这是我们对语言的建议。” 我们什么都没做,对吧?我们什么都没做。
所以我认为态度需要从“这只是它的样子”或“我们得到了这些其他好处,所以这没关系”转变为“这没关系”。如果我们把态度转变为“这没关系”,我想我们会对我们能完成的事情感到惊讶,因为我们在游戏等事物中指出的所有这些东西,我们说,“好吧,它们不同。这就是为什么它们运行得快。” 我们让它们变快的。它们以前也是慢的,或者我们以前不知道怎么做那个。
主持人 (Dennis): 抱歉,Marko。Gingerbeard 已经耐心等待了,所以我让他说话,然后我们可以回到这个问题。
Marko: 他们申请麦克风了吗?
主持人 (Dennis): 是的,是的。我批准了。你可能静音了。
Ginger Bill: 嘿,大家晚上好。总之,我是 Gingerbeard。我想我拿到了麦克风。Bill,你好。你好,Casey。你好吗?
Casey: 很好。你好吗?
Ginger Bill: 很好,很好。这里有很多关于 LLVM 的讨论,我发现这非常……总之。只是告诉大家我是谁。我是 Odin 编程语言的创建者,我的主要工作是在 Django Effects,致力于像 Emogen、Liquogen 和 Geogen 这样的实时模拟软件,用于流体和地形之类的东西。所以这都是非常高性能的东西。
但关于 LLVM 的一件事,因为我在开发 Odin(编程语言),我在过去的七年里几乎每天都在使用 LLVM。所以我现在非常了解 LLVM 代码库,也了解 LLVM 如何到达这一点的历史。LLVM 是,如果找不到更好的词,很多意外的东西。因为你必须明白 LLVM 最初是一个被 Apple 收购的大学项目,然后被开发并制成它自己的编译器,然后分支成它自己的东西。所以 LLVM 甚至不再意味着它最初的意思,最初显然是“低级虚拟机(Low Level Virtual Machine)”。LLVM 现在作为一个实际的缩写词没有任何意义。它只是一个术语。
但是是的,我一直在听这里的一些事情,我只是有点困惑,因为人们误解了必须如何构建静态单赋值(Static Single Assignment, SSA)编译器的本质方面,与 LLVM 变成现在这样的意外事情混为一谈。LLVM 本身,静态单赋值背后的想法,如果人们想知道的话,实际上是一种半纯粹的、功能性的、高级汇编语言,它基于大量的图论来做很多的优化。LLVM 的组织方式非常传统,非常面向对象(OOP),很明显它在过去的 20 年里已经进化了。
但并没有内在的原因说明它必须按现在这样组织。我认为这里的很多困惑只是假设,“好吧,LLVM 是这样的,如果我们要搞定 LLVM,那是很多工作。” 答案是肯定的,但 LLVM 的本质根本不需要这样架构。这只是有点像……看,我知道情况确实如此,因为我目前正在制作我自己的 LLVM 替代品,我也知道一个朋友也在做这个,因为我们已经受够了 LLVM,因为它太慢了。对于我们的工作来说,这实际上是整个编译时间的 95%。而且我们也知道 LLVM 过去也更快。在版本 3、4 和 5 中,它比最新版本快得多,大概快三倍。
所以发生了一些事情,而且不是优化级别的问题。代码库这些年来只是变得越来越糟。但我离题了。这就是我试图在这里指出的,只是一些人们评论时对我来说没意义的事情,因为这有点像,我很了解 LLVM。这不必是这样的。它只是因为历史原因恰好是这样的。
主持人 (Dennis): 谢谢你的评论,Ginger Bill。你叫什么名字?还是我就叫你 Ginger Bill?
Ginger Bill: 你可以叫我任何你想叫的。Bill, Ginger Bill, Ginger。我真的不在乎。
主持人 (Dennis): 谢谢你的评论。还有其他评论或问题吗?我们在 Twitter Space 上有很多人,所以我只是想确保如果有的话,我们可以让各种人提问或发表评论。
Ivica: 我只想同意 Casey。你至少得到了我的全力支持。是的,我们对实际上可能的事情有点悲观。我们应该……作为一个社区,我们绝对应该努力调查和理解性能的极限。编译器,Clang,实际上,编译器实际上是这方面的一个很好的例子,也是我们试图理解那种工作负载的极限性能是什么的一个很好的例子。它们绝对是困难的……我不会说困难,但与例如游戏相比,优化的方式不同。游戏玩家已经开发了……游戏开发者已经开发了他们自己的方式。这对他们很有效。编译器开发者还没有那样做。所以这希望是在未来,希望是好的。我有,我的意思是,在我脑海里,我有关于那可能如何工作的想法,我不确定。
主持人 (Dennis): 我只想说,你知道,好吧,我显然不是在为 LLVM 的性能辩护,对吧?我同意。它肯定不完美。很多很多性能瓶颈。但我说不仅有一个解决方案。比如,“嘿,让我们直接扔掉所有的代码库并重写它”,对吧?我相当确信还有其他解决方案。比如,你可以从硬件本身的角度来攻击这个问题,对吧?你可以设计更好的芯片来更好地处理这些瓶颈,对吧?甚至拿这个指针追踪来说,对吧?我的意思是,有很多尝试只是为了优化这个问题,对吧?所以,例如,在 Apple 芯片(M1 和 M2)中实现了这种智能预取(smart prefetching),对吧?所以他们做这种……他们试图预取。他们查看从内存中获取的字节流,并试图识别指针,并试图提前推测性地预取它们。另一个解决方案就是值预测(value prediction),对吧?再次,这是可以在硬件中实现的东西,对吧?我们可以通过预测值来打破那些依赖链。我不是说这很容易,对吧?但重写代码库也不容易。所以,是的。
Marko: 所以我们一直在谈论从头开始改变这个,通过不同会议的演讲,我们有 Casey 正在进行的 Computer Enhanced 和他的系列视频。但是你认为随着云成本的爆炸式增长,会有一个向你的观点转变的趋势吗?所以现在,你知道,性能悲观化(pessimization),当你是从云的角度来看时,它真的对每月的账单有底线影响。而且你有首席财务官(CFO)每个月看到这些账单,因为软件的性能不是最优的。所以,你知道,你有所有这些文章出现,关于人们试图遏制他们的云成本,甚至考虑将他们的应用程序迁回本地。所以你认为也会有来自上层的压力,更多地考虑软件的性能,而不是像你描述的那样强调 Clean Code 吗?
Casey: 关于这个有两点。一是因我对此没有太多的洞察力,因为我只是不知道,你知道,我不在这些公司的董事会会议室里。所以我不知道这真的会给人们施加什么样的压力。显然,如果对此有市场力量那就太好了。但与此同时,对于很多这些事情已经有了某种市场力量。
它们不生产高性能软件的原因是因为计算机行业有点……或者应该说软件行业,可能是因为它还处于婴儿期。我的意思是,我们甚至还没有完成 100 年的商业软件。只是没有足够的竞争。很多时候对于这些公司,你知道,你可以拿我们在 Twitter 上用来做这个的东西来说。一旦你考虑到社交网络等锁定效应,Twitter 并没有那么多竞争对手。并不是人们可以进来在性能上与他们竞争,对吧?就像,东西是捆绑在一起的。如果你想用 Facebook,你必须用 Facebook。你不能使用另一个供应商销售的高性能版 Facebook。
所以性能一直是一个增值服务(value add)。我的意思是,人们买 iPhone 的原因是它是第一款高性能的手机,智能手机,对吧?我的意思是,Windows CE 有所有相同的东西。实际上它有更多的东西。但它笨重且缓慢,用手指拖动东西时没有 60 帧每秒。同样的,我经常用 MapQuest 和 Google Maps 的例子,对吧?MapQuest 现在在哪里?嗯,它没了,因为 Google Maps 出来了一个你可以拖动地图的东西,对吧?
所以,性能一直是一个你可以用来区分你的产品的东西。它一直是一股市场力量。但底线是,只是没有足够严峻的竞争,而且在软件中有太多的垄断锁定,使得它真的成为我们看到人们编程方式的东西。这太糟糕了,但这只是现实。是的,你可以在某些特定情况下在性能方面攻击某人。我们看到人们现在成立公司,例如,他们的增值是,这是一个真正快速的 IDE,对吧?而不是一个慢的。或者这是一个快速的 JavaScript 运行时。这是一个快速的 Python 底层,随便什么。
所以,是的,你知道,你在有限的案例中看到它,人们可以针对某事,而且他们没有被其他市场力量阻挡。但在那些情况之外,很难看到只关注性能的人必然能发布竞争产品,因为很多这些产品有如此多的锁定,仅仅发布一个更高性能的版本是行不通的,行不通的,对吧?因为有这些其他的,你知道,垄断是我们行业跨越许多方面的一个大问题。
关于云,我就像,“好吧,是的,我明白你在说什么。” 那个底线是不同的,因为现在我们在谈论垄断者,垄断者自己坐在那里说,“我想保留这笔钱,但我却把它交给了 Azure,或者我把它交给了 AWS。” 也许那是一个很大的区别,因为现在垄断者有了问题,他们将在自己内部采取措施解决它,而以前竞争对手做不到,因为他们无法因为其他原因攻击垄断,所以他们无法带着更高性能的产品进来。我会相信这会发生吗?当然。我对它有乐观或悲观吗?我只是不知道。所以我猜我不得不说我不确定。
关于第二部分,就像我们有点试图用东西做的事情,我想区分一下——我只想在这里做——优化和性能意识(Optimization and Performance Awareness)。所以在 Computer Enhanced 上,核心是性能意识编程,而不是优化。为什么?因为老实说我并不是一个真正的优化人员。我大部分时间并不花在优化代码上。
我在行业中广泛看到的是,我们提出的许多建议和我们教人们编程的方式默认导致了缓慢的代码。如果我们只是教他们不同的实践,他们默认就会得到快速的代码。不是优化过的代码。仍然有一个懂优化的人可以介入并做一个快得多的版本。
但我认为我们作为一个行业目前面临的是,我们真的不必考虑如何让程序员输出最优代码。因为我们离最优代码太远了,如果我们只是专注于让程序员不要做那些比本来应该慢一百或一千倍的事情(这是今天的一个典型范围)。一个典型的非多线程 Python 程序通过解释器运行大概慢一千倍,对吧?例如。我展示过这方面的演示。
所以如果我们只是把人们移到默认更快的事情上,我想这就足够了。性能,仅仅做出导致更好性能的决定,我想就是我们真正需要得到大部分胜利的全部。我想大多数程序员可以学习那些事情,并忘掉他们被教导的那些实际上很糟糕的事情。所以我想在那方面表达一些乐观,因为我不认为我们必须教每个人如何计算周期和如何,比如,把东西优化得很宽(wide optimization)以及所有其他那些东西。我认为我们可以做一些事情来拿回很多倍数,这不需要人们成为优化专家。
主持人 (Dennis): 太棒了。Dennis,我们到了 90 分钟的标记。我们要现在开始收尾吗?
主持人 (Dennis): 我觉得那……因为在我看来,我认为 Casey 的最后陈述是总结他的目标以及我们作为性能工程师的目标应该是什么的一个很好的方式。但你们觉得呢?我们应该……我们想继续吗?我们还有其他问题吗?
主持人 (Dennis): 我只是在想有没有任何其他方法我们可以有所帮助?因为,比如编译器有没有办法变得更好,你知道,在优化这种 Clean……因为,好吧,我的意思是,甚至,你知道,就拿这个虚函数调用作为例子,对吧?所以,再次,有一些努力,比如编译器做去虚拟化(devirtualization)。而且,再次,这只是——它并不完美,对吧?显然,它没有解决所有情况。但我只是在想,你知道,有没有任何方法我们可以得到帮助,你知道,只是解决那些慢的代码库。你们觉得呢,伙计们?
Casey: 我确实认为这有可能。我认为 C++ 的编译模型有点坏了,对吧?C++ 在这方面有很多问题。但如果你想象一个不同的编译模型,也许是内存常驻的,更通用的或类似的东西,你可以想象一个系统,其中编译器可以跨虚函数调用进行分析。而且我想规范可能不允许他们这样做,但假设规范允许。比如,我不是说规范允许,因为 C++ 规范就像,无法穿透,对吧?但是,比如假设它有来自规范的授权这样做。真的没有理由你不能把一堆类变成联合和 switch 语句或其他任何东西,对吧?它们是可以互换的。问题只是,比如,编译器此时有点被要求做 Vtable 的事情。而且通常它也没有所有实现的知识,因为通常人们这样做是为了隐藏实现,对吧?
所以你可以想象走向一个编译器可以为你做更多的系统。但是,我的意思是,这需要很多人做很多事情。所以,我的意思是,这是一个社会问题,对吧?你必须去说服一堆人做一堆事情才能让那发生。但是如果你能,我看不到任何技术原因不能有这种想法,比如,“嘿,我替换了 class 这个词。” 或者我给类添加了一个属性叫,比如 switchify 或随便什么。或者 devirtualize,我想那是你用的词。我看不到任何实际原因它不能那样做。
主持人 (Dennis): 你和 Bob 会做一个播客还是什么吗?
Casey: 我不这么认为。但是如果要的话我也还没听说。
主持人 (Dennis): 好的,这真的很棒。我想这是一次富有成效的讨论。我们超时了四分钟。Casey,非常感谢你加入我们的 Twitter Space。
Casey: 不,没问题。
Marko: 我们真的很感激你的时间。
Casey: 这非常有成效。谢谢邀请我。这很棒。
主持人 (Dennis): 是的。谢谢,伙计们。祝大家有个美好的一天。
Marko: 谢谢。谢谢大家。再聊。
Casey: 谢谢。