【问题标题】:Where are these allocations coming from and how does declaring the parameters' types prevent them?这些分配来自哪里,声明参数的类型如何阻止它们?
【发布时间】:2022-02-02 02:32:25
【问题描述】:

所以我通过解决 ProjectEuler 问题来学习 Julia,我为problem 27 想出了这段代码:

function isPrime(num)
    num < 2 && return false
    for i in 2:trunc(Int, √num)
        if num % i == 0
            return false
        end
    end
    return true
end

function quadratic(n, a, b)
    return n^2 + a*n + b
end

function consecutivePrimes(a, b)
    count = 0
    tryNum = 0
    while true
        currResult = quadratic(tryNum, a, b)
        !isPrime(currResult) && (return count)
        count += 1
        tryNum += 1
    end
end

function longestConsecutivePrimes(modA, modB)
    longest = (0,0,0)
    for a in -modA:modA, b in -modB:modB
        consecutive = consecutivePrimes(a, b)
        longest[1] < consecutive && (longest = (consecutive, a, b))
    end
    return longest
end

A, B = 999, 1000
@time size, a, b = longestConsecutivePrimes(A, B)
println("size: $size, a: $a, b: $b")
println("a*b = $(a*b)")

产量

  0.106938 seconds (36.28 k allocations: 2.239 MiB, 38.39% compilation time)
size: 71, a: -61, b: 971
a*b = -59231

现在,通过将函数longestConsecutivePrimes中的参数替换为

function longestConsecutivePrimes(modA::Int64, modB::Int64)

脚本在独立于前一个终端的单独终端中运行时,会产生

  0.064875 seconds (1 allocation: 16 bytes)
size: 71, a: -61, b: 971
a*b = -59231

它的速度是原来的两倍,并且只有 1 次分配。

每个脚本运行都是在 linux 终端中进行的,而不是在 Julia REPL 中,每次运行之间都会关闭终端,因此一次运行不会从另一次的编译中“获利”。

分配是从哪里来的?声明参数的类型如何防止分配?

我觉得理解这一点将有助于我改进我以前和未来编写的 Julia 脚本。

【问题讨论】:

标签: optimization julia allocation


【解决方案1】:

在这里,当 AB 不再是全局变量时,分配也会消失,因此差异似乎可能在于类型推断更加困难(即来自类型推断代码本身的分配)。

将脚本的末尾保留在函数之外是否被认为是不好的做法?将它包装在一个打印结果和基准的函数中会更朱利安吗?

这里特别重要的是avoid (non-constant) global variables。在全局范围内使用简单的 print 语句(或带有 setup 代码的 @benchmark 行)很好,但任何更复杂的东西都可能需要非常量变量,因此应该按照手册的建议放入函数中。 (另请参阅同一手册页中的“注释取自非类型化位置的值”部分,这是您的第二个脚本正在执行的操作。)


编辑:我会保留它以防将来对其他人有用,但这种解释不适用于 cmets 中提到的 OP 的情况。

第一次@timed 函数时,由于使用两个Int 参数调用它,Julia 自动为modAmodB 编译了该函数的专用版本,即Ints。 @time 宏在其结果中也包含了此编译步骤所花费的分配和时间(从输出的“38.39% 编译时间”部分可以看出)。

当您将函数更改为具有类型注释并再次运行它时,Julia 不必编译任何内容,因为已经存在带有两个 Int 参数的 longestConsecutivePrimes 的编译版本。所以在这次运行中,只有函数本身的执行被计时,只有它自己的分配被报告。

所以添加类型注释不会改变分配或运行时间。然而,它所做的是向longestConsecutivePrimes 函数添加了一个新的“方法”——一个具有更专业参数的方法。因此,在此之后,任何纯 Int 参数的函数调用都将使用您的第二个定义,而使用其他类型的调用 - UInt8Int128 甚至 String 例如。 - 将使用您对函数的第一个定义。它是为Any 类型有效定义的。当然,在这种情况下,两个定义恰好是相同的。但总的来说,这种类型特化函数的方式是使用 Julia 的多分派范式的核心。

【讨论】:

  • 我在 Oscar 的回答中回复了您的评论,并编辑了问题以使其更清楚。运行不是在 REPL 中进行的,而是在 linux 终端中进行的,在每次运行之间关闭终端。
  • 另外,如果我先在终端中运行带有类型的脚本,然后在同一个终端中运行没有类型的版本而不关闭它,产生的结果与问题中所示的相同.
  • 啊,我想这种情况可能是因为AB 是全局变量。我以前经历过类似的行为。我稍后会更新答案。
  • 是的,这是它们成为全局变量的产物,尽管我没有专业知识来确切说明为什么会发生这种情况。如果您将最后 4 行放在 main 函数中,然后在脚本的最后一行中调用它,就会突然没有分配,甚至没有此处带有类型注释的一个 16 字节分配。
  • @code_warntype 的输出在所有三种情况下都是相同的。我想这是导致这些分配和编译时间的努力工作的类型推断?将事物放入函数中(本地化范围)或自己注释它们会使其工作变得更加容易,因此作为差异的来源是有意义的。
【解决方案2】:

你正在计时编译。如果再次运行 untyped 函数,您将看到它在没有额外分配的情况下运行。

【讨论】:

  • 我还要提到 BenchmarkTools.jl 中的 @benchmark,因为这对这里有很大帮助。
  • 我知道我在两个版本的代码中都在计时编译。尽管如此,当第一次运行第一个版本时,它在编译时显示 36.28 k 分配。第二个版本(带有类型声明),第一次运行时,只有一个分配。两个@time 输出都是从不同脚本的函数调用的第一次运行中产生的。
  • @MateusRodolfo 但两者都是从同一个 Julia REPL 会话中运行的,对吧?另外,请参阅我的答案以获取有关正在发生的事情的更多详细信息。
  • @SundarR 实际上没有。我将编辑问题以使其更清楚。两个版本的脚本都在 linux 终端中运行,在每个脚本运行之间关闭终端。
猜你喜欢
  • 2012-05-30
  • 2012-03-07
  • 1970-01-01
  • 1970-01-01
  • 2017-01-09
  • 2020-12-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多