4

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

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

但是,当我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)))::Tuple{T1,T2}
      SSAValue(2) = $(Expr(:invoke, MethodInstance for rand(::Array{AT,1}), :(Main.rand), :($(Expr(:invoke, MethodInstance for copy!(::Array{AT,1}, ::Tuple{T1,T2}), :(Base.copy!), :($(Expr(:foreigncall, :(:jl_alloc_array_1d), Array{AT,1}, svec(Any, Int64), Array{AT,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是有道理的——不会有任何方法可以按照我想象的方式调度。所以我尝试重新定义,以便它为每个子类型生成一个特定的方法,如下所示:ATgetX

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

知道我怎样才能让它工作吗?

4

2 回答 2

3

如果您定义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)))::Tuple{T1,T2}
      return (Main.getX)($(Expr(:invoke, MethodInstance for rand(::Array{AT,1}), :(Main.rand), :($(Expr(:invoke, MethodInstance for copy!(::Array{AT,1}, ::Tuple{T1,T2}), :(Base.copy!), :($(Expr(:foreigncall, :(:jl_alloc_array_1d), Array{AT,1}, svec(Any, Int64), Array{AT,1}, 0, 2, 0))), SSAValue(0)))))))::Int64
  end::Int64

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

于 2018-03-30T10:16:07.837 回答
0

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


我将julia' 的类型调度机制与C++' 的模板实例化混为一谈。我想我错误地认为它为每个调用它julia定义了一个新版本, 就像 C++ 模板机制一样。getXT

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

我期待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)))::Tuple{T1,T2
}
      return (Main.getX)($(Expr(:invoke, MethodInstance for rand(::Array{AT,1}), :(Main.rand), :($(Expr(:invoke, MethodInstance for copy!(::Array{AT,1}, ::Tuple{T1,T2}), :(Base.copy!), :($(Expr(:foreigncall, :(:jl_alloc_array_1d), Array{AT,1}, svec(Any, Int64), Array{AT,1}, 0, 2, 0))), SSAValue(0)))))))::Int64
  end::Int64

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

于 2018-03-29T17:20:43.337 回答