从抽象类型访问字段时,julia 类型不稳定

Posted

技术标签:

【中文标题】从抽象类型访问字段时,julia 类型不稳定【英文标题】:julia type unstable when accessing field from abstract type 【发布时间】:2018-09-08 18:05:24 【问题描述】:

我有一个关于访问字段时类型不稳定的问题 abstract type(在 julia v0.6 中)。

假设我有一个类型层次结构,所有这些都共享同一个实例变量。 我知道它既类型不稳定又不能保证正确访问 该字段正确,因为有人总是可以定义一个缺少的新子类型 预期的变量。但是,即使将成员访问权包装在 功能,访问仍然类型不稳定,我不知道为什么。

假设我们有一个简单的类型层次结构:

julia> begin
       abstract type AT end
       mutable struct T1 <: AT
         x::Int
       end
       mutable struct T2 <: AT
         x::Int
       end
       end

我们不直接访问a.x,而是将其包装在一个函数屏障中:

julia> getX(a::AT)::Int = a.x
>> getX (generic function with 1 method)

julia> @code_warntype getX(T1(1))
Variables:
  #self# <optimized out>
  a::T1

Body:
  begin
      return (Core.getfield)(a::T1, :x)::Int64
  end::Int64

请注意,通过此方法的访问是类型稳定的,因为它可以推断 a 的类型为 T1

但是,当我在编译器无法知道类型的上下文中使用 getX 时 变量提前,它仍然是类型不稳定的:

julia> foo() = getX(rand([T1(1),T2(2)]))
>> foo (generic function with 1 method)

julia> @code_warntype foo()
Variables:
  #self# <optimized out>
  T <optimized out>

Body:
  begin
      SSAValue(0) = (Core.tuple)($(Expr(:new, :(Main.T1), 1)), $(Expr(:new, :(Main.T2), 2)))::TupleT1,T2
      SSAValue(2) = $(Expr(:invoke, MethodInstance for rand(::ArrayAT,1), :(Main.rand), :($(Expr(:invoke, MethodInstance for copy!(::ArrayAT,1, ::TupleT1,T2), :(Base.copy!), :($(Expr(:foreigncall, :(:jl_alloc_array_1d), ArrayAT,1, svec(Any, Int64), ArrayAT,1, 0, 2, 0))), SSAValue(0))))))
      return (Core.typeassert)((Base.convert)(Main.Int, (Core.getfield)(SSAValue(2), :x)::Any)::Any, Main.Int)::Int64
  end::Int64

请注意,它内联了 getX 的主体,并将其替换为本质上 tmp.x::Int64。这让我感到惊讶,因为我期待 getX 发送到 我们在上面看到的相同定义的两个实例之一,其中没有断言 是必需的,因为类型是已知的。


我认为如果 getX实际上被定义,这确实是有道理的 对于抽象基类型AT——将没有方法可以分派到 我想象的方式。所以我尝试重新定义getX 这样它会生成 每个子类型的具体方法如下:

julia> getX(a::T where T<:AT)::Int = a.x
>> getX (generic function with 1 method)

但这实际上是一个相同的定义,并没有改变:

julia> methods(getX)
>> # 1 method for generic function "getX":
getX(a::AT) in Main at none:1

知道如何让它工作吗?

【问题讨论】:

getX 类型稳定,不需要类型断言。但是rand([T1(1),T2(2)]) 不是类型稳定的。 【参考方案1】:

如果你定义 getX 来获取 AT 的子类型,那么你就有了 foo 的类型稳定代码:

julia> function getX(a::T)::Int where T <: AT
           a.x
       end
getX (generic function with 1 method)

julia> foo() = getX(rand([T1(1),T2(2)]))
foo (generic function with 1 method)

julia> @code_warntype foo()
Variables:
  #self# <optimized out>
  T <optimized out>

