Hi,
I've seen struct composition used frequently for json decoding and representing SwiftUI state, for example:
struct TitleSubtitle: Decodable {
let title: Text
let subtitle: Text
}
struct Text: Decodable {
let value: String
let style: TextStyle
}
struct TextStyle: Decodable {
let color: String
let size: Int
}
While working on a binary size profiling tool, Emerge, I noticed whenever I add properties to a struct the size of the value witness increases, but the value witness of every type directly or indirectly referencing this struct also gets larger. So a small addition to a struct like TextStyle can result in a big size increase due to it's use in Text, and multiple uses of Text in TitleSubtitle.
As a concrete example, if you start with two structs:
struct Foo {
let prop1: String
}
struct Bar {
let prop1: String
let prop2: String
...
let prop20: String
}
and use nm to view symbol sizes in the binary:
0000000100004e4c t initializeWithCopy value witness for StructSizeTests.Bar
0000000100004ff8 t assignWithCopy value witness for StructSizeTests.Bar
...
0000000100005530 t initializeWithCopy value witness for StructSizeTests.Foo
000000010000555c t assignWithCopy value witness for StructSizeTests.Foo
Bar has an initializeWithCopy that's 0x1AC bytes, and Foo's is 0x2C bytes. Now if you change Foo to be
struct Foo {
let prop1: String
let prop2: Bar
}
Bar will stay the same size, but Foo gets a larger initializeWithCopy: 0x1C0 bytes. A similar thing happens to other parts of the value witness like assignWithCopy. It's as if the implementation of Bar is being copied into Foo. This was all tested with Xcode 13 RC.
I thought outlining would have taken care of any duplicate instructions here, but even though I'm compiling with -Osize and can verify _OUTLIINED_FUNCTION_* symbols in the binary, the overall size of the __text section is still increasing by about the size of Bar's value witness in this example.
I also tried moving around the order of properties in case there was an offset causing the new instructions to be different, but got the same result with this struct:
struct Foo {
let prop2: Bar
let prop1: String
}
A quick look in Hopper also indicates very similar instructions:
sub sp, sp, #0xb0
stp x28, x27, [sp, #0x50]
stp x26, x25, [sp, #0x60]
stp x24, x23, [sp, #0x70]
stp x22, x21, [sp, #0x80]
stp x20, x19, [sp, #0x90]
stp x29, x30, [sp, #0xa0]
add x29, sp, #0xa0
mov x19, x0
ldp x8, x0, [x1]
stp x8, x0, [x19]
ldp x8, x20, [x1, #0x10]
stp x8, x20, [x19, #0x10]
ldp x8, x21, [x1, #0x20]
stp x8, x21, [x19, #0x20]
vs.
sub sp, sp, #0xc0
stp x28, x27, [sp, #0x60]
stp x26, x25, [sp, #0x70]
stp x24, x23, [sp, #0x80]
stp x22, x21, [sp, #0x90]
stp x20, x19, [sp, #0xa0]
stp x29, x30, [sp, #0xb0]
add x29, sp, #0xb0
mov x19, x0
ldp x8, x0, [x1]
stp x8, x0, [x19]
ldp x8, x20, [x1, #0x10]
stp x8, x20, [x19, #0x10]
ldp x8, x21, [x1, #0x20]
stp x8, x21, [x19, #0x20]
It doesn't seem like this increase in size should be necessary, are there any tricks to reducing the size impact in cases like this, or any compiler flags to re-use some of these instructions instead of generating them twice? Could anyone help me understand why outlining doesn't handle this?
Thanks!