Why mutable structs are allocated on the heap?

question

#1

From https://docs.julialang.org/en/latest/manual/types/#Mutable-Composite-Types-1:

In order to support mutation, such objects are generally allocated on the heap, and have stable memory addresses.

If I define:

mutable struct MyT
x::Int
end 

and then use MyT inside a function, such as:

function f(x::Int) 
q = MyT(x);
q.x += 1
return q.x
end

I would expect q to be allocated on the stack, because it is a local variable that does not change type and MyT is isbits. But it is not:

@btime f(1) # 3.781 ns (1 allocation: 16 bytes)

Why?


#2

Julia has many optimisations, eliminating unnecessary allocations is one of them.


#3

But in this case the allocation seems unnecessary, yet it occurs. Or am I missing something?


#4

Sorry, on 0.7 this optimization is much more effective:

julia> @btime f(1)
  1.503 ns (0 allocations: 0 bytes)
2

julia> VERSION
v"0.7.0-rc3.3"

#5

Is there an easy to state rule to know when something will be allocated on the heap vs. the stack? If not, is there a macro (something like @code_warntype) that alerts me when a variable is heap allocated?


#6

0.6:

julia> @code_typed f(1)
CodeInfo(:(begin
        q = $(Expr(:new, :(Main.MyT), :(x))) # line 3:
        SSAValue(0) = (Base.add_int)((Core.getfield)(q, :x)::Int64, 1)::Int64
        (Core.setfield!)(q, :x, SSAValue(0))::Int64 # line 4:
        return (Core.getfield)(q, :x)::Int64
    end))=>Int64

0.7

julia> @code_typed f(1)
CodeInfo(
3 1 ─ %1 = (Base.add_int)(x, 1)::Int64          │╻ +
  └──      goto #3 if not false                 │
  2 ─      nothing::Nothing                     │
4 3 ─      return %1                            │
) => Int64

Can see that the new call creating the mutable type is elided.


#7

Thanks.


#8

One more question. If I have a vector of mutable structs such as this one (which only contain isbits fields), the vector will be a pointer to a list of pointers to the structs, or a single pointer to a single chunk of memory with all the structs?


#9

They will not lie contiguously in memory.


#10

Is there a way to force a contiguous block? Like for instance perhaps casting the Vector to a Tuple might be a good idea in this case?


#11

Don’t use mutable


#12

Just to elaborate on this a bit: if you want a contiguous array of structs, then they need to be immutable. But suppose you want to change a field in one of those structs? The Julian way to do it is to copy the whole struct over and then count on compiler optimizations to elide needless work. For example, here is a function (not tested) that changes one field of a struct that lives inside of an array:

    struct A
        a::Int
        b::Bool
    end

    function changeb!(v::Vector{A}, ind, newb::Bool)
        v[ind] = A(v[ind].a, newb)
        nothing
    end

For a struct that has many fields, this can get awkward. I wrote a macro a long time ago to automatically generate the above kind of code, but I haven’t used it recently, and I’m not sure it works any more. I suspect someone else may have a package for this purpose.


#13

Can they lie contiguously in memory when all attributes of the mutable struct are fixed length eg bits type?


#14

A little bit off-topic: this might be useful if you want every field in the immutable struct stored contiguously in memory.


#15

The issue of mutable/immutable structs is orthogonal to the issue of whether its fields are bits/nonbits. In other words, a particular mutable struct may have only bits objects, but it is still mutable (one can change the fields individually). Similarly, an immutable struct may have nonbits entries, but it is still immutable (one cannot change the fields individually). In terms of the memory layout, for both mutable and immutable, a nonbits field is stored via a pointer inside the struct, whereas a bits field is stored directly. (Note: the compiler is allowed to emit code that implements these properties differently for the purpose of improving performance as long as the results are indistinguishable from this description.)

For an array of structs, as Kristoffer Carlsson said, there is contiguous memory layout only for immutable structs. And if those immutable structs contain nonbits fields, then each struct object in the array internally will have pointers, so the data in the array may still be scattered.


#16

Note that Julia isbits returns false for a mutable struct, even if all the fields are isbits. I agree with the distinction of mutable/immutable vs. bits/nonbits… but then why isbits behaves like this?


#17

This explains it quite well I think:

help?> isbits
search: isbits isbitstype disable_sigint

  isbits(x)

  Return true if x is an instance of an isbitstype type.

help?> isbitstype
search: isbitstype

  isbitstype(T)

  Return true if type T is a "plain data" type, meaning it is immutable and contains no references to other values,
  only primitive types and other isbitstype types. Typical examples are numeric types such as UInt8, Float64, and
  Complex{Float64}. This category of types is significant since they are valid as type parameters, may not track
  isdefined / isassigned status, and have a defined layout that is compatible with C.

  Examples
  ≡≡≡≡≡≡≡≡≡≡

  julia> isbitstype(Complex{Float64})
  true

  julia> isbitstype(Complex)
  false

#18

Setfeild.jl does this.