Accessing a simple enum value consumes about 16K additional stack space. See code below:
import Foundation
enum Foo {
case x(Int)
}
func main() {
sleep(2)
print(Foo.x(1))
sleep(2)
}
main()
To reproduce the issue, put the code in a command line project, run vmmap in a terminal to monitor the process stack size, then run the above code:
$ while true; do vmmap $(pgrep StackSizeTest) 2>/dev/null | grep "^Stack" | grep -v Guard ; echo ""; sleep 1; done
(Note: replace the pgrep
command argument with your project name. Also make sure the project name is unique so that pgrep
returns only one pid).
Below is the output in my terminal. The process starts with 24K stack size, but grows to 40K when accessing a Foo
instance. I'm sure the stack size change is not caused by print()
, because if I print a integer, instead of Foo
instance, the stack size doesn't change.
Stack 7ff7bf700000-7ff7bff00000 [ 8192K 24K 24K 0K] rw-/rwx SM=PRV thread 0
Stack 8192K 24K 24K 0K 0K 0K 0K 1
Stack 7ff7bf700000-7ff7bff00000 [ 8192K 40K 40K 0K] rw-/rwx SM=PRV thread 0
Stack 8192K 40K 40K 0K 0K 0K 0K 1
My experiments show struct accessor has the same behavior. My environment: macOS 13.2.1, Xcode 14.2, Intel CPU.
I noticed this behavior because I'm investigating a stack overflow issue in my app. The enum values in my app have one associated value of about 1K size. However, accessing these enum values caused a worker thread stack size grow gradually from 20K to more than 512K (and hence overflowed). The enum accessor is compiled as outlined function in the app. Below are two versions of it, each having a different frame size (there are more versions in the binary and all of them are for the same enum type).
- This version creates a stack frame of about 84K size (0x15240 / 1024 = 84K).
acdbCNTests`outlined init with copy of FIDailyCEW:
-> 0x13c18c070 <+0>: pushq %rbp
0x13c18c071 <+1>: movq %rsp, %rbp
0x13c18c074 <+4>: subq $0x15240, %rsp ; imm = 0x15240
0x13c18c07b <+11>: movq %rdi, -0x50(%rbp)
0x13c18c07f <+15>: movq %rsi, -0x48(%rbp)
0x13c18c083 <+19>: movq %rsi, -0x40(%rbp)
0x13c18c087 <+23>: movq %rdi, -0x38(%rbp)
0x13c18c08b <+27>: movq %rsi, -0x30(%rbp)
0x13c18c08f <+31>: movq %rdi, %rax
0x13c18c092 <+34>: movq %rax, -0x28(%rbp)
0x13c18c096 <+38>: movq %rdi, -0x20(%rbp)
0x13c18c09a <+42>: xorl %eax, %eax
0x13c18c09c <+44>: movl %eax, %edi
0x13c18c09e <+46>: callq 0x13f1273b0 ; type metadata accessor for acdbCN.FICEW at <compiler-generated>
- This version creates a stack frame of about 522K size (0x82ab0 / 1024 = 522K). It caused stack overflow in worker thread.
acdbCNTests`outlined assign with take of SomeDailyCEW?:
0x13b582e20 <+0>: pushq %rbp
0x13b582e21 <+1>: movq %rsp, %rbp
0x13b582e24 <+4>: pushq %r14
0x13b582e26 <+6>: pushq %rbx
0x13b582e27 <+7>: subq $0x82ab0, %rsp ; imm = 0x82AB0
0x13b582e2e <+14>: movq %rdi, -0x50(%rbp)
0x13b582e32 <+18>: movq %rsi, -0x48(%rbp)
0x13b582e36 <+22>: movq %rsi, %rax
0x13b582e39 <+25>: movq %rax, -0x40(%rbp)
0x13b582e3d <+29>: movq %rdi, -0x38(%rbp)
0x13b582e41 <+33>: movq %rsi, -0x30(%rbp)
0x13b582e45 <+37>: xorl %eax, %eax
0x13b582e47 <+39>: movl %eax, %edi
-> 0x13b582e49 <+41>: callq 0x13b2c8950 ; type metadata accessor for acdbCN.SomeDailyCEW at <compiler-generated>
I wonder is this behavior by design or a bug? It can easily cause unexpected overflow. Is there some way or best practice to avoid this behavior?