AsyncAlgorithms bloat persists even after stripping and link-time-optimizations

this isn’t a new problem, but i am surprised it persists to this day, and remains resistant to all of the remedies that were predicted to mitigate it.

i did an experiment on Swift 6.2.4 creating a minimal server application that depends on SwiftNIO and AsyncAlgorithms, and compared the size of the compiled binary when using AsyncAlgorithms, and when literally copy-pasting the source files of AsyncChannel into a vendored copy of the module within the project.

Ubuntu 24.04

stage size (bytes, AsyncAlgorithms) size (bytes, vendored copy)
original 47,813,400 44,327,624
after strip 11,327,736 9,975,752
with LTO 10,274,616 8,985,856

macOS 26

stage size (bytes, AsyncAlgorithms) size (bytes, vendored copy)
original 18,194,792 16,003,880
after strip 8,193,496 7,024,776
with LTO 8,348,232 7,248,088

aside from the strange situation where --experimental-lto-mode full actually makes the binary larger on macOS, it is apparent that vendoring the source files of AsyncChannel is still the clear winner by a considerable margin.

when i first encountered this problem years ago, i was advised that dead code/symbols stripping and the (at the time, upcoming) swift build --experimental-lto-mode full feature would resolve the problem by design. but that has not really panned out.

10 Likes

The only solution I've found to reduce binary size of projects is to use package traits for every offending piece of code. That is the main reason why I wrote my own networking library without SwiftNIO and Foundation, and the minimal server I wrote using it came under 1 MB (without explicit flags or special linking; could shave it down to < 500 KiB if you disabled some package traits, suitable for embedded).

With Swift 6.3 around the corner and 6.1 becoming the last 'supported' minor release, I really hope more packages utilize package traits. Some of the popular libraries already require 6.1 or later yet they don't use them... :frowning:

