Raku中类型/约束的性能惩罚是什么?

31
与Perl 5相比,Raku引入了渐进式类型。逐渐类型化的面向对象语言的领域非常丰富,其中包括:Typed Racket、C#、StrongScript、Reticulated Python等。 在Raku官方网站上称“可选的渐进式类型检查不会增加运行时成本”。据我所知,一些渐进式类型语言(如Typed Racket和Reticulated Python)由于执行类型系统声音的策略而遭受严重的性能问题。另一方面,StrongScript中的具体类型表现良好,这要归功于相对便宜的名义子类型测试。关于渐进式类型分类的研究(不包括Raku):C#和StrongScript中的具体类型:使用类型构造函数上的运行时子类型测试来补充静态类型。虽然静态类型代码以本地速度执行,但值在类型不同的边界上动态检查。类型插入有效的强制转换,并导致可以优化的代码。它们也是可行的并且开销较低,但代价是表达能力和从未输入到已输入的迁移能力。Typed Racket:监控值以确保它们按照其分配的类型进行操作。与检查高阶和可变值的静态类型标记不同,包装器确保值始终符合其声明的类型,从而避免在类型化代码中进行强制转换。然而,为了实现这种严谨性,它会在类型化和非类型化边界处插入沉重的包装器。

Reticulated Python:介于上述两者之间;它添加类型强制转换,但仅对数据结构的顶层执行。瞬时语义的性能是具体类型的最坏情况 - 即几乎每次调用都需要强制转换。它在使用时检查类型,因此添加类型到程序中会引入更多的强制转换,并可能使程序变慢(即使在完全类型化的代码中也是如此)。

Raku 的运行时强制执行策略是否与 C# 和 StrongScript 中的具体类型类似,还是有自己的一套策略以确保没有像 Typed Racket 和 Reticulated Python 那样的明显性能问题?它是否有一个可靠的渐进式类型系统?

2个回答

35
Raku要求在程序中编写的类型约束最迟在运行时强制执行。如何实现这个承诺取决于编译器和运行时实现者。我将讨论Rakudo(编译器)和MoarVM(运行时)是如何实现的,因为这是我参与的部分。
初始编译本身在消除类型检查方面做得很少,因此我们产生的字节码中有很多类型检查。这里做出的赌注是分析需要时间,只有一些代码实际上会处于热点路径上(或对于非常短的脚本,根本没有热点路径),因此我们可以将其留给虚拟机来确定什么是热点,并专注于这些部分。
虚拟机执行了现代运行时所执行的典型分析,不仅记录哪些代码是热点,还记录参数类型、返回类型、词法类型等统计信息。尽管可能发生很多潜在的动态性,但在给定的应用程序中,现实情况是大量的代码是单态的(只看到一个类型,或对于例程,只有一个参数类型元组)。另一堆是多态的(看到几种不同的类型),相比之下,极少量是超多态的(加载了大量类型)。

根据获取的数据,运行时会产生特化:基于关于将要出现的确切类型的假设编译代码的版本。保护确切类型比关心子类型关系等更为便宜。因此,在这一点上我们得到了一个版本的代码,在其中有一些廉价的前提条件,并且我们已经使用它们来消除了更昂贵的类型检查(以及在代码中散布的其他类型检查替代品)。然而,这还不是真正免费的...

当进行调用时,可能会发生以下两种情况:

  • 对于小的被调用函数,进行内联。我们内联被调用函数的一个专门版本。如果调用者对类型的知识已足以证明类型假设,那么就不需要任何保护。实质上,被调用函数中的类型检查变成了免费的。我们可以内联多层次。此外,内联使我们可以跟踪通过被调用函数的数据流,从而可能让我们消除进一步的保护措施,例如有关被调用函数的返回值类型的保护。
  • 对于较大的被调用函数,我们可以执行专门链接-即直接调用一个特化版本并绕过其保护措施,因为我们可以使用调用者中的类型知识来证明我们满足保护措施的假设。同样,被调用函数的参数类型检查也因此变得免费。

但是对于那些不是函数调用的类型检查和赋值,比如返回值类型检查和赋值,该怎么办呢?我们将它们编译为函数调用,以便重复使用相同的机制。例如,在单态(通常)的情况下,返回类型检查会转换为一个保护加上对标识函数的调用,每当我们能够证明这个保护时,它就变成了一个简单的内联的标识函数。

