Code size difference with keypath vs closure

I was looking into how keypaths are compiled as functions, when I noticed something that caught me off-guard. In the following code:

public protocol P {}

public func firstVersion(_ args: [[P]]) -> [P] {
    let closure: ([P]) -> [P] = { $0 }
    return args.flatMap(closure)
}

public func secondVersion(_ args: [[P]]) -> [P] {
    let closure: ([P]) -> [P] = \.self
    return args.flatMap(closure)
}

there is a substantial difference in firstVersion and secondVersion at some optimization levels. With -Osize and -Ounchecked, the compiler outputs a reasonably short implementation for firstVersion, and then secondVersion just calls firstVersion. However, with plain -O, they get drastically different implementations, with firstVersion being over twice as long as secondVersion.

Godbolt link

Why are they different? Is \.self meant to perform differently than { $0 }?

4 Likes