How can I reduce the size of a swift application compiled as a static executable?

I've compiled a simple hello world application as a static executable. The size of the executable is massive at 38 megabytes.

$ cat main.swift
print("Hello, world!")

$ ls -la
total 37532
drwxr-xr-x. 2 root root       36 Aug  2 03:20 .
drwxr-xr-x. 3 root root       19 Jul 16 09:53 ..
-rwxr-xr-x. 1 root root 38428024 Aug  2 03:20 main
-rw-r--r--. 1 root root       23 Aug  2 03:12 main.swift

$ file main
main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, for GNU/Linux 3.2.0, with debug_info, not stripped

Enabling size and link-time optimizations only marginally reduces the size of the final executable.

$ swiftc -static-executable -Osize -lto=llvm-full main.swift
clang-10: warning: argument unused during compilation: '-pie' [-Wunused-command-line-argument]
$ ls -la
total 37296
drwxr-xr-x. 2 root root       36 Aug  2 03:22 .
drwxr-xr-x. 3 root root       19 Jul 16 09:53 ..
-rwxr-xr-x. 1 root root 38185968 Aug  2 03:22 main
-rw-r--r--. 1 root root       23 Aug  2 03:12 main.swift

Stripping the binary, again, only marginally reduces the size.

$ strip main
$ ls -la
total 34056
drwxr-xr-x. 2 root root       36 Aug  2 03:27 .
drwxr-xr-x. 3 root root       19 Jul 16 09:53 ..
-rwxr-xr-x. 1 root root 34868904 Aug  2 03:27 main
-rw-r--r--. 1 root root       23 Aug  2 03:12 main.swift
  • Is this normal?
  • Are there ways to trim the size of the binary?

I'm concerned that this might impact the distribution of binaries.

3 Likes

I've built dynamically linked versions of "Hello World" on macOS/Linux, and then, also built a static version on Linux (macOS' Swift compiler doesn't permit building statically linked files).

The dynamically linked binaries are small. 50k on macOS and 15k on Linux.

The statically linked binary on Linux (Ubuntu 20.04, amd64) comes in at 37M.

I think that has to be expected, since you are including all libraries -- not just the ones which are necessary for the print statement. To give you an idea what is included I ran nm:

  • 72 symbols in the dynamically linked binary (nm main-dyn | wc -l)
  • 40413 symbols in the statically linked binary (nm main | wc -l)

I have built statically linked binaries for C/C++ code + libpython on Linux beforehand and those came in at around 5MB. My point is: I don't think that 38M is "too much" -- if you really want to build and deploy your code statically linked. Those 38M are pretty much 99.99...% other developers' code and your own code contributions will not blow up the size of this binary much further. You would have to write quite a lot of code to just increase this binary by 1M.

Considering that most software nowadays comes in +100M and sometimes even in the gigabytes, I wouldn't be too worried about 38M.

Kim

I think you're comparing apples to oranges.

Swift.org states that the goal of the language is to help developers to "turn their ideas into apps on any platform." Apps are generally understood to be user-facing software nowadays, so anything with a GUI really. And, there are far more GUI applications out there than command-line tools.

Comparing 38M of a statically-linked Swift binary to the size of command-line tools is misleading. Yes, the Swift linker currently pulls in too many dependencies that are not needed -- for a "Hello World" program. Yes, Linux (or macOS) command-line tools are a lot smaller even when statically linked -- since they are written in a language that build on top of assembly and its standard libraries from the 70s/80s are specifically tailored to do one-and-only-one-thing very well. POSIX is 33 years old now.

