arrays - Julia 数组数组:(行 -> 列)性能
问题描述
在这里完成 Julia 新手。
给定一个数组数组,我想组合每个子数组的对应元素。像这样的东西:
[2, 7, 9] [2, 3, 2, 7, 3]
[3, 5, 4] [7, 5, 7, 9, 5]
[2, 7, 7] -> [9, 4, 7, 1, 1]
[7, 9, 1]
[3, 5, 1]
搜索 stackoverflow 我遇到了几个解决方案,而不是直接循环或列表理解。
julia> a=Vector{Int}[rand(1:10,3) for i=1:5]
5-element Array{Array{Int64,1},1}:
[2, 7, 9]
[3, 5, 4]
[2, 7, 7]
[7, 9, 1]
[3, 5, 1]
julia> using BenchmarkTools
julia> @btime a2=mapslices( x -> [x], hcat(a...), dims=2)[:]
6.174 μs (65 allocations: 3.45 KiB)
3-element Array{Array{Int64,1},1}:
[2, 3, 2, 7, 3]
[7, 5, 7, 9, 5]
[9, 4, 7, 1, 1]
julia> @btime a3=[getindex.(a,i) for i=1:length(a[1])]
948.087 ns (14 allocations: 768 bytes)
3-element Array{Array{Int64,1},1}:
[2, 3, 2, 7, 3]
[7, 5, 7, 9, 5]
[9, 4, 7, 1, 1]
我的问题是:为什么第二个比第一个快六倍?和hcat有关系吗?
解决方案
基线和基准测试正确
好的,首先让我们在我的计算机上建立基线。
在我们做任何其他事情之前,我们需要确保我们没有对全局变量进行基准测试。来自BenchmarkTools 自述文件:
如果您要基准测试的表达式依赖于外部变量,您应该使用
$
将它们“插入”到基准表达式中,以避免使用 globals 进行基准测试的问题。本质上,任何插值变量$x
或表达式$(...)
在基准测试开始之前都是“预先计算的”......
julia> a=Vector{Int}[rand(1:10,3) for i=1:5];
julia> @btime a2=mapslices( x -> [x], hcat($a...), dims=2)[:];
6.015 μs (65 allocations: 3.45 KiB)
julia> @btime a3=[getindex.($a,i) for i=1:length($a[1])];
149.228 ns (6 allocations: 544 bytes)
(如果我没有插值,我会得到和你大致相同的结果a3
999.500 ns (14 allocations: 768 bytes)
)。
所以它a3
不是快 6 倍,而是实际上快了 33 倍。
为什么有区别?
分配。
与其他操作(所有语言)相比,分配相当慢。我们可以看到a2
代码分配的比代码多得多a3
。
所以让我们看看分配的位:
a2
[x]
为每一列分配一个新的 1 元素数组hcat
分配一个连接所有内容的新矩阵mapslices
为从矩阵中取出的每个切片分配一个数组mapslice
分配一个数组来保存输出(有趣的是它不做视图,但我检查了)[:]
执行输出的重塑副本。(替代方案是vec
返回一个重塑视图)
a3
getindex.(a, i)
为输出的每一列分配一个数组(与mapslice
输入矩阵的内部切片相同)[ ... for ...]
为输出分配一个数组(与 mapslices 输出相同)所以我们可以看到有一堆额外的分配
a2
在a3
.如果我们只有与
hcat
.由于最初的问题询问是否是因为
hcat
让我们看一下。
我定义了一个新的基准保存到a4
. 它使用eachslice
which 将视图的(惰性)生成器返回到矩阵的切片中。所以那里的分配可以忽略不计。为了阻止它偷懒,我们collect
它。this 的最终输出是 an Array
of SubArray
s (而不是 an Array
of Arrays
),但这很好,它会像它仍然是 subtypes 一样工作AbstractArray
。
julia> @btime a4 = collect(eachslice(hcat($a...), dims=1));
734.320 ns (13 allocations: 704 bytes)
这里我们的主要分配是 - hcat
-collect
它分配输出(与 相同[ ... for ...]
)。
所以是的,它hcat
有效果,但它与大部分差异相去甚远。
喷溅和reduce(hcat, xs)
喷溅是一种代价。它通常很小,直到你开始喷出数百个项目,但因为这是一个微基准,其他一切都如此之快,让我们看看它是如何删除它的。
Julia 有一个优化的函数,reduce(hcat, xs)
可以xs
作为数组的数组。
所以让我们看看情况如何:
julia> @btime a2_s=mapslices(x -> [x], reduce(hcat, $a), dims=2);
5.278 μs (59 allocations: 3.17 KiB)
julia> @btime a4_s=collect(eachslice(reduce(hcat, $a), dims=1));
337.656 ns (8 allocations: 528 bytes)
我们可以看到这是有区别的。但是在a2
它并不多的情况下,因为它只hcat
完成一次,x->x
而在mapslices
复制切片时的缓慢分配hcat
会发生很多次。
我们能走得更快吗?
并不真地。
a3
这几乎是理想的代码。它不分配任何不返回的内容。
想一想,如果我们愿意转而使用StaticArrays ,我们可以获得真正快得不合理的东西。
julia> b = @SVector [@SVector [rand(1:10) for ii in 1:3] for i=1:5];
julia> @btime b3=[getindex.($b,i) for i in 1:length($b[1])];
36.055 ns (1 allocation: 208 bytes)
静态数组为编译器提供了更多信息。特别是所有数组的大小,以及它们都不会被改变的承诺。这意味着它可以: - 展开循环 - 编译时的边界检查 - 在堆栈(而不是堆)上分配它们 - 可能是我忘记的其他一些事情。
这让优化器(在 Julia 和 LLVM 中)变得非常疯狂。他们被编译成每个输入列(/输出行)基本上 2 个 SSE/AVX 矢量化移动操作,加上少量的固定开销。
julia> @code_native (b->[getindex.(b,i) for i in 1:length(b[1])])(b)
.section __TEXT,__text,regular,pure_instructions
; ┌ @ REPL[83]:1 within `#161'
subq $136, %rsp
vmovups (%rdi), %ymm0
vmovups 32(%rdi), %ymm1
vmovups 64(%rdi), %ymm2
vmovups 88(%rdi), %ymm3
vmovups %ymm3, 88(%rsp)
vmovups %ymm2, 64(%rsp)
vmovups %ymm1, 32(%rsp)
vmovups %ymm0, (%rsp)
movabsq $5152370032, %rax ## imm = 0x1331AED70
; │┌ @ generator.jl:32 within `Generator' @ generator.jl:32
vmovaps (%rax), %xmm0
vmovups %xmm0, 120(%rsp)
; │└
movabsq $collect, %rax
movq %rsp, %rdi
vzeroupper
callq *%rax
addq $136, %rsp
retq
nop
; └
推荐阅读
- laravel-5 - SQLSTATE[HY000] [1045] 用户 [...] 访问被拒绝(使用密码:YES) Laravel 5.2 on Live Web Server
- r - 将网格放置在 shapefile 中的多边形上,然后找到每个网格正方形的质心
- javascript - 在控制台中看不到 WebdriverIO 日志(webdriver 日志)
- javascript - 未定义从 json 返回
- javascript - 将 MMMM, dd yyyy 日期转换为 UTC 日期
- python - 包中的代理模型django
- sql-server - CONVERSION FAILED WHEN CONVERTING DATA/OR time 从字符串 VBA 到 SQL Server
- elasticsearch - 如何不将完成建议器存储在 Elasticsearch 的索引中
- spring - 使用wireTap时replyChannel超时
- javascript - 异步等待未处于保持状态 mongoose