【问题标题】:Fast single dispatch to get around multiple dispatch at runtime快速单次分派以在运行时绕过多个分派
【发布时间】:2022-01-02 20:59:25
【问题描述】:

当类型推断失败时(@code_warntype 打印输出中的::Any),我的理解是函数调用是动态调度的。换句话说,在运行时,会检查参数的类型以找到具体参数类型的特化 (MethodInstance)。需要在运行时而不是编译时执行此操作会导致性能成本。

(编辑:本来,我在类型检查和专业化查找之间说“多次调度找到合适的方法”,但我实际上不知道这部分是否发生在运行时。似乎只需要发生如果不存在有效的特化并且需要编译。)

在只需要检查一个参数的具体类型的情况下,是否可以进行更快的动态单次调度,例如在某种专业化查找表中?我只是找不到访问和调用MethodInstances 的方法,就好像它们是函数一样。

当谈到改变调度或专业化时,我想到了invoke@nospecializeinvoke 看起来可能会直接跳到指定的方法,但仍然必须检查多个参数类型和特化。 @nospecialize 不会跳过调度过程的任何部分,只会产生不同的特化。

编辑:一个带有 cmets 的最小示例,希望能描述我在说什么。

struct Foo end
struct Bar end

#   want to dispatch only on 1st argument
#          still want to specialize on 2nd argument
baz(::Foo, ::Integer) = 1
baz(::Foo, ::AbstractFloat) = 1.0
baz(::Bar, ::Integer) = 1im
baz(::Bar, ::AbstractFloat) = 1.0im

x = Any[Foo(), Bar(), Foo()]

# run test1(x, 1) or test1(x, 1.0)
function test1(x, second)
  #   first::Any in @code_warntype printout
  for first in x
    # first::Any requires dynamic dispatch of baz
    println(baz(first, second))
    # Is it possible to only dispatch -baz- on -first- given
    # the concrete types of the other arguments -second-?
  end
end

【问题讨论】:

  • 一种方法是将其余参数放入关键字参数中,因为这些参数不参与 MD,但也许举个例子会有所帮助
  • 我会写一个例子来运行,但我无法演示这么多的内部工作原理。
  • 可能不是您要查找的内容,但您可以使用明确的 if 分支根据第一个参数“调度”作为解决方法。这会很快。
  • @carstenbauer 1) 是的,在我的示例中检查 first 的具体类型的冗余 if-elseif 导致分支中的静态调度。尚未检查这是否可以扩展到更多类型或达到联合拆分限制之类的东西。 2) ManualDispatch.jl 似乎简化了 if-elseif 检查并且必须检查所有类型。检查线性 if-elseif 中的多种类型似乎没有最佳扩展,但分支静态调度可能是值得的。 3)Unityper.jl很酷,但是它把类型合并为1,所以我好像不能写一个多方法,只是每个分支的功能不同。

标签: dynamic julia single-dispatch


【解决方案1】:

按照您的要求进行操作的最简单方法是简单地在第二个参数上分派(通过不对第二个变量指定足以触发分派的类型断言),而是专门使用if 函数中的语句。例如:

struct Foo end
struct Bar end

# Note lack of type assertion on second variable. 
# We could also write `baz(::Foo, n::Number)` for same effect in this case, 
# but type annotations have no performance benefit in Julia if you're not 
# dispatching on them anyways.
function baz(::Foo, n) 
    if isa(n, Integer)
        1
    elseif isa(n, AbstractFloat)
        1.0
    else
        error("unsupported type")
    end
end

function baz(::Bar, n)
    if isa(n, Integer)
        1im
    elseif isa(n, AbstractFloat)
        1.0im
    else
        error("unsupported type")
    end
end

现在,这将做你想做的事

julia> x = Any[Foo(), Bar(), Foo()]
3-element Vector{Any}:
 Foo()
 Bar()
 Foo()

julia> test1(x, 1)
1
0 + 1im
1

julia> test1(x, 1.0)
1.0
0.0 + 1.0im
1.0

