<: Any
应该代表我不关心类型。
它类似于 可以是任何类型 (因此编译器对类型没有任何提示)。你也可以这样写:
struct TypeAny
payload::Container
end
这基本上与以下测试结果相同:
julia> Container{<:Any} <: Container
true
julia> Container <: Container{<:Any}
true
这种构造方式可能会导致运行时确定您容器中持有的对象的具体类型,而不是在编译时确定(正如您所怀疑的那样),因此会影响性能。
但请注意,如果将这样的提取出来的对象传递给一个函数,那么在调用函数内部进行动态分派后,代码将运行得很快(因为它将是类型稳定的)。您可以在
这里了解更多信息。
对于位类型,更复杂的事情发生了。如果某个具体的位类型是容器中的字段,则其作为值存储。如果其类型在编译时未知,则将其存储为引用(这将有额外的内存和运行时影响)。
如上所述,运行时之间的差异是由于在编译时该字段的类型未知。如果您更改定义为:
struct TypeAny{T}
payload::Container{T}
end
你说“我不关心类型,但将其存储在参数中”,这样编译器就会知道这个类型。
然后,payload
的类型将在编译时得到确认,所有东西都会更快。
如果以上内容有不清楚的地方或需要更多解释,请留言,我会扩展回答。
顺便说一句,通常最好使用BenchmarkTools.jl来分析代码的性能(除非您想测量编译时间也)。
编辑
请查看:
julia> loop(x) = for i in 1:10000000 getnonparametric(x) end
loop (generic function with 1 method)
julia> @code_native loop(xknown)
.text
pushq %rbp
movq %rsp, %rbp
pushq %rax
movq %rdx, -8(%rbp)
movl $74776584, %eax # imm = 0x4750008
addq $8, %rsp
popq %rbp
retq
julia> @code_native loop(xany)
.text
pushq %rbp
movq %rsp, %rbp
pushq %rax
movq %rdx, -8(%rbp)
movl $74776584, %eax # imm = 0x4750008
addq $8, %rsp
popq %rbp
retq
您可以看到编译器足够聪明,优化出整个循环(因为它本质上是无操作)。这就是 Julia 的强大之处(但另一方面也使得有时候进行基准测试很困难)。
以下示例向您展示了更准确的视图(请注意我使用了更复杂的表达式,因为即使是非常简单的表达式在循环中也可以被编译器优化掉):
julia> xknowns = fill(xknown, 10^6);
julia> xanys = fill(xany, 10^6);
julia> @btime sum(getnonparametric, $xanys)
12.373 ms (0 allocations: 0 bytes)
2000000
julia> @btime sum(getnonparametric, $xknowns)
519.700 μs (0 allocations: 0 bytes)
2000000
请注意,即使在这种情况下,编译器也足够“聪明”,能够正确地推断出表达式的返回类型,在这两种情况下,你都访问了非参数字段。
julia> @code_warntype sum(getnonparametric, xanys)
Variables
f::Core.Compiler.Const(getnonparametric, false)
a::Array{TypeAny,1}
Body::Int64
1 ─ nothing
│ %2 = Base.:(
└── return %2
julia> @code_warntype sum(getnonparametric, xknowns)
Variables
f::Core.Compiler.Const(getnonparametric, false)
a::Array{TypeKnown,1}
Body::Int64
1 ─ nothing
│ %2 = Base.:(
└── return %2
当你查看在两种情况下生成的本地代码时,差异的核心就可以看到:
julia> @code_native getnonparametric(xany)
.text
pushq %rbp
movq %rsp, %rbp
subq $48, %rsp
movq (%rcx), %rax
movq %rax, -16(%rbp)
movq $75966808, -8(%rbp) # imm = 0x4872958
movabsq $jl_f_getfield, %rax
leaq -16(%rbp), %rdx
xorl %ecx, %ecx
movl $2, %r8d
callq *%rax
movq (%rax), %rax
addq $48, %rsp
popq %rbp
retq
nopl (%rax,%rax)
julia> @code_native getnonparametric(xknown)
.text
pushq %rbp
movq %rsp, %rbp
movq (%rcx), %rax
movq 8(%rax), %rax
popq %rbp
retq
nopl (%rax)
如果您给类型添加参数,则一切都按预期工作:
julia> struct Container{T}
parametric::T
nonparametric::Int64
end
julia> struct TypeAny2{T}
payload::Container{T}
end
julia> xany2 = TypeAny2(Container([1], 2))
TypeAny2{Array{Int64,1}}(Container{Array{Int64,1}}([1], 2))
julia> @code_native getnonparametric(xany2)
.text
; ┌ @ REPL[9]:1 within `getnonparametric'
pushq %rbp
movq %rsp, %rbp
; │┌ @ Base.jl:20 within `getproperty'
movq (%rcx), %rax
; │└
movq 8(%rax), %rax
popq %rbp
retq
nopl (%rax)
; └
你有:
julia> xany2s = fill(xany2, 10^6);
julia> @btime sum(getnonparametric, $xany2s)
528.699 μs (0 allocations: 0 bytes)
2000000
总结
- 如果想要提高性能,请尽可能使用没有抽象类型字段的容器。
- 有时候,如果第1点中的条件不符合,编译器可以有效地处理并生成快速的机器代码,但通常不能保证(因此第1点中的建议仍然适用)。
getnonparametric
方法的关键性能问题在于它被循环调用(并且每次迭代都执行动态分派)。如果我将循环包装在一个函数loop(x) = for i in 1:10000000 getnonparametric(x) end
中,引入函数屏障并只执行一次分派,代码会快得多(对于@btime loop(xknown)
和@btime loop(xany)
都是 8ns)。谢谢! - Karel Horak