Raku 中类型/约束的性能损失?

Nil*_*ile 29 c# performance raku

与 Perl 5 相比,Raku 引入了渐进式类型。逐渐类型化的面向对象语言的景观非常丰富,包括:Typed Racket、C#、StrongScript、Reticulated Python。

Raku 官方网站上说“可选的渐进类型检查,无需额外的运行时间成本”。据我所知,由于强制类型系统健全性的策略,一些渐进式类型语言(如 Typed Racket 和 Reticulated Python)遭受了严重的性能问题。另一方面,由于相对便宜的名义子类型测试,StrongScript 中的具体类型表现良好。渐进式分类研究(不包括乐):

StrongScript 中的C#具体类型:在类型构造函数上使用运行时子类型测试来补充静态类型。虽然静态类型代码以本机速度执行,但值是在类型和非类型边界上动态检查的。类型插入有效的强制转换并导致可以优化的代码。它们也很健全,开销也很低,但是在表达能力和从无类型迁移到有类型的能力方面付出了代价。

Typed Racket:监视值以确保它们按照指定的类型运行。包装器不是检查静态类型标签(如混凝土)的高阶和可变值,而是确保值与其声明类型的持久一致性。它避免了类型化代码中的强制转换。然而,它为这种健全性付出的代价是在类型化-非类型化边界插入重量级包装器。

网状Python:介于以上两者之间;它添加了类型转换,但仅针对顶级数据结构。网状 Python 的瞬态语义的性能对于具体类型来说是最坏的情况——即,几乎每次调用都会进行强制转换。它在使用时检查类型,因此向程序添加类型的行为会引入更多强制转换并可能减慢程序速度(即使在完全类型化的代码中)。

Raku 的运行时执行策略是类似于 C# 和 StrongScript 中的具体类型,还是它有自己的一套策略来确保没有像 Typed Racket 和 Reticulated Python 那样明显的性能问题?它有健全的渐变类型系统吗?

Jon*_*ton 33

Raku 要求写入程序的类型约束最迟在运行时执行。如何保持承诺取决于编译器和运行时实现者。我将讨论 Rakudo(编译器)和 MoarVM(运行时)配对如何做到这一点,因为这就是我一直在研究的内容。

初始编译本身在分析方面做的很少,以消除类型检查,因此我们生成的字节码中有很多类型检查。这里的赌注是分析需要时间,只有一些代码实际上会发现自己在热路径上(或者对于非常短的脚本,没有热路径),所以我们不妨让 VM 来计算找出什么是热门,然后专注于那些位。

VM 执行现代运行时所做的典型分析,不仅记录哪些代码是热的,而且还记录参数类型、返回类型、词法类型等的统计信息。尽管可能发生大量潜在的动态变化,但在给定的应用程序中,现实情况是大量代码是单态的(只能看到一种类型,或者对于例程,一种参数类型元组)。另一组是多态的(看到几种不同的类型),而相对较小的数量是超态的(类型的负载)。

根据它获得的数据,运行时产生专业化:基于关于将显示哪些确切类型的假设编译的代码版本。防止精确类型比必须关心子类型关系等更便宜。所以在这一点上,我们已经有了一个代码版本,其中我们预先有一些廉价的先决条件,并且我们已经使用它们来消除更昂贵的类型检查(以及一些分散在代码中的额外保护,以取代其他类型检查)。然而,这并不是真正免费的......还没有。

拨打电话时,可能会发生以下两种情况之一:

  • 对于小的被调用者,会发生内联。我们内联被调用者的特化。如果调用者中的类型知识已经足以证明类型假设——通常是这样——那么就不需要任何保护。本质上,被调用者中的类型检查变得免费了。我们可以深入内联多个级别。此外,内联让我们可以跟踪通过被调用者的数据流,这可以让我们消除进一步的保护,例如关于被调用者中的返回值类型。
  • 对于较大的被调用者,我们可以执行特化链接——即直接调用特化并绕过其守卫,因为我们可以使用调用者中的类型知识来证明我们满足守卫假设。同样,被调用者参数类型检查因此变得免费。

但是那些不是调用的 type-y 的东西呢,比如返回值类型检查和赋值?我们也将它们编译为调用,因此我们可以重用相同的机制。例如,返回类型检查,在它是单态的情况下(通常),变成一个守卫+对恒等函数的调用,只要我们能证明卫士,就变成恒等函数,这是一个微不足道的内联.

