https://danielchasehooper.com/posts/why-swift-is-slow/ Daniel Hooper Home Posts Projects Twitter RSS Apple didn't fix Swift's biggest flaw June 12, 20245 minute read The Swift compiler is notoriously slow due to how types are inferred^ 1. Every June I hope that Apple will announce that they fixed it; sadly this is not that year. Here's an explanation by the creator of Swift, Chris Lattner (From his Mojo talk): My experience with Swift is we tried to make a really fancy bi-directional Hindley-Milner type checker and it's really great because you can have very beautiful minimal syntax but the problem is that A) compile times are really bad (particularly if you have complicated expressions) and B) the error messages are awful because now you have global constraint systems and when something goes wrong you have to infer what happened and the user can't know that something over there made it so something over here can't type check. In my experience it sounds great but it doesn't work super well. Let me explain what he means with an example: enum ThreatLevel { case red case midnight } enum KeyTime { case midnight case midday } func setThreatLevel(_ level: ThreatLevel) {...} setThreatLevel(.midnight) The .midnight on the last line could represent ThreatLevel.midnight or KeyTime.midnight. The Swift compiler has to use the surrounding context of setThreatLevel(), which has the type (ThreatLevel)->Void, to infer that we mean ThreatLevel.midnight. After the Swift compiler parses code into an abstract syntax tree, child nodes influence their parent's type and parent nodes influence their children's types (that's what Chris means by "bi-directional"). Compare this to the Zig language, in which types are determined without looking at the surrounding code. This approach becomes a problem when expressions contain many elements that each need their types inferred, with each affecting the others. This often occurs due to Swift's operator overloading, and the ExpressibleBy protocols. Every literal (string, number, boolean, dictionary, array) and every operator (* / + - etc) multiply the combinations the type checker must consider. Here's an example: let address = "127.0.0.1" let username = "steve" let password = "1234" let channel = 11 let url = "http://" + username + ":" + password + "@" + address + "/api/" + channel + "/picture" print(url) swiftc spends 42 seconds on these 12 lines on an M1 Pro^2, only to spit out the notorious error: the compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions. In the same amount of time, clang can perform a clean build of my 59,000 line C project 38 times. The issue is caused by using the + operator with the channel Int and a String literal. Thanks to the standard library's 17 overloads of + and 9 types adopting the ExpressibleByStringLiteral Protocol, the swift compiler can't rule out that there might be a combination of types and operators that make the expression valid, so it has to try them all. Just considering that the five string literals could be one of the possible nine types results in 59,049 combinations, but I suspect that's a lower bound, since it doesn't consider the many overloads of +. It gives up before getting through them all. You can fix the code by converting channel to String: let url = "http://" + username + ":" + password + "@" + address + "/api/" + String(channel) + "/picture" This now successfully compiles in 0.19 seconds! Maybe you think strings are complicated or something, so here's an example that is just math: let offset: Double = 5.0; let index: Int = 10; let angle = (180.0 - offset + index * 5.0) * .pi / 180; Again, we get error: the compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions, this time after "only" 8 seconds. The problem is due to index * 5.0, i.e. an int multiplied by a double. Even toy compilers handle equivalent code quickly, thanks to a context-free type system. Both examples are slow because they're invalid swift and the type checker falls out of the fast path in order to confirm all possible type combinations are invalid. You might think it's ok for invalid code to take a long time to compile. For me, 42 seconds to produce an "I give up" message is unacceptable. However, there are valid lines of swift that take a long time to compile too. Send me your slow lines (found using -Xfrontend -debug-time-function-bodies) and I'll add it to this post. Swift has come a long way from version 1, but on its 10th birthday it can still be slow. Unfortunately this can't be completely fixed by optimizing the current approach. It requires a different approach. Here's what I'd do: 1. Add a flag to swiftc that makes it infer types using only an expression's child AST nodes while ignoring the parent AST node. The flag would also disable the ExpressibleBy protocols, which by definition get their type from their context. 2. Make a feature that adds type annotations, casts, and enum names to existing code where necessary to compile with the new type checker 3. Update all sample code to compile with the flag This might be a reasonable stopping point: teams that care about compile times and good error messages could use the flag, and everyone else doesn't have to. It could go further though: 4. Enable the flag by default for new Xcode projects 5. Deprecate the old type inference approach With this new approach, you'd have to add type annotations in some places. I'm ok with that. As a result, we'd get faster compilation times and clearer error messages, but the extra verbosity might be too much for the swift community to swallow. Discuss on Twitter Discuss on Hacker News --------------------------------------------------------------------- 1. Technically some compile time slowness is due to the Swift/Xcode/ SwiftPM build system, but that's a different story. -[?] 2. Tested with both Swift 5.10 and 6 -[?] See more by Daniel