Body:
  begin
      SSAValue(0) = (Core.tuple)($(Expr(:new, :(Main.T1), 1)), $(Expr(:new, :(Main.T2), 2)))::TupleT1,T2
      return (Main.getX)($(Expr(:invoke, MethodInstance for rand(::ArrayAT,1), :(Main.rand), :($(Expr(:invoke, MethodInstance for copy!(::ArrayAT,1, ::TupleT1,T2), :(Base.copy!), :($(Expr(:foreigncall, :(:jl_alloc_array_1d), ArrayAT,1, svec(Any, Int64), ArrayAT,1, 0, 2, 0))), SSAValue(0)))))))::Int64
  end::Int64

我不知道为什么会这样,也许对此事有更多了解的人会对此有所了解。 此外,这只是类型稳定的,因为getX 保证返回Int。如果您不强制 getX 执行此操作,则不能保证返回 Int,因为您可能有不同的 AT 子类型包含非 Ints。

【讨论】:

是的,你是对的,这绝对解决了我的问题。它甚至比手动定义每种方法更可靠,每种方法在 5 种以上类型后停止工作......所以基本上我的问题只是我写了getX(a::T where T&lt;:AT)::Int 而不是getX(a::T)::Int where T &lt;: AT......它们之间有什么区别?为什么第二个只适用于function ... end 而不是更简单的f(x) = y 语法? 您可以将其作为单线:getX(a::T) where T &lt;: AT = a.x::Int。至于为什么这个有效而另一个无效,我不是 100% 确定,我不想只是猜测。可能值得参考 Julia slack 或 Discourse 上的问题。 啊,是的,谢谢。我忘记了括号语法。 :)【参考方案2】:

啊,所以我需要自己手动定义getX的不同版本。


我将julia 的类型调度机制与C++ 的模板混为一谈 实例化。我想我错误地想象julia 定义了一个新版本的 getX 对于每个 T 调用它,方式与 C++ 模板相同 机制。

在这种情况下,我说的几乎是正确的

我期待 getX 调度到两个实例之一 [...] 由于类型已知,因此不需要断言。

但是,在这种情况下,实际上并没有两种不同的方法来 派往——只有一个。如果我真的定义这两个不同的 方法,调度机制可以满足类型稳定性:

julia> begin
       abstract type AT end
       mutable struct T1 <: AT
         x::Int
       end
       mutable struct T2 <: AT
         x::Int
       end
       end

julia> getX(a::T1) = a.x
>> getX (generic function with 1 method)

julia> getX(a::T2) = a.x
>> getX (generic function with 2 methods)

julia> foo() = getX(rand([T1(1),T2(2)]))
>> foo (generic function with 1 method)

julia> @code_warntype foo()
Variables:
  #self# <optimized out>
  T <optimized out>

Body:
  begin
      SSAValue(0) = (Core.tuple)($(Expr(:new, :(Main.T1), 1)), $(Expr(:new, :(Main.T2), 2)))::TupleT1,T2

      return (Main.getX)($(Expr(:invoke, MethodInstance for rand(::ArrayAT,1), :(Main.rand), :($(Expr(:invoke, MethodInstance for copy!(::ArrayAT,1, ::TupleT1,T2), :(Base.copy!), :($(Expr(:foreigncall, :(:jl_alloc_array_1d), ArrayAT,1, svec(Any, Int64), ArrayAT,1, 0, 2, 0))), SSAValue(0)))))))::Int64
  end::Int64

我在这里找到了解决此问题的灵感: https://***.com/a/40223936/751061

【讨论】:

虽然,有趣的是,如果getX 的类型+方法的数量达到 5 个或更多,它会再次回到类型不稳定。 :( 也许那里有一些硬编码的值,它可以计算 4 种或更少的类型,但不能超过那个?

以上是关于从抽象类型访问字段时,julia 类型不稳定的主要内容,如果未能解决你的问题,请参考以下文章

Julia 中函数的抽象类型和多次调度

优化中的 Julia 抽象类型?

接口与抽象类比较

如何创建类型化工厂方法构造函数的类层次结构并使用抽象类型从 Scala 访问它们?

何时在抽象类 cpp 中声明受保护字段

007.类构造函数