在数组中查找唯一元素只是一个基本的算法难题。大多数幼稚的实现是O(n^2),虽然您可以在O(n) 时间内为已经排序的单个列表执行此操作,但是您必须先排序,甚至quicksort,正如我们所知,没有平均优于O(nlog(n))。
我花了一些时间来尝试制作比 Base.unique 更好的东西,但即使在迄今为止最好的情况下,我也只能做到几个百分点;通常,如果我能解决问题以更好地利用 CPU 的 SIMD 指令(例如 x86 上的 AVX 或 ARM 上的 Neon)和 LoopVectorization.jl,我只能击败基础,而这个特殊问题对 SIMD 不太友好.
我能想到一个技巧可能可以在这里帮助你一点。如果unique 很难,那么矩阵行上的unique 在技术上是一个需要优化的痛苦**。例如,考虑以下性能差异:
julia> using BenchmarkTools
julia> M = rand(1:20,10^6,4)
1000000×4 Matrix{Int64}:
17 9 17 4
2 19 19 18
13 9 14 7
⋮
19 8 2 17
20 20 5 9
julia> @benchmark unique($M, dims=1)
BenchmarkTools.Trial: 58 samples with 1 evaluation.
Range (min … max): 67.044 ms … 105.911 ms ┊ GC (min … max): 0.00% … 11.76%
Time (median): 85.795 ms ┊ GC (median): 0.00%
Time (mean ± σ): 86.759 ms ± 10.448 ms ┊ GC (mean ± σ): 3.87% ± 5.09%
█ █ ▁ ▁ ▁▄▁ ▄ ▁ ▄ ▁ ▁ ▁▁▁ ▁
▆▆▁▆▆▁▁▁▁▁▆█▁█▁▁▆█▁▁█▁███▁▁█▆▆█▁▁▆▆▆▆▁▁▁█▆█▁█▁███▁█▆▆▆▁▁▁▆▁▆ ▁
67 ms Histogram: frequency by time 106 ms <
Memory estimate: 27.14 MiB, allocs estimate: 47.
julia> V = prod(M, dims=2)
1000000×1 Matrix{Int64}:
10404
12996
11466
⋮
5168
18000
julia> @benchmark unique($V)
BenchmarkTools.Trial: 555 samples with 1 evaluation.
Range (min … max): 7.739 ms … 12.680 ms ┊ GC (min … max): 0.00% … 0.00%
Time (median): 8.512 ms ┊ GC (median): 0.00%
Time (mean ± σ): 8.989 ms ± 1.044 ms ┊ GC (mean ± σ): 0.00% ± 0.00%
▃▆█▁▃▄
▃▄▅▇████████▅▄▄▄▅▅▅▃▃▃▄▄▅▄▅▃▅▄▅▄▃▄▄▃▄▃▂▄▃▁▃▂▃▂▃▃▃▃▃▃▂▃▃▃▃▃ ▃
7.74 ms Histogram: frequency by time 11.9 ms <
Memory estimate: 103.06 KiB, allocs estimate: 24.
大约一个数量级。因此,如果有任何方法可以将矩阵转换为向量,那对你有很大帮助。
特别是,如果您的实数矩阵中的数字可以放入比Int64 或Float64 更小的类型,并且每行中只有几个数字,那么您可以潜在地可逆地将reinterpret 每行作为64位类型:
julia> M = Int16.(M)
1000000×4 Matrix{Int16}:
17 9 17 4
2 19 19 18
13 9 14 7
8 1 2 12
8 16 19 13
⋮
12 19 2 15
4 5 10 14
19 8 2 17
20 20 5 9
julia> Mᵥ = vec(reinterpret(UInt64, M'))
1000000-element reshape(reinterpret(UInt64, adjoint(::Matrix{Int16})), 1000000) with eltype UInt64:
0x0004001100090011
0x0012001300130002
0x0007000e0009000d
0x000c000200010008
0x000d001300100008
⋮
0x000f00020013000c
0x000e000a00050004
0x0011000200080013
0x0009000500140014
julia> reinterpret(Int16, unique(Mᵥ)')'
159674×4 adjoint(reinterpret(Int16, adjoint(::Vector{UInt64}))) with eltype Int16:
17 9 17 4
2 19 19 18
13 9 14 7
8 1 2 12
8 16 19 13
⋮
17 5 5 11
1 18 14 17
2 20 5 9
15 17 14 4
注意您必须确保每行中的位数加起来是正确的数字(如果必须填充),并且您必须先转置,因为 Julia 是列优先的!
但是,由于reinterpret 基本上是免费的,因此整个操作最终仍可能比 unique-by-rows 快得多:
julia> @benchmark unique($M, dims=1)
BenchmarkTools.Trial: 65 samples with 1 evaluation.
Range (min … max): 60.212 ms … 93.517 ms ┊ GC (min … max): 0.00% … 0.00%
Time (median): 78.691 ms ┊ GC (median): 0.00%
Time (mean ± σ): 77.909 ms ± 6.788 ms ┊ GC (mean ± σ): 3.85% ± 5.57%
▁ ▃ ▁ ▃▃▁ █ ▁ ▁
▄▁▁▁▄▁▁▁▁▁▁▁▁▄▄▄▄▄▇▇▄▁▄▁▄█▇▁▁▁▄▄▄▄▇█▄█▇▁███▄▄▄█▁▁▁▄▇▁▁▁█▁▁█ ▁
60.2 ms Histogram: frequency by time 89.1 ms <
Memory estimate: 23.48 MiB, allocs estimate: 47.
julia> @benchmark reinterpret(Int16,unique(vec(reinterpret(UInt64, $M')))')'
BenchmarkTools.Trial: 137 samples with 1 evaluation.
Range (min … max): 30.070 ms … 46.186 ms ┊ GC (min … max): 0.00% … 15.62%
Time (median): 36.334 ms ┊ GC (median): 0.00%
Time (mean ± σ): 36.433 ms ± 3.491 ms ┊ GC (mean ± σ): 1.65% ± 4.51%
▁ ▁ ▃ ▁ █▆ ▁▁▃▆ ▁▃▆▁▁▃▁█▃█▁▆▃▁ █ ▃
▄▁█▇▇█▇█▇█▇██▇████▇██████████████▇█▇▁▄▁▄▄█▁▁▇▁▄▄▄▁▁▄▁▄▁▁▄▁▄ ▄
30.1 ms Histogram: frequency by time 46.2 ms <
Memory estimate: 5.96 MiB, allocs estimate: 44.
既然我们已经将你的矩阵变成了一个向量,现在原则上我们也可以替换ThreadsX.unique,但在我的测试中,到目前为止这并不是更快:
julia> @benchmark reinterpret(Int16,ThreadsX.unique(vec(reinterpret(UInt64, $M')))')'
BenchmarkTools.Trial: 56 samples with 1 evaluation.
Range (min … max): 69.633 ms … 136.835 ms ┊ GC (min … max): 0.00% … 0.00%
Time (median): 80.427 ms ┊ GC (median): 0.00%
Time (mean ± σ): 89.547 ms ± 18.360 ms ┊ GC (mean ± σ): 10.91% ± 13.49%
▄ █ ▁
▆▆▁▄█▇▆█▇▁▇▇▁▆▁▄▁▁▁▁▁▁▁▁▄▄▁▁▁▄▁▁▆▁▄▄▁▁▁▁▄█▆▁▄▄▁▁▁▁▄▁▁▄▁▁▁▁▁▄ ▁
69.6 ms Histogram: frequency by time 131 ms <
Memory estimate: 121.28 MiB, allocs estimate: 3310.