还有更多内容要介绍:

  • 我以上所描述的机制建立在各种缓存和保护树周围,不是像我说的那样美好。有时候需要做出丑陋的东西才能学到足够的知识,从而知道如何构建漂亮的东西。值得注意的是,目前正在进行一些工作,将所有这些经验教训都融合到一个新的、统一的保护和分派机制中,这个机制还将承担当前语言中优化非常差的各种方面。预计将在未来几个月内发布。
  • 当前运行时已经做了一些非常有限的逃逸分析和标量替换。这意味着它可以跟踪数据流到短暂的对象中,从而找到更多要消除的类型检查(在消除内存分配的基础上)。正在进行一项工作,使它更加强大,提供部分逃逸分析、传递性分析,以便替换整个对象图并能够跟踪数据流和类型。
去年,一篇名为“瞬时类型检查(几乎)是免费的”(Transient typechecks are (almost) free)的论文被发表。虽然它与Raku/Rakudo/MoarVM无关,但这是我在学术文献中看到的最接近我们所做内容的描述。那是我第一次意识到,也许我们在这个领域正在进行某种创新。 :-)

@Nile 我有偏见,但我鼓励你看一下Jonathan回答中链接的论文——渐进式检查只有在你的运行时不试图变得高效时才会变慢,即使没有通用检查。它使用的方法与Raku/MoarVM非常相似(事实上,我们应该引用它!),我现在有一个博士生正在研究更接近的东西。随时联系我。- Michael Homer Jul 3 '20 at 8:00 --------------------------------(这条评论是从Liz的答案中最初留下的剪切/粘贴。我将其复制到这里,以便所有人都可以看到) - raiph

12
现在,jnthn撰写了一篇关于Rakudo和MoarVM的权威概述,截至2020年的情况。我认为可以发布一些手写历史笔记,涵盖2000年到2019年,这可能会引起一些读者的兴趣。

我的笔记是为了回答你问题中的摘录而组织的:

Raku中类型/约束的性能惩罚?

理论上不应该有惩罚,相反地应该增加性能。Larry Wall在早期(2001年)设计文档中写道:

随着您提供更多的类型信息,它将提供更高的性能和安全性

(这是在2005年的一次学术会议上引入“渐进式类型”术语之前的4年。)

因此,他的意图是,如果开发人员添加了适当的类型,则程序运行时更安全、更快/更轻,或两者兼备。

(并且/或能够与外部语言进行交互:“除了性能和安全性之外,类型信息还在编写与其他语言的接口方面非常有用。”十年后,他说类型的第一和第二个原因是多重分派和文档。)

我不知道是否有系统的努力来衡量Rakudo在实现类型不会减慢代码运行速度并且如果它们是本地类型,则可以可预测地加速的设计意图方面的程度。

此外,Rakudo仍在快速发展中,过去十年中整体性能提升了2-3倍。
(虽然Rakudo已经有15年的历史了,但它是随着Raku语言的不断演变而发展起来的,最终在过去几年中才稳定下来,Rakudo的总体开发分为三个步骤:“让它工作起来,让它正确地工作起来,让它快速工作起来”,后者仅在近年开始真正实施。)
引用一段话:“据我所知,一些逐渐类型化的语言(如Typed Racket和Reticulated Python)由于执行类型系统合规策略而遭受了严重的性能问题。” Gradual Typing from Theory to Practice (2019)总结了一篇2015年的论文,称:
一个系统性的尝试来测量[声音成本]…揭示了相当大的性能问题…
(可能是你们一直在读的那些问题)...
还说:
[而且]使用JIT编译器、名义类型、表示改进和定制编译器等方法可以显著提高性能。
现在将其性能特征与Rakudo和Raku进行比较:
  • Rakudo是一个15年历史的自定义编译器,带有几个后端,包括使用自定义MoarVM后端的x86 JIT

  • Raku语言具有(逐渐)名义类型系统。

  • Raku语言支持表示多态性。这就像所有表示改进的母亲,不是指其中之一,而是因为它将表示从结构中抽象出来,因此可以利用表示多态性带来的自由进行改进。

  • 还有其他与类型系统相关的潜在性能贡献;例如,我期望本地数组(包括多维稀疏等)有一天会成为重要贡献者。

另一方面,StrongScript中的具体类型由于相对廉价的名义子类型测试而表现良好

我注意到jnthn的评论:

防范确切类型比关心子类型关系等更便宜

我猜大约还需要5年左右的时间,才能确定Rakudo是否提供了足够的性能,使其逐渐类型化变得普遍有吸引力。

也许有一名陪审员(你好,Nile)将在未来一年左右首先对Raku(do)与其他逐渐类型化语言进行初步比较。

完备性

它是否具有完备的逐渐类型系统?

从数学处理的角度来看呢?我99%确定答案是否定的。

从被认为是完备的角度来看呢?唯一的保证只是内存安全吗?我认为是这样。还有更多的保证吗?好问题。

我所知道的是,Raku的类型系统是由像Larry Wall和Audrey Tang这样的黑客开发的。 (参见她2005年有关类型推断的笔记。)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接