With Xcode 26.4 finally adding support for them (though I don't know how good it is), they should be more viable in the near future. Biggest issue right now is that using traits in packages randomly breaks stuff in Xcode, so I've generally avoided them in my packages, and packages that use them.

1 Like

Note that this code bloat is something the Android workgroup has been discussing in recent months, and some of us want to look into more. @lukasa and @vedantk or @akyrtzi, are there any subject matter experts for this in the Swift project who we should be discussing our findings with?

It seems like this is a high-impact effort that would help all Swift platforms, such as @compnerd with Windows too if employing the right Windows-specific tools there, would be good to set up some knowledge-sharing and collaboration on this important issue @taylorswift brings up.

7 Likes

I’m honestly not sure who the best person to talk to is. I’d suggest we start with @tbkka and see if he has a suitable suggestion.

1 Like

I think this is a good problem to tackle. In my opinion, we should start off with creating tooling similar to bloaty that helps us analyze binaries and understand where the binary size is coming from. Potentially also writing a documentation article about this.

5 Likes

It looks like people are already using it on Swift, :grinning_face: @taylorswift, want to try it on your example and see what you find?

FWIW I added this to support a very early variant of embedded swift. It needs to be combined with --internalize-at-link and a couple other flags to really have an effect. Desktop swift today isn't able to dead strip as effectively as embedded due to things like reflection metadata. I'll avoid speaking more concretely as I don't understand the full details.

cc @Douglas_Gregor @Slava_Pestov might be able to explain what type metadata keeps "dead" code around. (or if thats even correct).


edit: 3-4 year old flag list:

-Ospeed
-Xfrontend -experimental-hermetic-seal-at-link
-whole-module-optimization
-Xfrontend -disable-reflection-metadata

today I think you might use: -enable-cmo-everything

1 Like

@taylorswift check internal sc-128 - we did try this and intended to have it in CI.

Basically bloaty 1.1 or later worked on macOS (but not on Linux) at that point in time (2023?).

It was possible to get output such as:


    FILE SIZE        VM SIZE    
 --------------  -------------- 
  10.2%  1.93Mi  10.0%  1.93Mi    [__LINKEDIT]
   5.8%  1.10Mi   5.7%  1.10Mi    /Users/hassila/GitHub/ordo/LocalPackages/Cache/Sources/Cache/CacheStorable.swift
   4.0%   777Ki   3.9%   777Ki    /Users/hassila/GitHub/ordo/Modules/Augmentation/Sources/Augmentation/Augmentation+Module.swift
   3.5%   686Ki   3.5%   686Ki    /Users/hassila/Library/Developer/Xcode/DerivedData/Ordo-gmejvaknewiunvewtbtnixiriytp/SourcePackages/checkouts/swift-lmdb/Sources/CLMDB/mdb.c
   2.2%   432Ki   2.2%   432Ki    [__TEXT,__eh_frame]
   2.2%   431Ki   2.2%   431Ki    [__TEXT,__cstring]
   2.0%   382Ki   1.9%   382Ki    [__TEXT,__text]
   1.8%   349Ki   1.8%   349Ki    /Users/hassila/Library/Developer/Xcode/DerivedData/Ordo-gmejvaknewiunvewtbtnixiriytp/SourcePackages/checkouts/swift-protobuf/Sources/SwiftProtobuf/descriptor.pb.swift
   0.3%  53.3Ki   1.7%   335Ki    /Users/hassila/Library/Developer/Xcode/DerivedData/Ordo-gmejvaknewiunvewtbtnixiriytp/SourcePackages/checkouts/swift-kafka-client/Sources/Crdkafka/librdkafka/src/rdkafka_sticky_assignor.c
   1.2%   239Ki   1.2%   239Ki    [__TEXT,__unwind_info]
   1.0%   199Ki   1.0%   199Ki    [__TEXT,__swift5_typeref]
   1.0%   197Ki   1.0%   197Ki    /Users/hassila/GitHub/ordo/LocalPackages/Cache/Sources/Cache/Cache.swift
   1.0%   191Ki   1.0%   191Ki    /Users/hassila/Library/Developer/Xcode/DerivedData/Ordo-gmejvaknewiunvewtbtnixiriytp/SourcePackages/checkouts/swift-kafka-client/Sources/Crdkafka/librdkafka/src/rdkafka_topic.c
   1.0%   191Ki   1.0%   191Ki    /Users/hassila/Library/Developer/Xcode/DerivedData/Ordo-gmejvaknewiunvewtbtnixiriytp/SourcePackages/checkouts/swift-kafka-client/Sources/Crdkafka/librdkafka/src/snappy.c


cli-developer-utilities git:(main) ✗ bloaty --demangle=full .build/debug/ordo-cache-tool 
    FILE SIZE        VM SIZE    
 --------------  -------------- 
  38.0%  7.80Mi  37.6%  7.80Mi    String Table
  34.6%  7.10Mi  34.3%  7.10Mi    __TEXT,__text
  17.3%  3.54Mi  17.1%  3.54Mi    Symbol Table
   3.6%   755Ki   3.6%   769Ki    [__LINKEDIT]
   0.0%       0   1.1%   227Ki    __DATA,__bss
   1.0%   212Ki   1.0%   212Ki    __TEXT,__eh_frame
   1.0%   202Ki   1.0%   202Ki    __TEXT,__const
   0.8%   175Ki   0.8%   175Ki    __DATA_CONST,__const
   0.8%   162Ki   0.8%   162Ki    Code Signature
   0.5%   102Ki   0.5%   102Ki    __TEXT,__unwind_info
   0.4%  91.0Ki   0.4%  87.4Ki    [26 Others]
   0.4%  81.1Ki   0.4%  81.1Ki    __TEXT,__cstring
   0.3%  64.2Ki   0.3%  64.2Ki    __DATA,__data
   0.3%  52.6Ki   0.2%  52.6Ki    __TEXT,__swift5_fieldmd
   0.2%  48.6Ki   0.2%  48.6Ki    Function Start Addresses
   0.2%  38.7Ki   0.2%  38.7Ki    __TEXT,__swift5_typeref
   0.2%  36.9Ki   0.2%  36.9Ki    __DATA,__objc_const
   0.2%  32.6Ki   0.2%  32.6Ki    __TEXT,__swift5_reflstr
   0.1%  18.4Ki   0.1%  18.4Ki    __TEXT,text_env
   0.1%  16.3Ki   0.1%  16.3Ki    __DATA_CONST,__got
   0.1%  15.5Ki   0.1%  15.5Ki    [__DATA_CONST]
 100.0%  20.5Mi 100.0%  20.7Mi    TOTAL

Overall this would be good - but given that bloaty works (good enough?!) - perhaps worth checking status and upstreaming any missing pieces? Not sure it helps for android/windows though…

1 Like

The best results (the smallest binary) I could get for an dynamic Android library, was with:

--Xswiftc -Osize
-Xlinker --version-script=my-exported-symbols.map 
--experimental-lto-mode=full
-Xfrontend -internalize-at-link

Does that look right? Am I missing anything?

Bloaty 1.1 is ~6years old so I recommend using the latest bloaty from source. A number of fixes for bloaty to work better with Swift binaries have landed recently, e.g. improved dwarf5 support, improved handling of dsyms, support for demangling Swift symbols. There’s a few pending pull requests as well that also improve bloaty support on macOS, such as support for archives, and fixes for vm addressing in universal binaries.

4 Likes