还有更多的事情要做。值得注意的是:

  • 我上面描述的机制是围绕着各种缓存和守卫树构建的,它并不像我说的那么漂亮。有时一个人需要建造丑陋的东西来学习足够的知识才能知道如何建造漂亮的东西。值得庆幸的是,当前的大量工作正在将所有这些学习整合到一个新的、统一的、保护和调度机制中,这也将承担今天优化得很差的语言的各个方面。这是由于在几个月内发布。
  • 当前运行时已经做了一些非常有限的逃逸分析和标量替换。这意味着它可以跟踪数据流到短期对象中,从而找到更多类型检查来消除(在消除内存分配的基础上)。正在努力使其更强大,提供部分逃逸分析、传递分析以标量替换整个对象图,从而能够通过它们跟踪数据流等类型。

去年,发表了一篇题为Transient typechecks(几乎)免费的论文。这根本不是关于 Raku/Rakudo/MoarVM,但它是我在学术文献中看到的最接近我们正在做的事情的描述。那是我第一次意识到我们可能正在这个领域做一些有点创新的事情。:-)


rai*_*iph 11

现在 jnthn 已经撰写了一份关于截至 2020 年 Rakudo 和 MoarVM 状况的权威概述,我觉得可以发表一些相当于非专家的文章,写一些涵盖 2000 年到 2019 年的手波浪式历史笔记,一些读者可能会感兴趣.

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

Raku 中类型/约束的性能损失?

不应该有惩罚,而是相反。也就是说,Larry Wall 在早期(2001 年)的设计文档中写道:

为它提供更多类型信息以提高性能和安全性

(这是在 2005 年学术会议上引入“渐进式打字”一词之前的 4 年。)

所以他的意图是,如果开发人员添加了合适的类型,程序运行得要么更安全,要么更快/更精简,或者两者兼而有之。

(和/或能够用于与外语的互操作:“除了性能和安全性之外,类型信息有用的另一个地方是编写与其他语言的接口。”。十年后,他说#1 和类型的 #2 原因是多次调度和文档。)

我不知道有任何系统性的努力来衡量 Rakudo 在多大程度上实现了类型永远不会减慢代码速度并且如果它们是本机类型则可以预测地加速它的设计意图。

此外,乐堂的变化仍然相对较快,十年间的整体年度绩效提高了 2-3 倍。

(虽然 Rakudo 已有 15 年的历史,但它是随着 Raku 语言的发展而发展起来的——最终在过去几年中安定下来——而 Rakudo 的整体发展阶段是“Make它工作,让它正常工作,让它快速工作”,后者近年来才真正开始发挥作用。)

据我所知,由于强制类型系统健全性的策略,一些渐进式类型语言(如 Typed Racket 和 Reticulated Python)遭受了严重的性能问题。

从理论到实践的渐进打字 (2019)总结了 2015 年的一篇论文,其中说:

第一次测量[健全性成本]的系统性努力......揭示了实质性的性能问题......

......(大概是你一直在读的那些)......

[并且] 使用JIT编译器、名义类型、表示改进和定制编译器可以显着提高性能......

现在将他们的上述性能配方与 Rakudo 和 Raku 的特征进行比较:

  • Rakudo 是一个已有15 年历史的定制编译器,具有多个后端,包括带有 x86 JIT的定制MoarVM后端。

  • Raku 语言有一个(渐进的)名义类型系统。

  • Raku 语言支持表示多态性。这就像所有表示改进之母,不是从一体的意义上说,而是从结构中抽象出表示,因此可以通过表示多态性带来的自由来进行改进。

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

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

我注意到 jnthn 的评论:

防止精确类型比必须关心子类型关系等更便宜

我的猜测是,关于 Rakudo 是否提供或将有朝一日提供足够的性能使其渐进式打字普遍具有吸引力,陪审团将在大约 5 年左右的时间里出来。

也许一位陪审员(嗨,尼罗河)将是第一个就 Raku(do)与其他渐进式语言在明年左右进行比较的初步结论?

健全性

它有健全的渐变类型系统吗?

从某种意义上说,有数学处理吗?我 99% 肯定答案是否定的。

从某种意义上说,它被认为是合理的?唯一假定的保证是内存安全?我想是这样。还有比这更多的吗?好问题。

我只能说 afaik Raku 的类型系统是由 Larry Wall 和 Audrey Tang 等黑客开发的。(参见她 2005 年关于类型推断的笔记。)