swiftlang / swift

The Swift Programming Language
https://swift.org
Apache License 2.0
67.32k stars 10.34k forks source link

Short-circuit Boolean logic in if-let statements #59008

Open davedelong opened 2 years ago

davedelong commented 2 years ago

Comma-separated statements in an if-let statement are logically AND'ed together. A typical optimization of || and && statements is to not execute the right-hand-side of the operator if the final value can be deduced from the left-hand-side. For example in C, this is a common pattern to avoid dereferencing NULL:

if (pointer != NULL && pointer->_someBooleanValue) { ... }

However, no optimization happens with if-let statements. Consider the following code:

func potentiallyExpensiveLookup(_ key: String) -> Int? {
    print("Performing expensive lookup")
    return key == "42" ? 42 : nil
}

func doTheThing(_ maybe: Bool) {
    if let value = potentiallyExpensiveLookup("0"), maybe == true {
        print("Matched: \(value)")
    } else {
        print("Did not match")
    }
}

If this example is run with doTheThing(false), we can look at the code and see the the value retrieved from potentially ExpensiveLookup() will never be used, because the maybe == true condition will always evaluate to false.

In every optimization mode (-O0, -O1, -O2, -O3, -Os, -Ofast, -Oz), running doTheThing(false) results in this output:

Performing expensive lookup
Did not match

However, the optimizer should be able to see that:

if let value = potentiallyExpensiveLookup(...), maybe == true {
    // true branch
} else {
    // false branch
}

is logically equivalent to:

if maybe == true {
    if let value = potentiallyExpensiveLookup(...) {
        // true branch
    } else {
        // false branch
    }
} else {
    // false branch
}

The optimizer should be able to recognize explicit boolean comparisons in if-left statements that do not rely on previously-bound values, and rewrite the expression to evaluate those first. This would allow programs to avoid executing potentially expensive lookups that are unnecessary.

karwa commented 2 years ago

It does short-circuit, but as you say, "a typical optimization of || and && statements is to not execute the right-hand-side of the operator if the final value can be deduced from the left-hand-side." Since you call potentiallyExpensiveLookup(...) first, it will be evaluated before checking maybe == true. What I think you are looking for is for the compiler to re-order those comparisons in a way which removes the lookup call.

Re-ordering code in order to entirely remove function calls is quite tricky IIUC, because potentiallyExpensiveLookup might have side-effects.

LucianoPAlmeida commented 2 years ago

However, the optimizer should be able to see that:

if let value = potentiallyExpensiveLookup(...), maybe == true {
    // true branch
} else {
    // false branch
}

is logically equivalent to:

if maybe == true {
    if let value = potentiallyExpensiveLookup(...) {
        // true branch
    } else {
        // false branch
    }
} else {
    // false branch
}

Because of side-effects that @karwa mentioned, is very hard to make that "logically equivalent" assumption.

Also, IIRC correctly ordering if let clauses and boolean check in if statements is already a syntax supported so it is possible to right code like

func potentiallyExpensiveLookup(_ key: String) -> Int? {
    print("Performing expensive lookup")
    return key == "42" ? 42 : nil
}

func doTheThing(_ maybe: Bool) {
    if maybe == true, let value = potentiallyExpensiveLookup("0") {
        print("Matched: \(value)")
    } else {
        print("Did not match")
    }
}

Which this equivalence assumption can be correct and the ordering is up to the user in the same way normal all boolean && works.