How can basic calculus operators be so slow to compile?

Hi,

I'm trying to understand how simple code such as this one can be so slow to compile.

        let i: CGFloat = 2.0
        let j: CGFloat = 2.0
        i + j // This line takes 73ms on my i7 MacBook Pro

If I'm replacing i+j with CGFloat(i + j), it goes up to 146ms.

Is there something I'm missing about this ? I'm testing this in a 100% empty project.

Thanks

1 Like

How are you measuring?

By using -Xfrontend -warn-long-expression-type-checking=

And if you do more than a single statement, like copy paste that code five times - you get the same measurement for each?

Interestingly, it only increases by 2ms on each subsequent pasting of the same line

there's some woodo magic in CGFloat, one could naïvely hope "it's just an alias to Double" but that's not the case. try Double, i bet that would be quicker.

I get the same result when using Double instead

Yeah, random guess is that a single line gets allocated some fixed time overhead for some reason - so probably a red herring. But I’m curious what someone who knows sees. Is it always the first or last line that gets the time? (Try with a function for each)

Weirder, only the first method of a file using this seems to be affected

Not too surprised, but don’t know the root cause ;-)

The time measurements in the type checker include all time spent while the type checker is working on that expression, even if the compiler is doing something only tangentially related to typechecking itself. And the compiler is structured to be very lazy, so that work can be pretty substantial.

In your case, I would guess that because this expression is the first time you’re using the + operator in the program, the compiler has to deserialize all of the + operators it knows about (something like 30-50, IIRC) so that it can figure out the right overload to use. This will likely involve disk I/O, memory allocations, analyzing their generic signatures, etc. The second time you use +, all of this has already been computed and cached, so the compiler only has to do the actual typechecking, which is a lot faster.

Because the typechecker timer also encompasses other work that’s done lazily during typechecking, it’s a very crude measure with a lot of noise and seemingly inexplicable variance. You should usually either set thresholds very high (like 500ms) so that it only tells you about expressions that are taking completely unreasonable amounts of time, or use the timer only when you specifically think there’s some kind of compilation performance problem and interpret the diagnostics with a skeptical eye.

22 Likes

Thanks for this detailed explanation, it explains the behavior I have in my test project.
However, in the real project I'm dealing with, I have a compiler warning on all first operator use of every file.

Here are a few examples during a single compilation, which seems to me there is another thing than "caching" operator during the first use.

In both below examples, constants are CGFloat.

titleLabel.bottomAnchor.constraint(
    equalTo: bottomAnchor, constant: -style.titleToBottomSpacing
) // Expression took 169ms to type-check
optionsContainer.trailingAnchor.constraint(
    equalTo: descriptionContainer.trailingAnchor, constant: -style.optionsMargins.right
) // Expression took 157ms to type-check 

In that one, I've even tried all these alternatives:
let maxY = -scrollView.contentOffset.y - scrollView.adjustedContentInset.top // Expression took 104ms to type-check

    let maxY: CGFloat = -scrollView.contentOffset.y - scrollView.adjustedContentInset.top
    --> 356ms

    let maxY: CGFloat = -(scrollView.contentOffset.y + scrollView.adjustedContentInset.top)
    --> 216ms

    let maxY: Double = -(scrollView.contentOffset.y + scrollView.adjustedContentInset.top)
    --> 351ms

    let contentOffsetY: CGFloat = scrollView.contentOffset.y
    let adjustedOffsetTop: CGFloat = scrollView.adjustedContentInset.top
    let maxY: CGFloat = -(contentOffsetY + adjustedOffsetTop)
    --> 388ms

    let contentOffsetY: CGFloat = scrollView.contentOffset.y
    let adjustedOffsetTop: CGFloat = scrollView.adjustedContentInset.top
    let maxY: CGFloat = -contentOffsetY - adjustedOffsetTop
    --> 365ms

    let contentOffsetY: CGFloat = -scrollView.contentOffset.y
    let adjustedOffsetTop: CGFloat = -scrollView.adjustedContentInset.top
    let maxY: CGFloat = contentOffsetY + adjustedOffsetTop
    --> 330ms

It wouldn't really be a problem if that was, as you explained, only on the first use of an operator, but when you have thousands of files, and each one is loosing between 100 and 300ms, global compilation time becomes really long.

All of your examples are using negation, so I wonder if it’s related to [SR-12062] Swift compiler slow when SignedNumeric is used inside closure · Issue #54498 · apple/swift · GitHub