Raku 性能和 Physics::Unit

Raku Performance and Physics::Unit

在假期里,我花了一些时间在 Physics::Unit 模块上,并消除了一些关于 raku 编译时间的挫折感。

我一直在努力解决的基本问题是希望使用 raku 的自定义后缀运算符机制来表达物理 SI 单位,而不需要等待 30 分钟 raku 来编译我的模块。

这就是明智的设计、惰性执行、试错和 raku 强大的工具如何从30分钟到13秒以内的故事!

让我们先来看看阳光下的高地。想象一下,raku 为科学家和教育工作者提供了一个简单直观的工具,可以自动计算出物理单位的计算结果。

类似这样的东西:

use Physics::Constants;
use Physics::Measure :ALL;

$Physics::Measure::round-to = 0.01;

my \λ = 2.5nm; 
my \ν = c / λ;  
my \Ep = ℎ * ν;  

say "Wavelength of photon (λ) is " ~λ;              #2.5 nm
say "Frequency of photon (ν) is " ~ν.norm;          #119.92 petahertz 
say "Energy of photon (Ep) is " ~Ep.norm;           #79.46 attojoule

在你问之前,是的 - 这是真实的代码,可以在几秒钟内完成编译。它使用了最新版本的 Physics::Measure 模块,而这个模块又使用了 Physics::Units 模块。让我指出一些关于 raku 独特的功能组合是如何帮助我们的很酷的事情。

  • 保持熟悉的符号,如 λ(lambda)和 ν(nu)的 unicode。
  • 变量名不加 $ 符号,以保持方程的简洁性
  • Physics::Constants - c (光速) 和 ℎ (普朗克常数)
  • Physics::Measure:ALL 导入所有 SI 单位后缀运算符。
  • postfix:<nm> 做 Measure.new( value => 2.5, unit => ‘nanometre’ )
  • 使用自定义的 ‘/’ 和 ‘*’ 运算符进行 Measure 数学运算。
  • 知道 Frequency 类类型接受 SI 单位赫兹。
  • 知道 Energy 类类型接受 SI 单位焦耳。
  • 可以对 Measure 对象进行归一化,并对输出进行四舍五入。

那么 - 怎么会那么难呢?好吧,魔鬼就在这个小小的单词 all【SI 单位后缀运算符】中。请看这个表格。

img

所以我们有27个单位和20个前缀 - 也就是,呃,540 种组合。而你想让我把这些都导入我的命名空间。你想让我有一个包含540个 Physics::Unit 类型的库,当我使用后缀时,这些类型会被加载。你想清楚了吗!?

所以 - 为了分享我在 raku Physics::Journey 中的痛苦,以下是我在优化模块代码中的经验教训。

尝试1 – 忽略它

我的第一直觉是要放弃这个问题。最初的 perl5 Physics::Unit 模块允许编码者通过一个字符串表达式来指定单位类型 - 就像这样。

my $u2 = GetUnit( 'kg m^2 / s^2' );

总之,我知道我需要单位表达式来处理文本变体,比如 ‘mile per hour’ 或 ‘mph’,或 ’m/s',‘ms^-1’,’m.s-1'(SI 派生单位表示法)或 ’m-s-¹'(SI 推荐的字符串表示法,带上标幂)。所以,从一开始就在 Physics::Unit 中内置了一个新的单位表达式解析器,用raku Grammars。然而,很明显的说:

my $l = Length.new( value => 42, units => 'yards' );

是一个相当冗长的方式来输入每个测量值。不过,这是一个很酷的方式来应用(对我来说也是学习)raku GrammarAction,这导致了一个灵活的,对人类友好的单位表达式方言作为一个内置的 Physics::Unit 工具包。

尝试2 - 可以工作但慢如狗

到目前为止,我的 Physics::Unit 模块很乐意接受一个单位字符串,用 UnitGrammar 解析它并创建一个合适的 Unit 对象实例。就像这样:

Unit.new( factor => 0.00016631, offset => 0, 
    defn => 'furlong / fortnight', 
    type => Speed, dims => [1,0,-1,0,0,0,0,0], 
    dmix => ("fortnight"=>-1,"furlong"=>1).MixHash, names => ['ff'] );

这个用户定义的对象是通过迭代它的根来生成的(例如)1 fortnight => 2 weeks => 14 days => 336 hours => 2,016 mins => 120,960 secs(因此系数属性)。超过270个内置的单位和前缀定义 - 涵盖 SI,US(英尺,英寸),帝国(品脱,加仑)等。而 .in() 方法则用于转换。[一个单位库似乎没有什么意义,除非它能支持常见的用法,比如 mph 以及这个单位和正式的 SI 单位之间的换算]。

