Julia:内存中存在大量可变结构时,为什么 GC 会很慢

cno*_*cno 3 julia

与非可变结构相比,为什么当大量可变结构加载到内存中时,垃圾回收速度要慢得多?在这两种情况下,对象树应该具有相同的大小。

julia> struct Foo
           a::Float64
           b::Float64
           c::Float64
       end

julia> mutable struct Bar
           a::Float64
           b::Float64
           c::Float64
       end

julia> @time dat1 = [Foo(0.0, 0.0, 0.0) for i in 1:1e9];
  9.706709 seconds (371.88 k allocations: 22.371 GiB, 0.14% gc time)

julia> @time GC.gc(true)
  0.104186 seconds, 100.00% gc time

julia> @time GC.gc(true)
  0.124675 seconds, 100.00% gc time

julia> @time dat2 = [Bar(0.0, 0.0, 0.0) for i in 1:1e9];
 71.870870 seconds (1.00 G allocations: 37.256 GiB, 73.88% gc time)

julia> @time GC.gc(true)
 47.695473 seconds, 100.00% gc time

julia> @time GC.gc(true)
 41.809898 seconds, 100.00% gc time
Run Code Online (Sandbox Code Playgroud)

Car*_*num 5

不可变结构可以直接存储在数组中。对于可变结构来说,这种情况永远不会发生。在您的情况下,Foo对象都直接存储在 中dat1,因此在创建 Arary 后实际上只有一个(尽管非常大)分配可访问。

在 的情况下dat2,每个Bar对象都会为其分配自己的一块内存,并且数组将包含对这些对象的引用。因此dat2,使用 ,您最终会得到 1G + 1 个可达分配。

您还可以使用以下命令查看此内容Base.sizeof

julia> sizeof(dat1)
24000000000

julia> sizeof(dat2)
8000000000
Run Code Online (Sandbox Code Playgroud)

您会看到它dat1是原来的 3 倍,因为每个数组条目都Float64直接包含 3,而 中的条目dat2仅占用每个指针的空间。

附带说明:对于这些类型的测试,最好使用而BenchmarkTools.@btime不是内置的@time. 因为它消除了结果中的编译开销,并且还多次运行代码以便为您提供更具代表性的结果:

@btime dat1 = [Foo(0.0, 0.0, 0.0) for i in 1:1e6]
  2.237 ms (2 allocations: 22.89 MiB)

@btime dat2 = [Bar(0.0, 0.0, 0.0) for i in 1:1e6]
  6.878 ms (1000002 allocations: 38.15 MiB)
Run Code Online (Sandbox Code Playgroud)

如上所述,这对于调试分配特别有用。因为dat1我们获得了 2 个分配(一个用于Array实例本身,一个用于数组存储其数据的内存块),而dat2我们每个元素都有一个额外的分配。