为什么 Raku 在使用多维数组时表现如此糟糕?

Jav*_*vi 13 performance raku

我很好奇为什么 Raku 在操作多维数组时表现如此糟糕。我在 Python、C# 和 Raku 中做了一个初始化二维矩阵的快速测试,而后者的耗用时间非常高。

对于乐

my @grid[4000;4000] = [[0 xx 4000] xx 4000];
# Elapsed time 42 seconds !!
Run Code Online (Sandbox Code Playgroud)

对于 Python

table= [ [ 0 for i in range(4000) ] for j in range(4000) ]
# Elapsed time 0.51 seconds
Run Code Online (Sandbox Code Playgroud)

C#

int [,]matrix = new int[4000,4000];
//Just for mimic same behaviour
for(int i=0;i<4000;i++)
   for(int j=0;j<4000;j++)
       matrix[i,j] = 0;
# Elapsed time 0.096 seconds
Run Code Online (Sandbox Code Playgroud)

我做错了吗?好像差别太大了。

rai*_*iph 19

初步直接比较

我将从与您的 Python 代码比您自己的翻译更接近的代码开始。我认为与您的 Python 最直接等效的 Raku 代码是:

my \table = [ [ 0 for ^4000 ] for ^4000 ];
say table[3999;3999]; # 0
Run Code Online (Sandbox Code Playgroud)

此代码声明了无印记标识符1。它:

  • 删除“塑造”([4000;4000]in my @table[4000;4000])。我放弃它是因为你的 Python 代码没有这样做。整形具有优势,但对性能有影响。2

  • 使用binding而不是assignment。我切换到绑定是因为您的 Python 代码正在执行绑定,而不是赋值。(Python 不区分这两者。)虽然 Raku 的赋值方法为通用代码带来了值得拥有的基本优势,但它对性能有影响。3


我开始回答的这段代码仍然很慢。

首先,2018 年 12 月通过 Rakudo 编译器运行的 Raku 代码比使用 2019 年 6 月的 Python 解释器在相同硬件上的 Python 代码慢约 5 倍。3

其次,Raku 代码和 Python 代码都很慢,例如与您的 C# 代码相比。我们可以做得更好...

速度快一千倍的惯用替代方案

以下代码值得考虑:

my \table = [ [ 0 xx Inf ] xx Inf ];
say table[ 100_000; 100_000 ]; # 0
Run Code Online (Sandbox Code Playgroud)

尽管此代码对应于名义上的100 亿个元素数组,而不是Python 和 C# 代码中仅有的1600 万个元素,但运行它的挂钟时间不到 Python 代码的一半,仅比 C# 慢 5 倍代码。这表明 Rakudo 运行 Raku 代码的速度是等效 Python 代码的一千多倍,是 C# 代码的一百倍。

因为表被初始化的乐代码显得是那么的快很多懒洋洋地使用xx Inf4唯一重要的工作是运行say生产线。这会导致创建 100,000 个第一维数组,然后仅用 100,000 个元素填充第 100,000 个第二维数组,以便say可以显示该0数组的最后一个元素中的保持。

有不止一种方法可以做到

您的问题背后的一个问题是,总是有不止一种方法可以做到这一点。5如果在速度至关重要的代码中遇到性能不佳的情况,像我所做的那样以不同的方式对其进行编码可能会产生巨大的差异。6

(另一个非常好的选择是问一个 SO 问题......)

未来

乐都经过精心设计,以高度optimiz,即能够1天运行得更快给予足够的编译工作,未来数年,比譬如Perl 5或Python 3就可以了,在理论上不断运行,除非他们经过地面重新设计和多年相应的编译器工作。

过去 25 年来 Java 的性能发生了什么事,这有点可以类比。Rakudo/NQP/MoarVM 大约完成了 Java 堆栈所经历的成熟过程的一半。

脚注

1我本来可以写的my $table := ...。但是形式的声明my \foo ...消除了对印记的考虑,并允许使用=而不是使用印记:=标识符所需要的。(作为奖励,“削减印记”会产生无印记的标识符,许多不使用印记的语言(当然包括 Python 和 C#)的编码人员都很熟悉。)

2整形可能有一天会为某些代码带来更快的数组操作。与此同时,正如在对您的问题的评论中所提到的,它目前显然相反,显着减慢了速度。我想这在很大程度上是因为目前每个数组访问都被天真地动态边界检查,慢慢地一切都结束了,而且也没有努力使用固定大小来帮助加快速度。此外,当我试图为您的代码提出一种快速解决方法时,由于当前未实现对固定大小数组的许多操作,我未能找到使用固定大小数组的方法。同样,这些有望在某一天实施,但到目前为止,对于任何致力于实施它们的人来说,这可能不是一个足够的痛点。

3在撰写本文时,TIO使用的是 2019 年 6 月的 Python 3.7.4 和 2018 年 12 月的 Rakudo v2018.12。Rakudo 的性能目前随着时间的推移比官方 Python 3 解释器显着提高,所以我会预计最新的 Rakudo 和最新的 Python 之间的差距(当 Rakudo 较慢时)比本答案中所述的要小得多。特别是,当前的工作显着提高了任务的绩效。

4 xx默认为延迟处理,但由于语言语义或当前编译器的限制,某些表达式会强制进行急切求值。在该岁v2018.12 Rakudo,对形式的表达[ [ foo xx bar ] xx baz ],以保持懒惰和不被强迫急切地评估, barbaz必须Inf。相比之下,my \table = [0 xx 100_000 for ^100_000]懒惰没有使用Inf. (后面的代码实际上Seq是在第一维中存储 100,000秒而不是 100,000Array秒——say WHAT table[0]显示Seq而不是Array——但大多数代码将无法发现差异——say table[99_999;99_999]仍将显示0。)

5有些人认为接受不止一种方式来思考和编写给定问题的解决方案是一种弱点。实际上,它至少在三个方面具有优势。首先,一般来说,任何给定的非平凡问题都可以通过许多不同的算法来解决,这些算法在性能方面存在巨大差异。这个答案包括一个已经存在一年的 Rakudo 已经可用的方法,在某些情况下,它在实践中比 Python 快一千多倍。其次,像 Raku 这样故意灵活和多范式的语言允许编码员(或编码员团队)表达他们认为优雅和可维护的解决方案,或者只是完成工作,基于他们的认为是最好的,而不是语言强加的。第三,乐堂作为优化编译器的性能目前显着变化。幸运的是,它有一个很棒的分析器6,因此可以看到瓶颈在哪里,并且具有很大的灵活性,因此可以尝试替代编码,这可能会产生更快的代码。

6当性能很重要时,或者如果您正在调查性能问题,请查阅Raku 文档关于性能的页面;该页面涵盖了一系列选项,包括使用 Rakudo 分析器。