【问题标题】:Any faster option rather than Base.unique() in Julia?有比 Julia 中的 Base.unique() 更快的选项吗?
【发布时间】:2022-01-20 18:42:31
【问题描述】:

我有一个二维矩阵,我想找到它的唯一行。例如,如果

M=[1 2 3;
   4 7 8;
   1 2 3;
   0 2 3]

那么,Base.unique 命令的输出,

Base.unique(M, dims=1)

是矩阵

[1 2 3;
 4 7 8;
 0 2 3]

这是我正在寻找的那个。但是,Base.unique 在矩阵很大时似乎很慢。还有其他更快的选择吗?

PS:ThreadsX 模块有一个类似的功能,ThreadsX.unique,根据this link,它比Base.unique 快。但是,它似乎只接受一维向量(或者至少这是我从该函数中推断出来的)。

【问题讨论】:

  • 还有其他语言做得更快吗?
  • @OscarSmith 我不知道。这是个好问题。
  • 从一些快速的时间来看,在我看来它不是很理想,但也不是那么遥远,实际上最佳的解决方案可能需要大量的工作来实现。
  • 我想知道性能问题是否是由于您正在比较向量。你能把这些整数连接成一个字符串并在它们上运行唯一吗?

标签: julia unique


【解决方案1】:

在数组中查找唯一元素只是一个基本的算法难题。大多数幼稚的实现是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.

大约一个数量级。因此,如果有任何方法可以矩阵转换为向量,那对你有很大帮助。

特别是,如果您的实数矩阵中的数字可以放入比Int64Float64 更小的类型,并且每行中只有几个数字,那么您可以潜在地可逆地将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.

【讨论】:

  • 请注意,reinterpret 解决方案的更通用替代方案是散列每一行并使散列唯一化。
  • 是的,这也是一个好点;我考虑过添加它,但由于哈希可能不可逆,那么您可能需要某种“argunique”来返回唯一元素的索引,而且我不知道任何实现的地方。跨度>
猜你喜欢
  • 2017-04-03
  • 2015-07-20
  • 2011-11-07
  • 1970-01-01
  • 1970-01-01
  • 2011-04-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多