However, when you look at alternative solutions for building apps (Qt, Electron, etc.), then 38M as a baseline is suddenly not so big anymore (it's actually rather small). Only Apple's own Objective-C apps are smaller than 38M on my laptop, whereas all other apps (Slack, Discord, Asana, etc.) clock in at +100M.

Does Swift need 38M to write "Hello world!" to a file descriptor? No! Writing to a file descriptor as a self-contained statically-linked command-line tool isn't what Swift tries to focus on though. At least not in my opinion. Swift isn't trying to compete with C, libc, and all the other highly tailored libraries for low-level tool development.

Kim

1 Like

While Swift is pulling in a number of imports that you may not be immediately using (like the Concurrency library is automatically imported on modern versions of Swift whether you're using concurrency features or not -- but that's only about 1M) it also introduces a lot of functionality and features by default that necessitates additional libraries. A lot of the Unicode support for example, is baked into the language, which is implemented with the ICU libs which seem pretty large as it is. The ICU static libs total at around 36M already. swiftCore is 11M.

The problem isn't so much that Golang or Rust are generating smaller binaries but that Swift has implicit library dependencies that are large already as it is. I'd be curious to see how big a Go or Rust binary would be that heavily utilized libicu.

5 Likes

I remember there being other threads in this forum discussing binary size reduction and the future of the libicu dependency more specifically. You might want to look at those discussions, I can’t link you to them right now though (on the phone).

2 Likes

Hmm -- I thought that you could disable concurrency features at some point, but I might only be remembering when they were available on an experimental basis, sorry. In any case, that wouldn't be viable as a long term strategy, especially now concurrency is part of the language.

As @odmir says, this has been discussed before. If it is a particular concern for you, perhaps you can chip in with slimming that down.

On the other hand, if you're deploying on the server, it shouldn't be hard to make sure that your linux distro has the system Swift package installed or to otherwise provision it with the Swift runtime as shared libraries that your app uses, assuming you have some control of that server.

It is not clear why you insist on the minority usecase of building a tiny static executable, which Go and Rust have focused on but Swift hasn't.

SwiftWasm faces the same binary size problem (relevant issue thread on GitHub), and there had been discussions on using system-included ICU library, instead of bundling one.

CC @Max_Desiatov and @kateinoigakukun, maybe there is some experience or solutions that you can share?

1 Like
  • I'm not going to spend months or years working on Swift for the sole goal of generating smaller binaries. Is this even a serious suggestion?

Nobody is asking you to work on smaller codegen, my first link above says that the main problem is statically linking in the ICU dependency. My guess is that implementing @compnerd's libicu suggestions from my second link would take something on the order of weeks, perhaps days if you really knew what you were doing.

The easiest option here is to just statically compile the program.

I doubt it: I would go with your third option, as Swift's static linking seems half-heartedly supported on linux (I think it's disabled on macOS, an OS I don't use). I tried using the -static-swift-stdlib option of the Swift package manager once and it didn't work too well, which won't work out well for you if you want to use the Swift package ecosystem.

I can't believe a simple question for whether or not it's possible to trim the size of a statically compiled binary was so controversial for this community and warrants so many defensive responses.

I don't see anything "controversial" or defensive about the community's response, we're just confused about why a tiny static executable is so important to you on the server. I'll note that you wrote a long response, but yet again did not answer my prompt of "why you insist on [that] minority usecase."

I think Swift static binaries could be optimized down to the level of Go and Rust, but nobody has bothered to.

Rather than continue the discussion down that niche path that Swift doesn't support yet, we're asking if you really need that because as @indiedotkim noted, binary size of 50-100 MB is generally not a concern on the server or desktop these days.

1 Like

AFAIK there are two main issues that cause huge binary sizes for statically linked projects:

  1. ICU
  2. Unused code in stdlib (and maybe other libraries?) doesn't get stripped out because there's no infrastructure to do so.

I don't know of any active work on resolving the first point, and @kateinoigakukun worked on the second point, and I'm not entirely sure what's the status of the latter.

1 Like

(post deleted by author)

1 Like

There's a lot of defensiveness in this thread in both directions. I understand your annoyance about people saying things you find irrelevant, and I agree that people shouldn't be telling you what you should care about. You've also gotten some specific help on your problem, and it's okay to just ignore the comments you don't find helpful, or to report them if you feel they're crossing a line. There's no call for taking broad shots at the community.

The vast majority of the size overhead of static linking is indeed ICU. Linkers often have tools for reporting where all the code in an executable comes from, which is the right place to start on this kind of investigation. I don't know how easy it would be to statically link everything except ICU, but that seems like it would largely satisfy your goals, since, unlike Swift, ICU is a relatively common dependency. Alternatively, I know there have been efforts to make a standard library that doesn't require ICU (and therefore skimps on Unicode support), although this might be undesirable when your program becomes less of a toy.

5 Likes