Boolean comparison causes extremely slow compilation

Since we moved parts of our codebase to swift, our compile times have effectively quadrupled. Trying to combat this, we've used the function and expression debug time flags to figure out if there's something to be saved by simplifying expressions.

Apart from the usual suspects (CGFloat / TimeInterval expressions) being slow, this one seems to be not making a lot of sense to us:

public override func hasItems(in: Store) -> Bool {
		guard let items = store.storage?.items else { return false }
		return items.isEmpty == false
	}

This type-checks in 400ms according to the compiler.

If we change the boolean comparision to use the negation operator (!), this type-checks in under 10ms:

public override func hasItems(in: Store) -> Bool {
		guard let items = store.storage?.items else { return false }
		return !items.isEmpty
	}

Is there anything we can do about this to make it compile in a sane amount of time?

In our project, we really prefer explicit comparison over negations, but swift seems to have horrible compile times with boolean comparisons.

We don't use any custom operators, nor do we make use of generics.

4 Likes

It seems like you found your answer. Use the negation operator.

I think the answer is because the equality operator has to be defined for types that are Equatable, so there are a lot of types to check to find the right implementation. Negation has a much smaller number of types to search, so the compiler finds the right method a lot quicker.

Hmm. Thanks I guess?

Wondering what the point is of having a == operator if it is extremely slow.

At runtime, it's not slow, unless dynamic dispatch comes into play, like any runtime method that can be overridden, and even then it's still pretty quick. Compile time is another matter. Also, testing a boolean for true/false is pretty simple. The equality operator can support some pretty complex equality conditions all bundled up in a simple syntax.

1 Like

This question, or at least this answer, appears to come up frequently.

@DeskA Your method hasItems can be written on one line like this, however it will then exhibit the slow compilation I guess

return store.storage?.items.isEmpty == false

or you could try this

extension Optional where Wrapped == Bool {
	var isFalse: Bool {
		return self == false
	}
}

return (store.storage?.items.isEmpty).isFalse

You can also do it this way (but I don't like this style)

return store.storage?.items.isEmpty ?? false

Talking about compilation time of course.

It seems just so unexpected (but very on-brand for swift) to have a simple boolean comparison to take 400ms in compilation time. I mean, the compiler knows both sides are bools so there should be only one candidate == operator implementation, right?

Even c++ doesn’t get this slow when compiling :frowning:

1 Like

Wonder if clang also has that feature to gauge the type checking duration. :thinking::face_with_monocle: I could use that in the near future.

That second option with the coalescing ?? operator is even slower.

It the end it feels very weird to need these kinds of workarounds, just to make our project compile within a reasonable time. I honestly wished swift would focus on stability and performance rather than bolting on more and more features :sob:

2 Likes

Do most of these == trigger the flag? It wouldn't be weird for most of the "first appearance" to spend some time with type checking, but I don't think one-time 400ms really amount to much.

There are multiple instances of boolean comparisons being slow in our project. I can easily cut our overal compilation time by 10s by getting rid of explicit comparisons unfortunately

1 Like

In fact, it does not know that both sides are of type Bool, because == can be implemented to compare heterogeneous types and false can be any type that's expressible by a Boolean literal. Therefore, it has to figure out every possible combination of implementations of == and types conforming to ExpressibleByBooleanLiteral that is available for use here to see if it's a better match.

struct S: ExpressibleByBooleanLiteral {
    init(booleanLiteral value: Bool) { }
    // Note that this type has no storage.
    // It throws away the literal value.
}

func == (lhs: Bool, rhs: S) { print("Hello") }
// Note that this `==` doesn't even return any value.

typealias BooleanLiteralType = S
false == true
// Prints "Hello".
// Note that no actual comparison is being done here.
// `==` is literally just a function that prints "Hello".

For numeric types specifically, there is a hardcoded compiler shortcut to make compile times tolerable until a general solution is discovered. No such optimization is hardcoded for Boolean values because it's not idiomatic to write == true and == false. If you do, it'll still work, but the compiler will look through every possible implementation for the best match. As @jonprescott as said, you've discovered the solution: don't write this.

3 Likes

Even without that (since they're not overloading ==), there are already a few ExpressibleByBooleanLiteral; Bool, ObjCBool, NSDecimalNumber (:woman_shrugging:) and its entire hierarchy, including NSNumber, NSValue, and if we include toll-free bridges; Decimal, CFNumber, and CFBoolean. Except for ObjCBool (:woman_shrugging::woman_shrugging:), all of them are Equatable. Sooo, yeah, that 's quite a few.

== is one of the operators with the most overloads in the standard library (over 70 overloads!), and currently the type checker is not very good at filtering out choices that won't work with the given argument types, so it ends up trying way too many of them even when it has enough information to narrow the choices down. Operator type checking performance is something we're actively working to improve.

17 Likes

@DeskA What is the type of items? If items is a type that conforms to a Collection, Array or Set type-checker should prefer (Bool, Bool) -> Bool overload because it's known that isEmpty is a Bool and default type for false is Bool as well. Since this is clearly not happening I suspect that it has something to do with items.

I certainly don't want to start a debate, but I want to stress out that prefixing a long boolean expression with ! is sometimes less clear than appending == false. Here I think there are several idioms that address several situations.

A fast path for boolean equality would thus be very welcome.

10 Likes

It's a plain Array<Item>:

Even doing this causes the issue for us:

public override func hasItems(in: Store) -> Bool {
		guard let items = store.storage?.items else { return false }
        let localIsEmpty = items.isEmpty
		return localIsEmpty == false
	}

I still fail to see why the compiler wouldn't know the type:

  • the compiler sees false
  • the compiler knows the (current?) type of BooleanLiteralType, how would it otherwise know how what false is
  • In my case that's bool
  • the other side is a bool also.
  • the only viable overload seems to be == (bool, bool) to me

I don't see why the compiler has to check all other overloads?

That's good news! Swift is 15% of our codebase, but amounts to 75% of our compilation time. Any improvement here would be very welcome.

Bool and Bool.==, like all fundamental types, are defined in the standard library and not built into the compiler. The ideal is that anything required to design these fundamental types should be available to everyone.

So again, there is no hardcoded compiler shortcut for overload resolution of Boolean operators. It's a binary operator like any other operator, with a literal operand of unknown type like any other literal operand, and the compiler must evaluate available overloads at the point of use to see which matches best.

When @hborla makes the compiler better at resolving operator overloads generally, then this will get better. But the compiler has no magic here where it knows about which Boolean types exist and have matching operators without actually doing the work of overload resolution.

4 Likes

Just wanna add. There has been some, like preference for +/- with the same type on both sides. Though they are usually for the extremely-common-but-extremely-slow case. These usually are just a stopgap until better system is realized. Hard to say for Bool.== when there's also !.

Terms of Service

Privacy Policy

Cookie Policy