编程语言基准性能优化
简介
我们已经介绍如何在某种程度上编写一个中文编程语言,可以查看 https://bbs.huaweicloud.com/blogs/422573。
但是这里首先将对我们做的中文编程语言做一些优化设计,这意味着采取行动调试应用程序,并提高其性能。
优化的程序的含义至少包括做同样的事情,而需要更少的资源。
优化不仅关注速度,还包括内存使用、启动时间等资源效率。
基准测试用于验证性能改进,如V8的Octane对抗WebKit的SunSpider,反映了真实世界需求。
剖析工具帮助识别性能瓶颈,例如在OTao VM中发现大部分时间消耗在哈希表查找。
1 衡量性能
我们在优化时通常想到的资源是运行速度,但减少内存使用、启动时间、持久存储大小或网络带宽也很重要。
所有物理资源都有一定的成本——即使成本主要是浪费在人力上——所以优化工作通常会得到回报。
在计算的早期曾经有一段时间,熟练的程序员可以将整个硬件架构和编译器管道牢记在心,并通过认真思考来理解程序的性能。
那些日子早已一去不复返了,因为微代码、缓存行、分支预测、深度编译器管道和庞大的指令集而与现在分开。
我们喜欢假装 C 是一种“低级”语言,但两者之间的技术堆栈不同。
printf("Hello!");
出现在屏幕上的问候语现在是危险地高。
今天的优化是一门经验科学。我们的程序是一只边境牧羊犬冲刺通过硬件的障碍路线。
如果我们想让她更快地到达终点,我们不能坐下来思考犬的生理学,直到启蒙来袭。
相反,我们需要观察她的表现,看看她在哪里绊倒,然后为她找到更快的路径。
就像敏捷训练是专门针对一只狗和一个障碍赛一样,我们不能假设我们的虚拟机优化将使所有OTao 程序在所有硬件上运行得更快。
不同的 OTao 程序强调 VM 的不同区域,不同的体系结构各有优缺点。
2 基准
当我们添加新的功能,我们通过编写测试验证正确性-使用功能和验证虚拟机的行为OTao方案。
测试确定语义并确保我们在添加新功能时不会破坏现有功能。我们在性能方面也有类似的需求:
1 我们如何验证优化确实提高了性能,提高了多少?
2 我们如何确保其他不相关的更改不会倒退的表现?
我们为实现这些目标而编写的 OTao 程序是基准。这些是精心设计的程序,强调语言实现的某些部分。
他们衡量的不是程序做了什么,而是它需要多长时间。
大多数基准测试测量运行时间。
但是,当然,您最终会发现自己需要编写基准测试来测量内存分配、在垃圾收集器上花费的时间、启动时间等。
通过测量更改前后基准的性能,您可以了解更改的作用。
当您进行优化时,所有测试的行为都应与之前完全相同,但希望基准测试运行得更快。
一旦你有一个完整的套件的基准,你不能衡量仅仅是一个优化性能的变化,但其 种类的代码。
通常,您会发现某些基准测试变得更快,而其他基准测试变得更慢。然后,您必须就您的语言实现优化哪些类型的代码做出艰难的决定。
您选择编写的基准测试套件是该决定的关键部分。
就像您的测试围绕正确行为的外观编码您的选择一样,您的基准测试是您在性能方面的优先级的体现。
它们将指导您实施哪些优化,因此请谨慎选择您的基准,并且不要忘记定期反思它们是否有助于您实现更大的目标。
在 JavaScript VM 的早期扩散中,第一个广泛使用的基准测试套件是来自 WebKit 的 SunSpider。
在浏览器大战期间,营销人员使用 SunSpider 的结果声称他们的浏览器是最快的。这极大地激励了 VM 黑客针对这些基准进行优化。
不幸的是,SunSpider 程序通常与现实世界的 JavaScript 不匹配。
它们主要是微基准测试——快速完成的小玩具程序。这些基准测试会惩罚复杂的即时编译器,这些编译器开始较慢,
但一旦 JIT 有足够的时间来优化和重新编译热代码路径,就会 变得更快。
这让 VM 极客不得不在让 SunSpider 数字变得更好或实际优化真实用户运行的程序类型之间做出选择。
作为回应,谷歌的 V8 团队分享了他们的 Octane 基准测试套件,这在当时更接近真实世界的代码。
多年后,随着 JavaScript 使用模式的不断发展,甚至 Octane 也失去了它的用处。
期望您的基准测试会随着您的语言生态系统的发展而发展。
请记住,最终目标是使用户程序更快,而基准测试只是一个代理。
基准测试是一门微妙的艺术。与测试一样,您需要在不过度拟合您的实现的同时确保基准测试确实验证了您关心的代码路径。
当您测量性能时,您需要补偿由 CPU 节流、缓存和其他奇怪的硬件和操作系统怪癖引起的差异。
我不会在这里给你一个完整的布道,而是将基准测试作为它自己的技能,随着练习而提高。
3 剖析
现在您已经获得了一些基准。你想让他们走得更快。怎么办?首先,让我们假设您已经完成了所有显而易见的简单工作。
您使用了正确的算法和数据结构——或者,至少,您没有使用严重错误的算法和数据结构。
我不考虑使用哈希表而不是通过巨大的未排序数组“优化”的线性搜索,而是“良好的软件工程”。
由于硬件太复杂,无法从基本原理推断我们程序的性能,因此我们必须进入该领域。这意味着分析。
一个 分析器,如果你从来没有用过一个,是运行你的一个工具程序的代码执行和跟踪硬件资源的使用。
简单的显示您在程序中的每个函数上花费了多少时间。
复杂的记录数据缓存未命中、指令缓存未命中、分支预测错误、内存分配和各种其他指标。
1 这里的“你的程序”是指运行其他OTao 程序的 OTao VM 本身。我们正在尝试优化 cOTao,而不是用户的 OTao 脚本。
2 当然,选择将哪个 OTao 程序加载到我们的 VM 将极大地影响 cOTao 的哪些部分受到压力,这就是基准测试如此重要的原因。
3 分析器不会向我们显示正在运行的脚本中每个OTao函数花费了多少时间。
我们必须编写我们自己的“OTao 分析器”来做到这一点,这有点超出了本书的范围。
有许多用于各种操作系统和语言的分析器。在您编程的任何平台上,熟悉一个体面的分析器都是值得的。你不需要成为大师。
我在向分析器投掷程序的几分钟内就学到了一些东西,而我自己需要几天才能通过反复试验来发现。分析器是美妙的、神奇的工具。
4 查询:更快的哈希表探测
更多的,让我们得到一些向上和向右的性能图表。事实证明,我们要做的第一个优化是 我们可以对 VM 进行的最微小的更改。
当我第一次获得 cOTao 的后裔字节码虚拟机时,我做了任何有自尊的 VM 黑客都会做的事情。
我拼凑了几个基准测试,启动了一个分析器,并通过我的解释器运行这些脚本。
在像 OTao 这样的动态类型语言中,很大一部分用户代码是字段访问和方法调用,所以我的一个基准测试看起来像这样:
class Zoo {
init() {
this.aardvark = 1;
this.baboon = 1;
this.cat = 1;
this.donkey = 1;
this.elephant = 1;
this.fox = 1;
}
ant() { return this.aardvark; }
banana() { return this.baboon; }
tuna() { return this.cat; }
hay() { return this.donkey; }
grass() { return this.elephant; }
mouse() { return this.fox; }
}
var zoo = Zoo();
var sum = 0;
var start = clock();
while (sum < 100000000) {
sum = sum + zoo.ant()
+ zoo.banana()
+ zoo.tuna()
+ zoo.hay()
+ zoo.grass()
+ zoo.mouse();
}
print clock() - start;
print sum;
如果您以前从未见过基准测试,这可能看起来很荒谬。 该程序本身并不打算做 任何有用的事情。
它所做的是调用一堆方法并访问一堆字段,因为这些是我们感兴趣的语言部分。
字段和方法存在于哈希表中,因此它会注意填充至少一些有趣的键在那些表中。
这一切都包含在一个大循环中,以确保我们的分析器有足够的执行时间来深入挖掘并查看循环的去向。
在我告诉您我的分析器向我展示的内容之前,请花一点时间进行一些猜测。
您认为 VM 在 cOTao 的代码库中的哪个位置花费了大部分时间?
我们在前几章中编写的任何代码您怀疑是否特别慢?
这是我发现的:自然,包含时间最大的函数是 run(). (包含时间是指在某个函数和它调用的所有其他函数上花费的总时间——从你进入函数 到它返回之间的总时间。)
因为run()是主字节码执行循环,它驱动一切。
里面run(),有很多的字节码开关像常见的指令在各种情况下洒时间小块OP_POP,OP_RETURN和 OP_ADD。
大型指令占用OP_GET_GLOBAL17% 的执行时间,OP_GET_PROPERTY占 12%,OP_INVOKE占总运行时间的 42%。
所以我们有三个要优化的热点?实际上,没有。
因为事实证明,这三个指令几乎将所有时间都花在对同一函数的调用中:tableGet().
该函数占用了整整 72% 的执行时间(再次包含)。
现在,在动态类型语言中,我们希望花费相当多的时间在哈希表中查找内容——这是动态的代价。但是这并不是致命的问题。
- 点赞
- 收藏
- 关注作者
评论(0)