提问者:小点点

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


与Perl5不同的是,Raku引入了渐进式类型。 逐渐类型化的面向对象语言的景观是丰富的,包括:类型化的Racket,C#,StrongScript,网状Python。

在Raku官方网站上说“可选的渐进式类型检查,不需要额外的运行时间成本”。 据我所知,一些渐进式键入语言(如Typed Racket和Reticulated Python)由于执行类型系统健全性的策略而遭受严重的性能问题。 另一方面,由于相对便宜的标称子类型测试,StrongScript中的具体类型执行得很好。 渐变分型(不含Raku)分类研究;

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

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

网纹蟒:介于两者之间; 它添加类型强制转换,但只针对数据结构的顶层。 对于具体类型来说,网络Python的瞬态语义的性能是最坏的情况--也就是说,几乎在每个调用中都有一个强制转换。 它在使用时检查类型,因此向程序添加类型的行为会引入更多的强制转换,并可能使程序变慢(即使是在完全类型的代码中)。

Raku的运行时强制策略是否类似于C#和StrongScript中的具体类型,或者它有自己的一套策略来确保不存在类型化Racket和网状Python那样明显的性能问题? 它有健全的渐进式系统吗?


共3个答案

匿名用户

Raku要求写入程序的类型约束最迟在运行时强制执行。 如何实现这个承诺取决于编译器和运行时实现器。 我将讨论Rakudo(编译器)和MoarVM(运行时)的配对是如何实现这一点的,因为这是我所做的工作。

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

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

基于它获得的数据,运行库生成专门化:基于对将显示的确切类型的假设而编译的代码版本。 防范精确类型比关心子类型关系等要便宜得多。 因此,在这一点上,我们得到了一个版本的代码,在这个版本中,我们有一些廉价的前置条件,并且我们已经使用它们来消除更昂贵的类型检查(以及一些分散在代码中的额外保护来替代其他类型检查)。 不过,这还不是真正免费的。。。

进行呼叫时,可能会发生以下两种情况之一:

  • 对于小的被调用项,将进行内联。 我们内联了被调用方的专门化。 如果调用方中的类型知识已经足以证明类型假设(通常是这样),那么就不需要进行任何保护。 实际上,被调用方中的类型检查变得免费了。 我们可以内联多层深度。 此外,内联使我们能够跟踪通过被调用方的数据流,这可能使我们消除进一步的保护,例如关于被调用方中的返回值类型的保护。
  • 对于较大的被调用方,我们可以执行专门化链接--即直接调用专门化并绕过它的保护,因为我们可以使用调用方中的类型知识来证明我们符合保护假设。 这样,被调用方参数类型检查也就自由了。

但是,不是调用的y类型的东西呢,比如返回值类型检查和赋值呢? 我们也将它们编译为调用,因此我们可以重用相同的机制。 例如,一个返回类型检查,如果它是单态的(通常),就会变成一个保护+一个对标识函数的调用,而每当我们能够证明保护时,它就会变成标识函数,这是一个简单的内联函数。

还会有更多。 值得注意的是:

  • 我上面描述的机制是围绕各种缓存和保护树构建的,并不像我说的那么漂亮。 有时候,一个人需要建造丑陋的建筑,才能学会如何建造美好的建筑。 值得庆幸的是,当前的一系列工作正在将所有这些学习合并到一个新的,统一的,保护和调度机制中,这也将承担该语言目前优化非常差的各个方面。 这将在几个月内登陆。
  • 当前运行库已经执行了一些非常有限的转义分析和标量替换。 这意味着它可以跟踪进入短期对象的数据流,从而发现要消除的类型检查更多(在消除内存分配的基础上)。 目前正在进行使其更强大的工作,提供部分转义分析和传递分析,以便用标量替换整个对象图,从而能够通过它们跟踪数据流等类型。

去年,一篇题为“瞬态类型检查(几乎)免费”的论文发表了。 它完全不是关于Raku/Rakudo/MoarVM的,但它是我在学术文献中看到的与我们正在做的事情最接近的描述。 那是我第一次意识到也许我们在这个领域做一些创新的事情。 :-)

