Unexpected binary size & long build times when using complex nested structs

We are seeing unexpected build times resulting in huge binary sizes when dealing with large nested structs. The following code produces around 30mb of binary:

struct Foo {
    let foo1: Foo2?
    let foo2: Foo2?
    let foo3: Foo2?
    let foo4: Foo2?
    let foo5: Foo2?
}

struct Foo2 {

    let foo1: Foo3?
    let foo2: Foo3?
    let foo3: Foo3?
    let foo4: Foo3?
    let foo5: Foo3?
    let foo6: Foo3?
    let foo7: Foo3?
    let foo8: Foo3?
    let foo9: Foo3?
    let foo10: Foo3?
    let foo11: Foo3?
    let foo12: Foo3?
    let foo13: Foo3?
    let foo14: Foo3?
    let foo15: Foo3?
    let foo16: Foo3?
    let foo17: Foo3?
    let foo18: Foo3?
    let foo19: Foo3?
    let foo20: Foo3?


}

struct Foo3 {
    let foo1: Foo4?
    let foo2: Foo4?
    let foo3: Foo4?
    let foo4: Foo4?
    let foo5: Foo4?
    let foo6: Foo4?
    let foo7: Foo4?
    let foo8: Foo4?
    let foo9: Foo4?
    let foo10: Foo4?
    let foo11: Foo4?
    let foo12: Foo4?
    let foo13: Foo4?
    let foo14: Foo4?
    let foo15: Foo4?
    let foo16: Foo4?
    let foo17: Foo4?
    let foo18: Foo4?
    let foo19: Foo4?
    let foo20: Foo4?
}

struct Foo4 {
    let foo1: String?
    let foo2: String?
    let foo3: String?
    let foo4: String?
    let foo5: String?
    let foo6: String?
    let foo7: String?
    let foo8: String?
    let foo9: String?
    let foo10: String?
    let foo11: String?
    let foo12: String?
    let foo13: String?
    let foo14: String?
    let foo15: String?
    let foo16: String?
    let foo17: String?
    let foo18: String?
    let foo19: String?
    let foo20: String?


}

Is this expected behavior? When replacing the structs with classes everything is fast and the binary is very compact, as expected.

This is a long-standing issue, see [SR-14136] Nested structs make the compiler hang with 100% CPU usage · Issue #56516 · swiftlang/swift · GitHub

1 Like

In our case the compiler does not hang, but takes quite long to finish with an huge binary as a result.

Yes, the current algorithm is non-linear, it's not an infinite loop. But the effect to an end user is frequently the same, unfortunately: it doesn't complete in a reasonable amount of time.

An unintended side effect of this bug is that it "forces" you to implement your huge structs as copy-on-write, which makes the compiler performance issue and the runtime performance issues disappear. Unfortunately, that involves some manual toil (move the properties to a nested class, add computed properties on the struct that forward to it, along with calls to isKnownUniquelyReferenced to handle copying them when needed).

Even worse! We have a few custom collection similar like:

struct Foo {
    let all: [Foo]
    let first: Foo2
    let last: Foo2
}

Just converting the stored properties into computed ones saves us about 20mb in binary size

struct Foo {
    let all: [Foo]
    let first: Foo2 { all[0]}
    let last: Foo2  { ... }
}

Losing automatic Codable, Equatable & Hashable when moving to classes would hurt even more

1 Like

Is there any way how to find code which produces those issues? I guess we have a few of those in our codebase, but are unable find them all.