Julia:抽象类型与类型联合

Julia: Abstract types vs Union of types

我对 Julia 很陌生,在尝试做某些事情时,我仍然对哪种风格更好有一些疑问...例如,我对使用抽象类型的性能或风格差异有很多疑问与定义联合。

举个例子:假设我们要实现几种类型的单位(Mob、Knight 等),它们应该共享大部分(如果不是全部)属性和大部分(如果不是全部)方法。 我看到提供结构的两个选项:首先,可以声明一个抽象类型 AbstractUnit,其他类型从中派生,然后为抽象类型创建方法。它看起来像这样:

abstract type AbstractUnit end

mutable struct Knight <: AbstractUnit
    id     :: Int
    [...]
end

mutable struct Peasant <: AbstractUnit
    id     :: Int
    [...]
end

id(u::T)      where T <: AbstractUnit = u.id
[...]

或者,可以定义类型的联合并为该联合创建方法。它看起来像这样:

mutable struct Knight
    id     :: Int
    [...]
end

mutable struct Peasant
    id     :: Int
    [...]
end

const Unit = Union{Knight,Peasant}

id(u::Unit) = u.id
[...]

我理解这两种方法之间的一些概念差异,并且认为第一种方法更具扩展性。但是,我对性能有很多疑问。例如,就运行时的内存分配而言,创建 AbstractUnit 数组与联合类型数组会有多糟糕?

谢谢!

绝对在方法的参数类型注释中使用 AbstractUnit 而不是 Unit。实际上,联合也是抽象类型,但正如您所指出的,您不能向其添加新类型。在任何一种情况下,该方法都被编译为每个具体类型的专门化,例如 KnightPeasant,因此您的方法的性能不会有所不同。

对于Arrays的元素类型参数,有isbits Union optimization,但顾名思义,它只有在你的Union中的所有类型都是isbitstype时才有效(没有指针,不可变) .你的结构是可变的,所以这已经不适用了。看,当实例直接存储在数组中时,内存访问速度更快,并且元素类型参数(Vector{T} 中的 T)必须是具体的 isbitstype 才能允许这样做。当元素类型参数是抽象的或可变的时,通常 数组仅直接存储指向实际实例的指针,因为多个或可变的具体类型可能具有未知且可变的内存大小。如果抽象类型是 isbits Union,则实例可以直接存储在 Array 中:每个元素分配足够的内存以包含 Union 中最大的具体类型以及指定其类型的每个元素的标记字节.一个字节只有 256 个值,所以大概这只适用于最多 256 个具体类型的联合。

Vector{AbstractUnit} 上使用 Vector{Unit} 的另一种可能的优化是 Union-splitting 类型不稳定性。我真的无法制作一个比链接的博客更好地解释它的示例方法,所以我只提供简短的版本。当 Julia 的编译器根本无法推断方法中变量的类型时(@code_warntype 中的 ::Any 注释),涉及变量的内部方法调用必须在 运行-time,这可能会花费大量时间。但是,如果 Julia 的编译器可以将变量推断为几个具体类型的联合(实际上,最多 4 个),则可以使用每种类型的条件分支来消除大多数类型检查并在编译时进行分派。 Vector{AbstractUnit} 可以包含任意数量的类型 <: AbstractUnit,因此编译器无法使用联合拆分。 Vector{Unit} 但是让编译器知道元素必须是 KnightPeasant,这允许联合拆分。

P.S。这通常是初学者混淆的根源,但是 Unit 是抽象类型,而 Vector{Unit} 是具体类型,只是具有抽象类型 参数 。毕竟,它可以有实例,所有数组直接包含指向 KnightPeasant 实例的指针。