【问题标题】:Does julia perform code monomorphization for recursively polymorphic types?Julia 是否对递归多态类型执行代码单态化?
【发布时间】:2022-01-25 11:53:27
【问题描述】:

我注意到在执行代码单态化的语言(例如:C++、Rust 等)中实现多态递归类型是非常困难的,如果不是不可能的话。这通常是因为编译器需要为每个可能的类型实例化生成代码,这通常会导致无限递归。

支持这一点的语言通常使用类型擦除。编译器不会尝试实例化下一个递归调用,因为它已经知道类型的布局。

Julia 执行代码单态化,但它支持多态递归。我的猜测是,它通过延迟实例化泛型类型或函数直到它被实际调用来做到这一点。但是,我认为这最终可能会使用大量内存,尤其是在递归非常深的情况下。所以我的问题是,julia 是否仍会为多态递归类型执行代码单态化,还是会退回到类型擦除或其他方法?

【问题讨论】:

  • 我认为单态化等同于具体类型和静态分派的方法特化,但我对什么是多态递归类型一无所知,因为我无法真正阅读我找到的 Haskell 示例。多态递归类型的方法在 Julia 中是什么样的?最近的这篇文章是一个例子吗? stackoverflow.com/questions/70408982/…
  • 每个节点都来自一种类型的二叉树,该类型可以是叶子或树对(可以是叶子或树对(可以是......))是递归多态类型?如果是这样,Julia 只会为它看到的类型生成代码。如果你有一个 tree walker,Julia 会首先生成 pair case。它将使用该代码下降到树中,直到遇到叶子,并在此时为叶子案例生成代码。还要记住,多态性与 Julia 中看起来并不完全一样。它只是对有助于函数调度的类型的约束。
  • stackoverflow.com/questions/70408982/… 中的 Branch{T} 的答案对您有帮助吗?

标签: recursion julia polymorphism monomorphism


【解决方案1】:

这个问题看起来与Reducing JIT time with recursive calls in Julia非常相似

为了回答这个问题,我将修改、更正并详细说明那里给出的代码。

首先是一些定义:

abstract type BiTree{T} end

struct Branch{T} <: BiTree{T} 
    left::BiTree{T}
    right::BiTree{T}
end

struct Leaf{T} <: BiTree{T}
    value::T
end

Base.foldl(f, b::Branch) = f(foldl(f, b.left), foldl(f, b.right))
Base.foldl(f, l::Leaf) = f(l.value)


# fancy and concise constructor operations
(⊕)(l::Branch, r::Branch) = Branch(l, r) # just for illustration
(⊕)(l, r::Branch) = Branch(Leaf(l), r)
(⊕)(l::Branch, r) = Branch(l, Leaf(r))
(⊕)(l, r) = Branch(Leaf(l), Leaf(r))

这里我们有一个抽象类型和两个子类型,一个组合用于树中的内部节点,一个组合用于叶子。我们还有一个两行的递归操作来定义如何折叠或减少树中的值,以及一个简洁的树中缀构造函数。

如果我们定义my_tree,然后用加法折叠它,我们会得到:

julia> my_tree = ((((6 ⊕ 7) ⊕ (6 ⊕ 7)) ⊕ ((7 ⊕ 7) ⊕ (0 ⊕ 7))) ⊕ (((8 ⊕ 7) ⊕ (7 ⊕ 7)) ⊕ ((8 ⊕ 8) ⊕ (8 ⊕ 0)))) ⊕ ((((2 ⊕ 4) ⊕ 7) ⊕ (6 ⊕ (0 ⊕ 5))) ⊕ (((6 ⊕ 8) ⊕ (2 ⊕ 8)) ⊕ ((2 ⊕ 1) ⊕ (4 ⊕ 5))));

julia> typeof(my_tree)
Branch{Int64}

julia> foldl(+, my_tree)
160

请注意,my_tree 的类型完全暴露了它是具有某种子节点的内部节点,但我们无法真正看到它有多深。我们没有像Branch{Branch{Leaf{Int32}, Branch{... 这样的类型。 Branch{Int64}BiTree{Int64} 的事实是可见的使用

julia> isa(my_tree, BiTree{Int64})
true

但仅从 my_tree 的值看不出来,在类型中看不到深度。

如果我们将生成的方法视为迄今为止我们工作的副作用,我们会看到这一点

julia> using MethodAnalysis

julia> methodinstances(⊕)
4-element Vector{Core.MethodInstance}:
 MethodInstance for ⊕(::Branch{Int64}, ::Branch{Int64})
 MethodInstance for ⊕(::Int64, ::Branch{Int64})
 MethodInstance for ⊕(::Branch{Int64}, ::Int64)
 MethodInstance for ⊕(::Int64, ::Int64)

julia> methodinstances(foldl)
3-element Vector{Core.MethodInstance}:
 MethodInstance for foldl(::typeof(+), ::Branch{Int64})
 MethodInstance for foldl(::typeof(+), ::Leaf{Int64})
 MethodInstance for foldl(::typeof(+), ::BiTree{Int64})

无论我们尝试构建什么 32 位整数树,这就是我们所需要的。无论我们尝试使用+ 减少什么树,这就是我们所需要的。

如果我们尝试使用不同的操作符折叠,我们可以获得更多的方法

julia> foldl(max, my_tree)
8

julia> methodinstances(foldl)
6-element Vector{Core.MethodInstance}:
 MethodInstance for foldl(::typeof(+), ::Branch{Int64})
 MethodInstance for foldl(::typeof(max), ::Branch{Int64})
 MethodInstance for foldl(::typeof(+), ::Leaf{Int64})
 MethodInstance for foldl(::typeof(max), ::Leaf{Int64})
 MethodInstance for foldl(::typeof(+), ::BiTree{Int64})
 MethodInstance for foldl(::typeof(max), ::BiTree{Int64})

这里有趣的是方法的数量增加了,但并没有爆炸。

【讨论】:

  • 澄清一下,BiTree{T} 是参数化的abstract 类型,struct 字段中的注解left::BiTree{T} 与方法中的作用不同争论。默认情况下,方法将在编译时专门化并为 each 适合的具体类型分派(单态化?)。然而,结构将存储一个可以表示 每个 拟合类型的引用,并且将在运行时检查该字段的具体类型以执行操作。运行时检查听起来很糟糕,但这意味着同一具体类型 Branch{T} 可以指向 BiTree{T} 的任何子类型。
  • 为了扩展您的(重要)说明,为特定的具体类型生成代码,声明为持有抽象类型的变量将实际上持有一些具体类型(我们将在运行时知道该类型)。方法分派基于运行时最具体的匹配,而不是我们在代码中声明的内容。特定于类型的代码生成和运行时对象的类型特定性之间的这种对称性是关键,其好处很大,但很微妙。
猜你喜欢
  • 2014-08-15
  • 1970-01-01
  • 2016-10-27
  • 2015-03-20
  • 1970-01-01
  • 1970-01-01
  • 2023-03-03
  • 1970-01-01
  • 2014-08-13
相关资源
最近更新 更多