如何在Julia中沿布尔数组的轴进行按位或归约?
我正试图找到最好的方法,在Julia中将3D布尔掩码数组按位或缩减为2D 当然,我总是可以编写for循环:如何在Julia中沿布尔数组的轴进行按位或归约?,julia,Julia,我正试图找到最好的方法,在Julia中将3D布尔掩码数组按位或缩减为2D 当然,我总是可以编写for循环: x = randbool(3,3,3) out = copy(x[:,:,1]) for i = 1:3 for j = 1:3 for k = 2:3 out[i,j] |= x[i,j,k] end end end 但是我想知道是否有更好的方法来减少 out = x[:,:,1] | x[:,:,2] | x[:
x = randbool(3,3,3)
out = copy(x[:,:,1])
for i = 1:3
for j = 1:3
for k = 2:3
out[i,j] |= x[i,j,k]
end
end
end
但是我想知道是否有更好的方法来减少
out = x[:,:,1] | x[:,:,2] | x[:,:,3]
但我做了一些基准测试:
function simple(n,x)
out = x[:,:,1] | x[:,:,2]
for k = 3:n
@inbounds out |= x[:,:,k]
end
return out
end
function forloops(n,x)
out = copy(x[:,:,1])
for i = 1:n
for j = 1:n
for k = 2:n
@inbounds out[i,j] |= x[i,j,k]
end
end
end
return out
end
function forloopscolfirst(n,x)
out = copy(x[:,:,1])
for j = 1:n
for i = 1:n
for k = 2:n
@inbounds out[i,j] |= x[i,j,k]
end
end
end
return out
end
shorty(n,x) = |([x[:,:,i] for i in 1:n]...)
timholy(n,x) = any(x,3)
function runtest(n)
x = randbool(n,n,n)
@time out1 = simple(n,x)
@time out2 = forloops(n,x)
@time out3 = forloopscolfirst(n,x)
@time out4 = shorty(n,x)
@time out5 = timholy(n,x)
println(all(out1 .== out2))
println(all(out1 .== out3))
println(all(out1 .== out4))
println(all(out1 .== out5))
end
runtest(3)
runtest(500)
结果如下
# For 500
simple: 0.039403016 seconds (39716840 bytes allocated)
forloops: 6.259421683 seconds (77504 bytes allocated)
forloopscolfirst 1.809124505 seconds (77504 bytes allocated)
shorty: elapsed time: 0.050384062 seconds (39464608 bytes allocated)
timholy: 2.396887396 seconds (31784 bytes allocated)
因此,我将使用
simple
或shorty
可以应用各种标准的优化技巧和提示,但这里要做的关键观察是Julia按顺序组织数组。对于小尺寸阵列,这不容易看到,但当阵列变大时,这就说明了问题。有一种方法reduce,它经过优化以在集合上执行功能(在本例中为或),但它是有代价的。如果组合步骤的数量相对较少,则最好简单地循环。在所有情况下,最小化内存访问次数总的来说更好。下面是利用这两件事进行优化的各种尝试
各种尝试和观察
初始函数
这里有一个函数,它以您的示例为例并对其进行了推广
function boolReduce1(x)
out = copy(x[:,:,1])
for i = 1:size(x,1)
for j = 1:size(x,2)
for k = 2:size(x,3)
out[i,j] |= x[i,j,k]
end
end
end
out
end
创建一个相当大的阵列,我们可以计算它的性能
julia> @time boolReduce1(b);
elapsed time: 42.372058096 seconds (1056704 bytes allocated)
应用优化
下面是另一个类似的版本,但带有标准类型提示,使用@inbounds并反转循环
function boolReduce2(b::BitArray{3})
a = BitArray{2}(size(b)[1:2]...)
for j = 1:size(b,2)
for i = 1:size(b,1)
@inbounds a[i,j] = b[i,j,1]
for k = 2:size(b,3)
@inbounds a[i,j] |= b[i,j,k]
end
end
end
a
end
慢慢来
julia> @time boolReduce2(b);
elapsed time: 12.892392891 seconds (500520 bytes allocated)
洞察力
第二个函数要快得多,而且由于没有创建临时数组,所以分配的内存也更少。但是,如果我们简单地取第一个函数并反转数组索引,会怎么样
function boolReduce3(x)
out = copy(x[:,:,1])
for j = 1:size(x,2)
for i = 1:size(x,1)
for k = 2:size(x,3)
out[i,j] |= x[i,j,k]
end
end
end
out
end
现在慢慢来
julia> @time boolReduce3(b);
elapsed time: 12.451501749 seconds (1056704 bytes allocated)
这和第二个函数一样快
使用reduce
我们可以使用一个名为reduce的函数来消除第三个循环。它的功能是使用上一个操作的结果对所有元素重复应用一个操作。这正是我们想要的
function boolReduce4(b)
a = BitArray{2}(size(b)[1:2]...)
for j = 1:size(b,2)
for i = 1:size(b,1)
@inbounds a[i,j] = reduce(|,b[i,j,:])
end
end
a
end
现在慢慢来
julia> @time boolReduce4(b);
elapsed time: 15.828273008 seconds (1503092520 bytes allocated, 4.07% gc time)
没关系,但速度甚至不如简单的优化原版。原因是,看看分配的所有额外内存。这是因为必须从各地复制数据,以生成用于reduce的输入
结合事物
但是,如果我们尽可能最大限度地发挥洞察力,会怎么样呢。第一个索引不是减少最后一个索引,而是
function boolReduceX(b)
a = BitArray{2}(size(b)[2:3]...)
for j = 1:size(b,3)
for i = 1:size(b,2)
@inbounds a[i,j] = reduce(|,b[:,i,j])
end
end
a
end
现在创建一个类似的数组并计时
julia> c = randbool(200,2000,2000);
julia> @time boolReduceX(c);
elapsed time: 1.877547669 seconds (927092520 bytes allocated, 21.66% gc time)
使大型阵列的功能比原始版本快20倍。很好
但是如果中等尺寸呢?
如果大小非常大,则上面的函数看起来最好,但如果数据集大小较小,则使用reduce不会产生足够的回报,下面的函数会更快。包括版本2中的临时变速器。boolReduceX的另一个版本使用循环而不是reduce(此处未显示),速度更快
function boolReduce5(b)
a = BitArray{2}(size(b)[1:2]...)
for j = 1:size(b,2)
for i = 1:size(b,1)
@inbounds t = b[i,j,1]
for k = 2:size(b,3)
@inbounds t |= b[i,j,k]
end
@inbounds a[i,j] = t
end
end
a
end
julia> b = randbool(2000,2000,20);
julia> c = randbool(20,2000,2000);
julia> @time boolReduceX(c);
elapsed time: 1.535334322 seconds (799092520 bytes allocated, 23.79% gc time)
julia> @time boolReduce5(b);
elapsed time: 0.491410981 seconds (500520 bytes allocated)
尝试任何(x,3)
。只需在此处多输入一点,这样StackOverflow就不会禁止此响应。开发更快。这只是你想投入多少工作的问题。naïve-devectorized方法速度较慢,因为它是一个位数组:提取连续区域和按位或两者都可以一次在64位块上完成,但naïve-devectorized方法一次操作一个元素。最重要的是,索引位数组的速度很慢,这既是因为涉及到一系列位操作,也是因为由于边界检查,它目前无法内联。这里有一个被开发的策略,但它利用了位数组的结构。大部分代码都是从copy_块复制粘贴的!在bitarray.jl中,我没有试图美化它(对不起!)
记录在案,
b
上的那些类型提示对性能没有帮助。@IainDunning-Hehe,是的,我看到了。我将从最后两个版本中删除它们,但将其保留在第二个版本中以说明这一点。@waTeim并在调用reduce时使用ArrayView->view使boolReduceX更快:)我迫不及待地等待,直到它在v0.4中出现,只需警告您需要将位数组转换为数组{Bool}@waTeim,我喜欢您答案的深度。如果你把Iain的一些测试作为比较,我会转而接受你的测试。干杯我现在已经将这个答案纳入了我的基准测试中。绝对是最短的,但不是最快的(令人惊讶!)这是一个很好的例子,说明了矢量化是如何发挥作用的。关键的一点是,由于位数组是用向量{Uint64}中的位表示的,所以向量化版本大大减少了您必须执行的位提取量。谢谢,Iain!我喜欢最好的答案也是最短的答案之一。在我看来,分配的字节数和运行时间之间的相关性要强得多。有时这可能仍然是正确的,但我肯定必须更频繁地检查这一假设。我的头脑中也有这种关联,但回答一些堆栈溢出问题已经稍微改变了我的观点。
function devec(n::Int, x::BitArray)
src = x.chunks
out = falses(n, n)
dest = out.chunks
numbits = n*n
kd0 = 1
ld0 = 0
for j = 1:n
pos_s = (n*n)*(j-1)+1
kd1, ld1 = Base.get_chunks_id(numbits - 1)
ks0, ls0 = Base.get_chunks_id(pos_s)
ks1, ls1 = Base.get_chunks_id(pos_s + numbits - 1)
delta_kd = kd1 - kd0
delta_ks = ks1 - ks0
u = Base._msk64
if delta_kd == 0
msk_d0 = ~(u << ld0) | (u << (ld1+1))
else
msk_d0 = ~(u << ld0)
msk_d1 = (u << (ld1+1))
end
if delta_ks == 0
msk_s0 = (u << ls0) & ~(u << (ls1+1))
else
msk_s0 = (u << ls0)
end
chunk_s0 = Base.glue_src_bitchunks(src, ks0, ks1, msk_s0, ls0)
dest[kd0] |= (dest[kd0] & msk_d0) | ((chunk_s0 << ld0) & ~msk_d0)
delta_kd == 0 && continue
for i = 1 : kd1 - kd0
chunk_s1 = Base.glue_src_bitchunks(src, ks0 + i, ks1, msk_s0, ls0)
chunk_s = (chunk_s0 >>> (64 - ld0)) | (chunk_s1 << ld0)
dest[kd0 + i] |= chunk_s
chunk_s0 = chunk_s1
end
end
out
end
simple: 0.051321131 seconds (46356000 bytes allocated, 30.03% gc time)
forloops: 6.226652258 seconds (92976 bytes allocated)
forloopscolfirst: 2.099381939 seconds (89472 bytes allocated)
shorty: 0.060194226 seconds (46387760 bytes allocated, 36.27% gc time)
timholy: 2.464298752 seconds (31784 bytes allocated)
devec: 0.008734413 seconds (31472 bytes allocated)