How do I put my Swift libraries on a diet?

Folks,

I have a binding to the Godot engine for Swift, which surfaces about a thousand types from Godot to Swift, with over 14,000 public functions and properties. This in practice translates into a shared library of about 111 megs of size and a static library of about the same size.

Up until now, if a user wrote some game code that called a handful of APIs, they had to either (a) bundle my chubby 100 meg set of dylibs, or bundle a 100 meg plugin for the luxury of using Swift. Either way is a library so large that only a mother can truly love it.

The Rust users on the other hand, can link their code with the Rust bindings and when all is said and done, a common extension will consume just what you use, so somewhere in the ballpark of 4-5 megs (your code, the glue and the rest).

First thought, “You know what, if I can just slice and dice these 100 megabytes worth of code into different libraries, at least users would be able to make their own adventure”. But SwiftPM seems to have been designed in such a way that this is not possible today - at least not without hiring a beowulf cluster of interns to maintain the resulting mess – you can read more about that whole saga here [1], but today’s post is not about that.

That fancy solution was not going to work.

Being a man of possibility, I reached for Plan B, let us just link the code with a static library and call it a day, and post some nice numbers about how I beat Rust with Swift. But alas, static linking in Swift is not what I have come to know and love from the C world - where only the stuff you use gets included in the final binary. It seems like Swift’s version of static linking is to bring everything whether your code uses it or not.

After extensive googling and spending every fancy AI token I could get my hands on, it does not seem like this is possible to do - but the information seems spotty and out of date in various places, and I am reaching out to my fellow human experts for help on this matter.

If I write a small sample program that merely prints out Node.self(a type from my library), the prices I pay, based on the approach I use are as follows:

  • Passing -dead_strip, -no_exported_symbols to the linker for my executable do not seem to do anything. The linker will still include everything in there. 100 megs.
  • If I compile SwiftGodot’s static library and my target, with the experimental flag -experimental-hermetic-seal-at-link along with -lto=llvm-full, does nothing. Also 100 megs binary.
  • If I compile my binary with -internalize-at-link and -lto=llvm-full I see some wins, it now gets to 23 megs.

I am not sure what the last one does, but every single type remains in there - things that nothing references and can not be possibly reached via any path.

There is a very old thread that touches on some of these things, but it seems to have died two years ago:

And also this one:

So I would love to know what is the state of the art when it comes to Swift to link with static libraries that might be binding large API surfaces but only small chunks are needed - I imagine the server people will have a similar set of challenges.

If anyone is curious, here is my current attempt:

Once you download that, checkout the linker-reduction-attempts branch and the sample in “Sample2”

Miguel

[1] [Pitch] SwiftPM: Allow targets to depend on products in the same package - #40 by migueldeicaza - funnily this used to work in Swift 6.1, but in Swift 6.2 the feature broke and crashes Xcode as well, I suspect it only worked by accident.

24 Likes

I apologize in advance, as I will not be of any help today.

However, I though you should know that this line:

made me laugh out loud so hard that my kids came into the office to check on me ^^

7 Likes

