`now` 在 1000 万次迭代循环中变慢

`now` becomes slow in a 10 million iterations loop

我有一个SnowFlake script for Python, and I convert it to a Raku模块,调用了10,000,000次,速度很慢(文件test.raku):

use IdWorker;

my $worker = IdWorker.new(worker_id => 10, sequence => 0);
my @ids = gather for (1...10000000) { take $worker.get_id() };

my $duration = now - INIT now;
say sprintf("%-8s %-8s %-20s", @ids.elems, Set(@ids).elems, $duration);

正如@codesections 的回答所说,now 花费了很多时间。

Python 大约需要 12 秒,而 Raku 需要几分钟。我该如何解决这个问题?

这个空 for 循环大约需要 0.12 秒:

for (1...10000000) {
    ;
}

并且 $worker 上的呼叫 get_id() 需要 分钟:

for (1...10000000) {
    $worker.get_id();
}

我认为这里的问题不是来自构建数组,而是来自 now 本身——这似乎出奇地慢。

例如,这段代码:

no worries; # skip printing warning for useless `now`
for ^10_000_000 { now }
say now - INIT now;

也需要几分钟才能到达 运行。这让我觉得这是一个错误,我将打开一个问题 [编辑:我在这个问题上找到了 rakudo/rakudo#3620。好消息是已经有了修复计划。] 由于您的代码在每次迭代中多次调用 now,这个问题对您的循环的影响更大。

除此之外,您还可以在其他几个方面加快此代码的速度:

首先,使用隐式 return(即将 return new_id; 更改为 new_id,并对其他使用 return 的地方进行类似更改)一般略faster/letsJIT优化好一点。

二、线路

my @ids = gather for (1...10000000) { take $worker.get_id() };

使用 gather/take 有点浪费(它增加了对惰性列表的支持,只是一个更复杂的结构)。您可以将其简化为

my @ids = (1...10000000).map: { $worker.get_id() };

(不过这仍然构建了一个中间 Seq。)

第三——虽然从代码更改的角度来看尽可能小,但从性能影响来看这一个更重要——是将 (1...10000000) 更改为 (1..10000000)。区别在于 ...sequence operator while .. is the range 运算符。与范围相比,序列具有一些超能力(如果您好奇,请参阅文档),但在这样的循环中迭代要慢得多。

同样,这些都是小问题;我认为now的性能是最大的问题。

长期 now 缓慢的解决方案是修复它(我们正在努力!)不过,作为临时解决方法,如果您不介意使用比通常建议的用户代码略低的级别,您可以使用 nqp::time_n 获取当前时间的浮点秒数。使用它会使您的 get_timestamp 方法看起来像:

method get_timestamp() {
    use nqp;
    (nqp::time_n() * 1000).Int;
}

通过这个解决方法和我上面建议的其他重构,你的代码现在在我的机器上执行大约 55 秒——仍然没有我希望的 Raku 快,但好一个数量级以上比我们开始的地方。