General compilation / type inference speed weird issues

Working on a fairly large swift app (over 2min of compilation time on an m1), and my team recently started to try to optimize build times using the xcode "build with timing summary" function, as well as the additional compilation flags -warn-long-function-bodies= and -warn-long-expression-type-checking

Some results were not surprisingly very slow (such as array expressions containing layout constraints), but others were very surprising. So much that i'm worried it's a symptom of a greater issue with our compilation settings, or that maybe we don't even understand the xcode timing properly.

One of the most surprising example is this one :

struct Style {
…
static let kWidthRatio = CGFloat(255.0 / 375.0) <-- warning expression took more than 50 ms to compile (62 ms)
…
}

The line goes under 10ms if instead we choose to write
static let kMediaWidthRatio = CGFloat(255.0) / CGFloat(375.0)

This struct is nested in another ones, but i'm still not able to really understand how such a simple expression is able to take a few dozen milliseconds to compile, and even worse how can the difference between the two expression account for a factor of 5.

Is this the symptom of something else , such as bad module isolation ? We're pretty light on generics, we don't use any kind of framework that would extend basic types with fancy generics, etc..

These two are not the same thing. One is dividing two numbers of the default literal type (Double) and then converting to CGFloat, and the other is dividing two numbers of type CGFloat.

To type check the first expression, the compiler has to figure out (to a first approximation; there are shortcuts that the compiler takes) every possible pair of types expressible by float literals which can be divided, then check which resulting type can be converted to CGFloat.

Is there a reason you’re not writing static let kWidthRatio: CGFloat = 255 / 375 or static let kWidthRatio = 255 / 375 as CGFloat? If you intentionally want the higher precision of division using Double on 32-bit platforms, I would write static let kWidthRatio = CGFloat(255 / 375 as Double).

Hi, thanks for taking the time to answer.

Honestly, i never thought about optimising this code in any way (for compilation time i mean). I just wanted to divide two floating point numbers and store the result in a CGfloat because that's what UIKit is using, using default precision because i don't care about it at all.

How many different types expressible by float literals is there in the swift stdlib / Foundation for this pairing algorithm to take 50ms ?

The reason i'm not writing 255 / 375 is purely a reflex from the times i wrote C code, assuming int division is going to be used, and the result will be 0.

FWIW, type-checking tends to have a lot of cacheable computations. So the first time it stumbles upon one particular scenario may take a good amount of time, but subsequent statements tend to be quicker. I wouldn't fret if this is one of a few instances.

FWIW2, you can check for the conformance in the API doc–ExpressibleByFloatLiteral.

1 Like

This problem is going to go away for you with the adoption of SE-0307; you'll be able to work in terms of Double and it'll be automagically converted to CGFloat when you call UIKit APIs.

But while it exists, it points out a common misunderstanding that users have. The notation CGFloat(...) is a conversion from one type to another type, and Swift literals do not have a type except from their context, so there is never a reason to surround a literal with an initializer in Swift. To give the compiler the context necessary for a literal to have the type you want, use the as operator.

This misconception was so common that we had to have a Swift Evolution proposal creating an exception so that CGFloat(42) would be a synonym for 42 as CGFloat (and similarly for other literal types). But as you see here, that doesn't apply as soon as you put anything other than a literal in the parentheses, such as an operator.

50 milliseconds is with the hacks to save time; there are so many overloads to consider that solving this would otherwise take more than 50 ms.

3 Likes