并且由于这有效地从所有可能需要专门研究的类型中手动选择了两个案例来专门化,我可以想象这种技术具有性能优势的场景(当然,它在 Julia 中不言而喻,如果可能的话,最好首先找到并消除类型不稳定性的根源)。

然而,在这个问题的上下文中,重要的是要指出即使我们已经消除了函数的 second 参数的调度,如果 first 参数(即,您 调度的参数)类型不稳定,则这些 baz 函数的性能可能仍然很差——就像问题中的情况一样因为使用了Array{Any}而写的。

请尝试使用至少具有 some 类型约束的数组。例如:

julia> function test2(x, second)
           s = 1+1im
           for first in x
               s += baz(first, second)
           end
           s
       end
test2 (generic function with 1 method)

julia> using BenchmarkTools

julia> x = Any[Foo(), Bar(), Foo()];

julia> @benchmark test2($x, 1)
BenchmarkTools.Trial: 10000 samples with 998 evaluations.
 Range (min … max):  13.845 ns … 71.554 ns  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     13.869 ns              ┊ GC (median):    0.00%
 Time  (mean ± σ):   15.397 ns ±  3.821 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%

  █▅  ▃ ▄  ▄      ▄       ▄                                 ▃ ▁
  ██▇▆█▇██▄█▇▇▄▃▁▁██▁▃▃▁▁▃██▃▁▃▁▁▄▃▃▃▆▆▅▆▆▅▅▄▁▁▄▃▃▃▁▃▁▄▁▁▃▄▄█ █
  13.8 ns      Histogram: log(frequency) by time      30.2 ns <

 Memory estimate: 0 bytes, allocs estimate: 0.

julia> x = Union{Foo,Bar}[Foo(), Bar(), Foo()];

julia> @benchmark test2($x, 1)
BenchmarkTools.Trial: 10000 samples with 1000 evaluations.
 Range (min … max):  4.654 ns … 62.311 ns  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     4.707 ns              ┊ GC (median):    0.00%
 Time  (mean ± σ):   5.471 ns ±  1.714 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%

  █▂▂▃▄ ▃  ▄▁    ▄▂      ▅▁                               ▁▄ ▁
  ███████▁▁██▁▁▁▁██▁▁▁▁▁▁██▁▁▁▄▁▃▁▃▁▁▁▁▃▁▁▁▁▃▁▃▃▁▁▁▁▃▁▁▁▁▁██ █
  4.65 ns      Histogram: log(frequency) by time     10.2 ns <

 Memory estimate: 0 bytes, allocs estimate: 0.

【讨论】:

  • 1) 结合第二个参数的方法会减少方法的数量,但不会减少特化的数量(baz(::Foo, n) 仍然编译 baz(::Foo,::Int)baz(::Foo,::Float64))。我不知道如何分析编译的调度部分,但在您的方法中,仍然必须在运行时检查所有具体类型以找到正确的专业化。 @nospecialize 会减少专业化,但我确实希望在方法中推断出第二个参数的类型。 2)类型约束使用(小)联合拆分优化,这很有用,但我想像更多类型。
  • 如果您既不想在第二个参数上调度也不让编译器专门处理第二个参数,那么只需在上面的函数中添加@nospecialize。不过,您提出问题的方式听起来像是您想避免在第二个参数上发送,而不是专门化。
  • 如果您要求的内容与我的回答完全不同,我怀疑这是否会带来性能优势,但您可能需要更清楚地表达您的问题。
  • 措辞是准确的,我想避免运行时调度第二个参数,但仍然专注于它。也许如果我这样说:如果我将第二个参数类型固定为Int,理论上我可以只检查第一个参数类型以找到正确的专业化。我读到的是,总是通过检查所有参数类型来找到特化,就好像它们中的一个元组是某个哈希表中的一个键一样;这个实现不能通过重写方法来改变。不确定,因为我无法在编译和运行时追踪关于调度的实现级细节的完整来源。
猜你喜欢
  • 2019-07-12
  • 2019-03-26
  • 1970-01-01
  • 2015-05-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-10-11
相关资源
最近更新 更多