匿名用户

现在jnthn已经写了一篇关于2020年Rakudo和MoarVM的权威综述,我觉得可以发表一篇非专业人士写的关于2000年到2019年的历史笔记,这些笔记可能会引起一些读者的兴趣。

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

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

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

更多的性能和安全性,因为您可以使用更多的类型信息

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

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

(和/或能够用于与外国语言的互操作:“除了性能和安全性,类型信息的另一个有用之处是编写其他语言的接口。”十年后,他说类型的第一和第二个原因是多重分派和文档。)

我不知道有任何系统性的努力来衡量Rakudo在多大程度上实现了这样的设计意图,即类型永远不会降低代码的速度,如果它们是原生类型,则可以预测地加快代码的速度。

此外,Rakudo仍在相对快速地变化,十年前的总体年度业绩改善幅度在2-3倍之间。

(虽然Rakudo只有15岁,但它是随着Raku语言的发展而发展起来的--在过去几年里终于安定下来了--而且Rakudo发展的总体阶段是经过深思熟虑的1-2-3“让它工作,让它正确工作,让它快速工作”,而后者直到最近几年才真正开始发挥作用。)

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

逐步打字从理论到实践(2019年)总结了2015年的一篇论文说:

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

。。。(大概是你读到的那些)。。。。

使用JIT编译器,标称类型,表示改进和自定义编译器,可以显著提高性能。。。

现在将他们上面的表演秘方与Rakudo和Raku的特性进行比较:

>

  • Rakudo是一个有15年历史的自定义编译程序,它有几个后端,包括带有x86 JIT的自定义MoarVM后端。

    拉库语有一个(渐变的)名词类型系统。

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

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

    另一方面,由于相对便宜的标称子类型测试,StrongScript中的具体类型执行得很好

    我注意到JNTHN的评论:

    防范精确类型比关心子类型关系等要便宜

    我的猜测是,关于Rakudo是否正在提供,或者有朝一日会提供足够的性能,使它的渐进式打字具有普遍吸引力,陪审团还将在5年左右的时间内做出判断。

    也许有一位陪审员(嗨,尼罗河)会在未来一年左右的时间里,第一个就拉库语(do)与其他逐渐类型化的语言相比,得出一些试探性的结论?

    它有健全的渐进式系统吗?

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

    在被认为是健全的意义上? 唯一的保证就是内存安全? 我也这么想。 还有比这更多的吗? 问得好。

    我所能说的是,afaik Raku的类型系统是由拉里·沃尔和奥黛丽·唐这样的黑客开发的。 (参见她2005年关于类型推理笔记。)

  • 匿名用户

    在Raku中,类型检查总是发生在引擎盖下面。 只是你没意识到。 如果优化器能够发现它不需要检查类型,它将优化检查。

    问题是,如果你说:

    my $foo = 42;
    

    你实际上是在说:

    my Any $foo = 42;
    

    其中any是几乎所有类的函数根。 并且由于42~~any给出true,因此允许该赋值。

    现在,如果您确实给出了一个特定的类型,比如int,它将对该类型执行smartmatch。 而且由于42~~int给出true,这也是允许的。

    但是,如果您指定了一个类型,例如str,它将执行一个42~~str,这是false,因此将给您一个执行错误。

    my Str $foo = 42;
    # Type check failed in assignment to $foo; expected Str but got Int (42)
    

    我认为这是一个完善的渐进式键入系统,因为类型检查总是会发生,不管是默认类型,内置类型还是您自己设计的类型。 基本上,如果它智能匹配,则可以用于类型检查。

    智能匹配使用accepts方法。 如果你说:

    say foo ~~ bar;
    

    你基本上是在说:

    say bar.ACCEPTS(foo);
    

    您可以在自己类的accepts方法中放入所需的任何代码:

    class FOO {
        method ACCEPTS($value) {
            $value == 42
        }
    }
    say 42 ~~ FOO;   # True
    say 666 ~~ FOO;  # False
    

    希望这能稍微解释一下检字机。