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!