【问题标题】:Raku list comprehension乐列表理解
【发布时间】:2017-02-22 12:39:16
【问题描述】:

我使用的是具有 8 GB RAM 的 Windows 10 i7 第 4 代笔记本电脑。

我想找出从 1 到 1000000000 可被 5 整除的数字的总和。

我正在尝试在 Raku REPL 中运行此代码:

($_ if $_%5==0 for 1..1000000000).sum

代码运行了 45 分钟,仍然没有输出。我该如何克服它?

在这种情况下如何应用并发?我认为上述问题可以通过并发或任务并行来解决!!

【问题讨论】:

  • 而我得到的答案是 100000000500000000。

标签: list-comprehension raku


【解决方案1】:

我该如何克服它?

通过选择更好的算法:

my \N = 1_000_000_000。您对价值感兴趣

[+] grep * %% 5, 1..N

相同
[+] map * * 5, 1..N/5

反过来

5 * [+] 1..N/5

Rakudo 足够聪明,可以在恒定时间内对一个范围求和,您会(几乎)立即得到结果。

【讨论】:

  • 几乎可以肯定,最快的方法是:5 * (N div 5) * (N div 5 + 1) / 2
【解决方案2】:

这很慢,因为您正在尝试物化大量物品。作为替代方案,您可以构建一个序列:(5, 10 … 1000000000).sum,但这仍然会具体化并保持大量元素,所以它仍然很慢。您可以创建一个 C 风格的循环并为每个增量添加总和,但这并不有趣(对于足够大的数字仍然很慢)。

你可以用数学来解决这个问题:能被 N 整除的数字是 N 的倍数,如果我们从该序列中分解出 N,我们会发现你要查找的所有数字的总和是 N * (1 + 2 + ... + floor 1000000000/N) .由于这是一个连续的范围,我们可以使用它的Range.sum(它知道如何快速完成)来计算该部分。因此我们得到:

sub sum-of-stuff (\n, \d) { d * sum 1..n div d; }
say sum-of-stuff 1000000000, 5 # OUTPUT: 100000000500000000

所以这是计算问题的最快和最明智的方法,但这并不是最有趣的!

您提到了并发性,所以让我们试一试这种方法。我们的问题是物化东西,所以我们需要找到一种方法,通过我们可用的核心数量将我们的原始数字范围分块,然后开始物化和寻找每个单独核心的乘数的工作。然后我们将对每个核心中每个块中的内容进行汇总,然后返回主线程并汇总块的总和以获得最终答案。在代码中,它看起来像这样:

constant N = 10000;
constant DIV = 5;
constant CORES = 32;
constant batch = N div CORES;

(0, batch … N, N).rotor(2 => -1).flat.map({$^a^..$^b})
    .race(:batch :degree(CORES)).map(*.grep(* %% DIV).sum).sum.say;

batch 是每个核心需要处理的块的大小,并且 这是对每一位分解所有工作的单线的解释:

我们使用序列运算符创建0, batch, 2*batch, 3*batch 等序列,直到N。由于我们希望 N 成为其中的一部分,因此我们第二次包含它:

(0, batch … N, N)

我们现在想要的是使用该序列来创建一堆Range 对象,我们想重用序列中的元素,所以我们使用.rotor,批处理为 2,backstep 为 1,然后展平子列表并使用 .map 创建 Range 对象(.map: *^..* 看起来好多了,但可惜的是,Whatever Stars 不想在这种安排中使用咖喱):

.rotor(2 => -1).flat.map({$^a^..$^b})

现在有趣的是,我们使用.race 方法创建一个HyperSeq,以便使用我们所有的内核对其进行评估。它的:batch 命名参数允许您指定每批要处理多少个元素,它的:degree 指定要使用多少个线程。我们已经对我们的数据进行了批处理,所以对于:batch,我们使用1。对于:degree,我们使用我们的核心数量。为什么我们不告诉它批量处理我们的东西?因为物化是我们的敌人,我们想在不同的线程中物化。告诉它批处理将在一个线程中具体化所有内容:

.race(:batch :degree(CORES))

所以现在我们已经掌握了 HyperSeq,我们可以对其进行映射。每个项目都是我们的Range 对象,大小适合一个批次,回想一下。所以我们会在上面调用.grep,寻找能被我们想要的除数整除的数字,然后我们会调用.sum

.map(*.grep(* %% DIV).sum)

顶部的最后一颗樱桃,我们想把每个块的总和相加并打印结果,所以我们调用

.sum.say;

多田!

你也可以用这种方式重写有趣的部分,并使用 Promises 代替 .race

say sum await (0, batch … N, N).rotor(2 => -1).flat.map:
    { start sum grep * %% DIV, $^a^..$^b }

类似但稍短一些。曾经为我们制作 Ranges 的地图现在也触发了一个 Promise(使用 start 关键字),它对块进行 greps 和求和。在行首,我们添加了await,等待所有promise的结果,然后将结果汇总。

它仍然很慢,并且无法解决您最初的问题,因此为什么您应该使用一个好的算法;)

干杯。

【讨论】:

    【解决方案3】:

    低于 1_000_000_000 的值需要多长时间,比如 1_000_000?在我的机器上大约需要 3 秒。从中推断,要检查的值是 1000 倍,这意味着它需要大约 3000 秒。

    为了使事情变得更快,您可以使用 %%(可被整除)运算符,而不是 %(取模):

    ($_ if $_ %% 5 for 1..1000000000).sum
    

    FWIW,我希望这会更快。我会考虑让 %% 更快,但恐怕您仍会查看算法的小时刻度时间。

    【讨论】:

    • FWIW,我刚刚提交了 Int %% Int(快 14 倍)和 Int % Int(快 8 倍)的加速,前提是整数值适合本机(通常为 64 位)整数。跨度>
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-02-25
    • 2011-12-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-10
    • 1970-01-01
    相关资源
    最近更新 更多