Is this a bug (that statically linked code is not stripped down to what's used) or a "feature" –still a bug but virtually impossible to fix due to language design?

I don’t have a real answer for you here, but I wanted to point you at Determining Why a Symbol is Referenced, which explains the linker’s -why_live option. I’m curious to see if these symbols are live because they’ve actually been marked as no_dead_strip, and whether the -internalize-at-link changes that.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

3 Likes

I don’t have a real answer for you here, but I wanted to point you at Determining Why a Symbol is Referenced, which explains the linker’s -why_live option. I’m curious to see if these symbols are live because they’ve actually been marked as no_dead_strip, and whether the -internalize-at-link changes that.

Thank you for the pointer to this -why_live option, that was great.

I started with a funky symbol that should not have been there, and it found a root, then I picked that root, and it looks like the dont-dead-strip flag is still the cause:

_$s10SwiftGodot10OpenXRHandC10BoneUpdateOSYAAMA from OpenXRHand.swift.o
  dont-dead-strip
_$s10SwiftGodot10OpenXRHandC10BoneUpdateOSYAAMA from OpenXRHand.swift.o
  _$s10SwiftGodot10OpenXRHandC10BoneUpdateOSYAAMA from OpenXRHand.swift.o
_$s10SwiftGodot10OpenXRHandC10BoneUpdateOSYAAMA from OpenXRHand.swift.o
  _$s10SwiftGodot10OpenXRHandC10BoneUpdateOSYAAMA from OpenXRHand.swift.o
_$s10SwiftGodot10OpenXRHandC10BoneUpdateOSYAAMA from OpenXRHand.swift.o
  _$s10SwiftGodot10OpenXRHandC10BoneUpdateOSYAAMA from OpenXRHand.swift.o
_$s10SwiftGodot10OpenXRHandC10BoneUpdateOSYAAMA from OpenXRHand.swift.o
  _$s10SwiftGodot10OpenXRHandC10BoneUpdateOSYAAMA from OpenXRHand.swift.o

Demangled, it looks like this is:

reflection metadata associated type descriptor SwiftGodot.OpenXRHand.BoneUpdate : Swift.RawRepresentable in SwiftGodot

(Edited for clarity, full version)

There are 607 symbols in this file as reported by nm -m, and 56 of them are flagged with no dead strip according to it raw version:

0000000000008be8 (__TEXT,__swift5_fieldmd) non-external [no dead strip] reflection metadata field descriptor SwiftGodot.OpenXRHand.BoneUpdate
0000000000008708 (__TEXT,__swift5_assocty) non-external [no dead strip] reflection metadata associated type descriptor SwiftGodot.OpenXRHand.BoneUpdate : Swift.RawRepresentable in SwiftGodot
0000000000008720 (__TEXT,__swift5_assocty) non-external [no dead strip] reflection metadata associated type descriptor SwiftGodot.OpenXRHand.BoneUpdate : Swift.CaseIterable in SwiftGodot
0000000000008b80 (__TEXT,__swift5_fieldmd) non-external [no dead strip] reflection metadata field descriptor SwiftGodot.OpenXRHand.MotionRange
00000000000086a8 (__TEXT,__swift5_assocty) non-external [no dead strip] reflection metadata associated type descriptor SwiftGodot.OpenXRHand.MotionRange : Swift.RawRepresentable in SwiftGodot
00000000000086c0 (__TEXT,__swift5_assocty) non-external [no dead strip] reflection metadata associated type descriptor SwiftGodot.OpenXRHand.MotionRange : Swift.CaseIterable in SwiftGodot
0000000000008bb4 (__TEXT,__swift5_fieldmd) non-external [no dead strip] reflection metadata field descriptor SwiftGodot.OpenXRHand.SkeletonRig
00000000000086d8 (__TEXT,__swift5_assocty) non-external [no dead strip] reflection metadata associated type descriptor SwiftGodot.OpenXRHand.SkeletonRig : Swift.RawRepresentable in SwiftGodot
00000000000086f0 (__TEXT,__swift5_assocty) non-external [no dead strip] reflection metadata associated type descriptor SwiftGodot.OpenXRHand.SkeletonRig : Swift.CaseIterable in SwiftGodot
0000000000018e38 (__DATA,__bss) non-external [no dead strip] static SwiftGodot.OpenXRHand.(method_get_hand in _0B6DE4D26A8E536FD53858E8549F662E) : Swift.UnsafeRawPointer
0000000000018e28 (__DATA,__bss) non-external [no dead strip] static SwiftGodot.OpenXRHand.(method_set_hand in _0B6DE4D26A8E536FD53858E8549F662E) : Swift.UnsafeRawPointer
0000000000018eb8 (__DATA,__bss) non-external [no dead strip] static SwiftGodot.OpenXRHand.(method_get_bone_update in _0B6DE4D26A8E536FD53858E8549F662E) : Swift.UnsafeRawPointer
0000000000018ea8 (__DATA,__bss) non-external [no dead strip] static SwiftGodot.OpenXRHand.(method_set_bone_update in _0B6DE4D26A8E536FD53858E8549F662E) : Swift.UnsafeRawPointer
0000000000018e78 (__DATA,__bss) non-external [no dead strip] static SwiftGodot.OpenXRHand.(method_get_motion_range in _0B6DE4D26A8E536FD53858E8549F662E) : Swift.UnsafeRawPointer
0000000000018e98 (__DATA,__bss) non-external [no dead strip] static SwiftGodot.OpenXRHand.(method_get_skeleton_rig in _0B6DE4D26A8E536FD53858E8549F662E) : Swift.UnsafeRawPointer
0000000000018e68 (__DATA,__bss) non-external [no dead strip] static SwiftGodot.OpenXRHand.(method_set_motion_range in _0B6DE4D26A8E536FD53858E8549F662E) : Swift.UnsafeRawPointer
0000000000018e88 (__DATA,__bss) non-external [no dead strip] static SwiftGodot.OpenXRHand.(method_set_skeleton_rig in _0B6DE4D26A8E536FD53858E8549F662E) : Swift.UnsafeRawPointer
0000000000018e58 (__DATA,__bss) non-external [no dead strip] static SwiftGodot.OpenXRHand.(method_get_hand_skeleton in _0B6DE4D26A8E536FD53858E8549F662E) : Swift.UnsafeRawPointer
0000000000018e48 (__DATA,__bss) non-external [no dead strip] static SwiftGodot.OpenXRHand.(method_set_hand_skeleton in _0B6DE4D26A8E536FD53858E8549F662E) : Swift.UnsafeRawPointer
0000000000008b4c (__TEXT,__swift5_fieldmd) non-external [no dead strip] reflection metadata field descriptor SwiftGodot.OpenXRHand.Hands
0000000000008678 (__TEXT,__swift5_assocty) non-external [no dead strip] reflection metadata associated type descriptor SwiftGodot.OpenXRHand.Hands : Swift.RawRepresentable in SwiftGodot
0000000000008690 (__TEXT,__swift5_assocty) non-external [no dead strip] reflection metadata associated type descriptor SwiftGodot.OpenXRHand.Hands : Swift.CaseIterable in SwiftGodot
0000000000018e18 (__DATA,__bss) non-external [no dead strip] static SwiftGodot.OpenXRHand.(className in _0B6DE4D26A8E536FD53858E8549F662E) : SwiftGodot.StringName
0000000000008b3c (__TEXT,__swift5_fieldmd) non-external [no dead strip] reflection metadata field descriptor SwiftGodot.OpenXRHand
0000000000008290 (__TEXT,__const) weak private external [no dead strip] ___swift_reflection_version
0000000000008e80 (__DATA,__const) weak private external [no dead strip] __swift_FORCE_LOAD_$_swiftCoreFoundation_$_SwiftGodot
0000000000008e88 (__DATA,__const) weak private external [no dead strip] __swift_FORCE_LOAD_$_swiftDispatch_$_SwiftGodot
0000000000008e70 (__DATA,__const) weak private external [no dead strip] __swift_FORCE_LOAD_$_swiftFoundation_$_SwiftGodot
0000000000008e98 (__DATA,__const) weak private external [no dead strip] __swift_FORCE_LOAD_$_swiftIOKit_$_SwiftGodot
0000000000008e78 (__DATA,__const) weak private external [no dead strip] __swift_FORCE_LOAD_$_swiftObjectiveC_$_SwiftGodot
0000000000008e68 (__DATA,__const) weak private external [no dead strip] __swift_FORCE_LOAD_$_swiftUniformTypeIdentifiers_$_SwiftGodot
0000000000008e90 (__DATA,__const) weak private external [no dead strip] __swift_FORCE_LOAD_$_swiftXPC_$_SwiftGodot
0000000000008e60 (__DATA,__const) weak private external [no dead strip] __swift_FORCE_LOAD_$_swift_Builtin_float_$_SwiftGodot
0000000000008f98 (__DATA,__objc_classlist) non-external [no dead strip] _objc_classestype metadata for SwiftGodot.OpenXRHand
0000000000008f80 (__TEXT,__swift5_types) non-external [no dead strip] lnominal type descriptor runtime record for SwiftGodot.OpenXRHand.BoneUpdate
0000000000008f64 (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.BoneUpdate : Swift.Hashable in SwiftGodot
0000000000008f60 (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.BoneUpdate : Swift.Equatable in SwiftGodot
0000000000008f68 (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.BoneUpdate : Swift.RawRepresentable in SwiftGodot
0000000000008f6c (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.BoneUpdate : Swift.CaseIterable in SwiftGodot
0000000000008f78 (__TEXT,__swift5_types) non-external [no dead strip] lnominal type descriptor runtime record for SwiftGodot.OpenXRHand.MotionRange
0000000000008f44 (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.MotionRange : Swift.Hashable in SwiftGodot
0000000000008f40 (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.MotionRange : Swift.Equatable in SwiftGodot
0000000000008f48 (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.MotionRange : Swift.RawRepresentable in SwiftGodot
0000000000008f4c (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.MotionRange : Swift.CaseIterable in SwiftGodot
0000000000008f7c (__TEXT,__swift5_types) non-external [no dead strip] lnominal type descriptor runtime record for SwiftGodot.OpenXRHand.SkeletonRig
0000000000008f54 (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.SkeletonRig : Swift.Hashable in SwiftGodot
0000000000008f50 (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.SkeletonRig : Swift.Equatable in SwiftGodot
0000000000008f58 (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.SkeletonRig : Swift.RawRepresentable in SwiftGodot
0000000000008f5c (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.SkeletonRig : Swift.CaseIterable in SwiftGodot
0000000000008f74 (__TEXT,__swift5_types) non-external [no dead strip] lnominal type descriptor runtime record for SwiftGodot.OpenXRHand.Hands
0000000000008f34 (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.Hands : Swift.Hashable in SwiftGodot
0000000000008f30 (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.Hands : Swift.Equatable in SwiftGodot
0000000000008f38 (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.Hands : Swift.RawRepresentable in SwiftGodot
0000000000008f3c (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.Hands : Swift.CaseIterable in SwiftGodot
0000000000008f70 (__TEXT,__swift5_types) non-external [no dead strip] lnominal type descriptor runtime record for SwiftGodot.OpenXRHand
0000000000008f84 (__LLVM,__swift_modhash) non-external [no dead strip] l_llvm.swift_module_hash

The version that does not compile the library with -internalize-at-link has 171 symbols that are flagged with no dead strip, it is large, so I pasted it on github instead.

With the information that so much is coming from fields named ‘reflection’, I Googled, and it looks like I can disable reflection in Swift using -disable-reflection-metadata, this reduced the number of symbols flagged as no dead strip from 56 to 42, but the binary remains at 23 megs, so this was not sufficient.

There are the remaining no dead strip that keep pulling the code:

0000000000018be8 (__DATA,__bss) non-external [no dead strip] static SwiftGodot.OpenXRHand.(method_get_hand in _0B6DE4D26A8E536FD53858E8549F662E) : Swift.UnsafeRawPointer
0000000000018bd8 (__DATA,__bss) non-external [no dead strip] static SwiftGodot.OpenXRHand.(method_set_hand in _0B6DE4D26A8E536FD53858E8549F662E) : Swift.UnsafeRawPointer
0000000000018c68 (__DATA,__bss) non-external [no dead strip] static SwiftGodot.OpenXRHand.(method_get_bone_update in _0B6DE4D26A8E536FD53858E8549F662E) : Swift.UnsafeRawPointer
0000000000018c58 (__DATA,__bss) non-external [no dead strip] static SwiftGodot.OpenXRHand.(method_set_bone_update in _0B6DE4D26A8E536FD53858E8549F662E) : Swift.UnsafeRawPointer
0000000000018c28 (__DATA,__bss) non-external [no dead strip] static SwiftGodot.OpenXRHand.(method_get_motion_range in _0B6DE4D26A8E536FD53858E8549F662E) : Swift.UnsafeRawPointer
0000000000018c48 (__DATA,__bss) non-external [no dead strip] static SwiftGodot.OpenXRHand.(method_get_skeleton_rig in _0B6DE4D26A8E536FD53858E8549F662E) : Swift.UnsafeRawPointer
0000000000018c18 (__DATA,__bss) non-external [no dead strip] static SwiftGodot.OpenXRHand.(method_set_motion_range in _0B6DE4D26A8E536FD53858E8549F662E) : Swift.UnsafeRawPointer
0000000000018c38 (__DATA,__bss) non-external [no dead strip] static SwiftGodot.OpenXRHand.(method_set_skeleton_rig in _0B6DE4D26A8E536FD53858E8549F662E) : Swift.UnsafeRawPointer
0000000000018c08 (__DATA,__bss) non-external [no dead strip] static SwiftGodot.OpenXRHand.(method_get_hand_skeleton in _0B6DE4D26A8E536FD53858E8549F662E) : Swift.UnsafeRawPointer
0000000000018bf8 (__DATA,__bss) non-external [no dead strip] static SwiftGodot.OpenXRHand.(method_set_hand_skeleton in _0B6DE4D26A8E536FD53858E8549F662E) : Swift.UnsafeRawPointer
0000000000018bc8 (__DATA,__bss) non-external [no dead strip] static SwiftGodot.OpenXRHand.(className in _0B6DE4D26A8E536FD53858E8549F662E) : SwiftGodot.StringName
0000000000008c30 (__DATA,__const) weak private external [no dead strip] __swift_FORCE_LOAD_$_swiftCoreFoundation_$_SwiftGodot
0000000000008c38 (__DATA,__const) weak private external [no dead strip] __swift_FORCE_LOAD_$_swiftDispatch_$_SwiftGodot
0000000000008c20 (__DATA,__const) weak private external [no dead strip] __swift_FORCE_LOAD_$_swiftFoundation_$_SwiftGodot
0000000000008c48 (__DATA,__const) weak private external [no dead strip] __swift_FORCE_LOAD_$_swiftIOKit_$_SwiftGodot
0000000000008c28 (__DATA,__const) weak private external [no dead strip] __swift_FORCE_LOAD_$_swiftObjectiveC_$_SwiftGodot
0000000000008c18 (__DATA,__const) weak private external [no dead strip] __swift_FORCE_LOAD_$_swiftUniformTypeIdentifiers_$_SwiftGodot
0000000000008c40 (__DATA,__const) weak private external [no dead strip] __swift_FORCE_LOAD_$_swiftXPC_$_SwiftGodot
0000000000008c10 (__DATA,__const) weak private external [no dead strip] __swift_FORCE_LOAD_$_swift_Builtin_float_$_SwiftGodot
0000000000008d48 (__DATA,__objc_classlist) non-external [no dead strip] _objc_classestype metadata for SwiftGodot.OpenXRHand
0000000000008d30 (__TEXT,__swift5_types) non-external [no dead strip] lnominal type descriptor runtime record for SwiftGodot.OpenXRHand.BoneUpdate
0000000000008d14 (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.BoneUpdate : Swift.Hashable in SwiftGodot
0000000000008d10 (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.BoneUpdate : Swift.Equatable in SwiftGodot
0000000000008d18 (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.BoneUpdate : Swift.RawRepresentable in SwiftGodot
0000000000008d1c (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.BoneUpdate : Swift.CaseIterable in SwiftGodot
0000000000008d28 (__TEXT,__swift5_types) non-external [no dead strip] lnominal type descriptor runtime record for SwiftGodot.OpenXRHand.MotionRange
0000000000008cf4 (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.MotionRange : Swift.Hashable in SwiftGodot
0000000000008cf0 (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.MotionRange : Swift.Equatable in SwiftGodot
0000000000008cf8 (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.MotionRange : Swift.RawRepresentable in SwiftGodot
0000000000008cfc (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.MotionRange : Swift.CaseIterable in SwiftGodot
0000000000008d2c (__TEXT,__swift5_types) non-external [no dead strip] lnominal type descriptor runtime record for SwiftGodot.OpenXRHand.SkeletonRig
0000000000008d04 (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.SkeletonRig : Swift.Hashable in SwiftGodot
0000000000008d00 (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.SkeletonRig : Swift.Equatable in SwiftGodot
0000000000008d08 (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.SkeletonRig : Swift.RawRepresentable in SwiftGodot
0000000000008d0c (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.SkeletonRig : Swift.CaseIterable in SwiftGodot
0000000000008d24 (__TEXT,__swift5_types) non-external [no dead strip] lnominal type descriptor runtime record for SwiftGodot.OpenXRHand.Hands
0000000000008ce4 (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.Hands : Swift.Hashable in SwiftGodot
0000000000008ce0 (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.Hands : Swift.Equatable in SwiftGodot
0000000000008ce8 (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.Hands : Swift.RawRepresentable in SwiftGodot
0000000000008cec (__TEXT,__swift5_proto) non-external [no dead strip] lprotocol conformance descriptor runtime record for SwiftGodot.OpenXRHand.Hands : Swift.CaseIterable in SwiftGodot
0000000000008d20 (__TEXT,__swift5_types) non-external [no dead strip] lnominal type descriptor runtime record for SwiftGodot.OpenXRHand
0000000000008d34 (__LLVM,__swift_modhash) non-external [no dead strip] l_llvm.swift_module_hash

The classification is roughly like this:

  • 10 file private methods are flagged in this way
  • One private static variable classNameis flagged in this way
  • 8 symbols prefixed __swift_FORCE_LOAD_$ and suffixed $_SwiftGodot
  • 1 _objc_classestype metadata for SwiftGodot.OpenXRHand
  • 21 ‘descriptor runtime records' for public enums defined inside that class

This is how this one method:

0000000000018bd8 (__DATA,__bss) non-external [no dead strip] static SwiftGodot.OpenXRHand.(method_set_hand in _0B6DE4D26A8E536FD53858E8549F662E) : Swift.UnsafeRawPointer

Is defined in Swift:

    fileprivate static let method_set_hand: GDExtensionMethodBindPtr = {
        var methodName = FastStringName("set_hand")
        return withUnsafePointer(to: &OpenXRHand.godotClassName.content) { classPtr in
            withUnsafePointer(to: &methodName.content) { mnamePtr in
                gi.classdb_get_method_bind(classPtr, mnamePtr, 1849328560)!
            }
            
        }
        
    }()

This method is invoked by the property named hand, which is curiously not flagged as no dead strip, so the property that calls it manages to get yanked, and I can see no trace of it on the resulting executable, but the fileprivate definition for method_set_hand does make it into the final executable. I end up with some 26,000 of those :-)

This does feel like a bug.

4 Likes

libSwiftGodot.so comes out to 21,209,280 bytes (20.2 MiB / 21.2 MB) on my Linux machine when built in release mode with no other special flags and stripped (suitable for production; which isn't that bad for a whole game engine in my opinion). with the 6.2-release toolchain

You could definitely shave it down further if you adopt package traits (requires Swift 6.1) for the more "heavy" code.

From a quick skim over the project (and using nm -S --size-sort) I can see some package traits applicable to reduce the binary further:

  • CustomStringConvertible trait for generated description
  • CustomDebugStringConvertible trait for generated debugDescription
  • a trait to enable deprecations
  • reflection
  • a trait to enable print (or use #if DEBUG?)
  • static let
  • traits for conformances to RawRepresentable

However, I do think Swift should strip "dead" code more aggressively since the burden of optimizing binary size currently largely falls upon the developer and their ability to tinker with Swift.

1 Like

Thanks for these suggestions.

libSwiftGodot.so comes out to 21,209,280 bytes (20.2 MiB / 21.2 MB) on my Linux machine when built in release mode with no other special flags and stripped (suitable for production; which isn't that bad for a whole game engine in my opinion).

I am glad to hear this works better for the Linux crew, on MacOS, building in release mode and stripping the symbols still left me with a 90 meg build sadly.

I will check out Linux, as the MacOS version of nm -S –size-sort does not seem to be printing out the symbol sizes, so I am a bit blind here, so that is a good pointer for some possible savings.

I tried traits in a separate branch, that is one of the various hacks I tried (traits-sizes branch) - the experience with Xcode is not great. I took a different approach, trying to come up with “profiles” that made some sort of sense, but the poor integration into Xcode also require creating a constellation of Package.swifts for common idioms.

One observation: I only declare some 40 or so description, debugDescriptionand conformances are for some enumerations, I wonder what you saw that is bringing so much stuff in here.

Also, what do you mean by static let and RawRepresentable?

Update: I have found a source that prevents code from being linked out, it seems like static members of a class force the method to be flagged with `no dead strip`. I am going to look at restructuring this code to avoid the statics and see if that helps.

Is _every_ static member flagged with no dead strip? Or just a portion of those?

Another update, removing the static methods has helped a bit, I ended up rewriting the code and inlining the job these static methods were doing into the callsite, and I got some nice savings for the debug build, it went from 22,996,656 to 18,249,488 bytes.

A release build after stripping gets to 8,493,328, better, but still 4.3 megs away from Rust.

It seems like a large portion of my bloat 3,740,124 bytes worth of it is due to a virtual method that ends up pulling every class that implements it.

I have put together a small testing project where I am trying different flags:

I tried these flags for the final executable:

        .executableTarget(
            name: "ConsumerStrip",
            dependencies: ["SimpleLibraryStrip"],
            swiftSettings: [
                .unsafeFlags([
                    "-Xfrontend", "-internalize-at-link",
                    "-Xfrontend", "-lto=llvm-full",
                    "-Xfrontend", "-disable-reflection-metadata",
                    "-Xfrontend", "-experimental-hermetic-seal-at-link"
                ])
            ],
            linkerSettings: [
                .unsafeFlags([
                    "-Xlinker", "-dead_strip",
                    "-Xlinker", "-no_exported_symbols",
                    // For quickly figuring out why something is getting pulled
                    "-Xlinker", "-why_live",
                    "-Xlinker", "foo_bar"
                ])
            ]
        ),

But even those do not manage to remove the virtual method definition.

Using why_live for the symbol of my base class definition of the virtual method UBCLASSABLE.OVERRIDE_ME_MAYBE` shows this reference chain:

SimpleLibraryStrip.SUBCLASSABLE.OVERRIDE_ME_MAYBE() -> () from SimpleLibrary.swift.o
  full type metadata for SimpleLibraryStrip.SUBCLASSABLE from SimpleLibrary.swift.o
    type metadata accessor for SimpleLibraryStrip.SUBCLASSABLE from SimpleLibrary.swift.o
      nominal type descriptor for SimpleLibraryStrip.SUBCLASSABLE from SimpleLibrary.swift.o
        lnominal type descriptor runtime record for SimpleLibraryStrip.SUBCLASSABLE from SimpleLibrary.swift.o
SimpleLibraryStrip.SUBCLASSABLE.OVERRIDE_ME_MAYBE() -> () from SimpleLibrary.swift.o
  nominal type descriptor for SimpleLibraryStrip.SUBCLASSABLE from SimpleLibrary.swift.o
    lnominal type descriptor runtime record for SimpleLibraryStrip.SUBCLASSABLE from SimpleLibrary.swift.o

@Joannis_Orlandos I can see other static members that do not get that treatment, lots of them look to be swift related. I suspect ‘final’ on a file private might also help not flag it.

5 Likes

Not sure if this will help @migueldeicaza but you might want to try the emerge tools (https://www.emergetools.com) to analyse and help slim down the SDK size.

One thing I do know can contribute a lot to larger apps/SDKs is a lot of protocol conformances. So perhaps cutting back on that might help?

Examples: