Do value types get allocated in the heap in these cases?

Hello everyone, hope you're well.
I've been investigating memory management lately, and got a bit stuck on a subject of stack vs heap memory allocation.
When talking about a context of a function - things are clear: any value type parameters are copied and allocated on the stack, any value types created in a function are also allocated on the stack (given all the requirements for stack allocation are met).
What I don't understand is allocation of value types outside of a function.

Let's say we have the following:

class Foo {
  let a: Int
}

Int is a value type, but it is stored in a class instance.
Does this mean that the property would be allocated on the heap as well?
If so, how is it accessed? By a reference, or the compiler does some trickery to copy the value into the stack for current context and keep using it until the function is returned?
Would anything regarding memory management change if a would become a var?

Another one, what if some value is described globally:

enum Globals {
static var someMutableSharedState = 0
}

Same questions for this use case.

Can someone please explain these cases?
Thank you for the help)

Bird's-eye view approximation:

class Foo {
  let a: Int
  var b: Int
}

Here both a and b are allocated inline within the storage allocated for the class instance, e.g. a would be at offset 16, b at offset 24 of that storage and the overall instance size would be 32. Typically that would be allocated on heap unless compiler could prove that heap allocation is not needed.

enum Globals {
  static var someMutableSharedState = 42
  static let someImmutableSharedState = 24
}

These will be allocated in writeable and read-only data segments correspondingly (special areas of heap). The latter could in theory be allocated in the code segment or even not be allocated at all, if compiler decides to use 24 literal instead. Could well depend upon the constant in question, if it's small like 0 and 1 that's one scenario, if it's a big value like 0x1122334455667788 that's another scenario.

This is very approximate, and if you need to know exactly what your version of compiler is doing for your target environment - check the generated code or check the actual address in runtime with something like this:

func isStackMemory(_ ptr: UnsafeRawPointer) -> Bool {
    let stackBase = pthread_get_stackaddr_np(pthread_self())
    let stackSize = pthread_get_stacksize_np(pthread_self())
    return ptr <= stackBase && ptr >= stackBase - stackSize
}

Bear in mind that the presence of the extra check might have effect on the code being generated (like in quantum mechanics), so it's worth checking the generated code still or check the address in debugger without having the check in your actual code.

8 Likes