但是,现在我来实现我的后缀运算符 - 那么我需要在第一次编译时将540个定义传递给 Grammar,它需要建立540个对象实例。欢迎来到30分钟+的编译时间。

在我对这个批评走得太远之前 - 我想指出几个非常重要的注意事项。

  1. “所以,我们终于有了一个真正的 Perl 6,它也可以和所有这些语言竞争,至少在功能方面。它还不能与 Perl 5 竞争的一个方面是在性能方面,但今年上半年我们已经让它的运行速度几乎是圣诞节时的两倍。我们还有很多优化的空间。但 Perl 6 的目标是最终超越所有其他语言。Perl 一直以来都是在提高标准,然后再提高,再提高。” Larry Wall 在 2016 年的 Slashdot 上……而且优化和增强功能一直都在出现。
  2. Raku 重新编译是一个非常大的速度倍增器 - 即使是30分钟的编译时间,预编译的二进制文件加载和运行也只需要12秒左右。
  3. 我个人赞同这样的观点:Raku 是一门非常高级的语言,随之而来的程序员在精度和表达上的生产力优势超过了几秒钟的编译时间。我相信,硬件会继续改进,很快就会消除任何明显的延迟 - 比如最近苹果 M1 的发布。

尝试3 - 存量单位盲区

Sooo - 第三次尝试将所需的功能和合理的速度结合起来,就是预先生成540个单位的字面值 - “库存"单位。因此,代码可以在"转储"模式下运行,使用 Grammar 生成单位字面值,并存储到文本文件中,然后将它们粘贴回模块源,这样在发布的版本中,它们就可以通过"快速启动"模式读取。

通过重复使用相同的 Grammar 来预生成和快速生成用户定义的单位,这消除了任何潜在的兼容性问题。出于模块安全和代码完整性的考虑,我选择不对任何一个 MONKEY-SEE-NO-EVAL 进行妥协。

性能上的改进非常显著。通过绕过 Grammar 步骤,第一次编译时间降到了约 340s,预编译开始时间降到了 3s 以下。尽管如此,我还是不满意发布一个首次编译时间如此之慢的模块,于是寻找更好的设计。

尝试4 - 惰性实例化

在第四遍的时候,我决定用惰性的单位实例化来重构模块。因此,单位定义被初始化为哈希数据映射,但实际上只创建了少量的对象(基本单位和前缀)。

然后,当一个新的对象被调用时,它就会根据需求"懒惰"地生成。即使在极端的情况下,如 “furlong / fortnight” 的例子,也只创建了 O(10) 个对象。

通过取消库存单位,模块源减少了2000多行(每个对象字面值 540 x 4行)。性能又有了很大的提高 - 这次是 60s 的首次编译和 2.6s 的预编译启动时间。

然而,Physics::Measure 代码仍然需要体现后缀运算符,并将其输出到用户程序中。因此有540行要单独编译。每个 postfix 这样声明。

sub postfix:<m> ( Real:D $x ) is export { do-postfix( $x, 'm' ) }

尝试5 - UNIT::EXPORT 包

更妙的是,我又可以从优秀的 raku 文档中学习了 - 这次我发现了 UNIT::EXPORT,它给了我一个良好的开端,让我只用6行代码就能制作出一个程序化的导出所有 540 个 postfixes。再见了,样板代码:

my package EXPORT::ALL {
  for %affix-by-name.keys -> $u {
    OUR::{'&postfix:<' ~ $u ~ '>'} := 
                    sub (Real:D $x) { do-postfix($x,"$u") };
  }   
}

这就有了额外的性能提升 - 下面的最终数据……

尝试6 - 选择性导入

最后,说一下选择性导入。在尝试5之前,我尝试给不常用的单位贴上标签(:DEFAULT, :electricultural, :mechanical, :universal 和 :ALL 如果你有兴趣)。但即使将 :DEFAULT 减少到只占总数的 25%,这也没有显著减少第一次编译时间。我把这解释为编译器需要处理所有的行,即使用户程序没有指定导入标签。

但如果使用包的方法,Physics::Measure :ALL 将导出所有的 SI 单位。如果你想要更快的速度,并且不使用后缀运算符,只需删除 :ALL 标签即可。

最终结果

所以,最新的速度测量结果(在我的低规格 1.2GHz/8GB 笔记本电脑上)是:

# use Physics::Measure;      ...10s first-, 1.2s pre- compiled
# use Physics::Measure :ALL; ...13s first-, 2.8s pre- compiled

YMMV!

~p6steve (pronounced p-sics)

Raku 

comments powered by Disqus