Imperative vs. functional performance

The following function:

extension Array {
    public func firstResult<T>(_ transform: @escaping (Element) -> T?) -> T? {
        return lazy.compactMap { transform($0) }.first
    }
}

seems to run much slower (results from profiling, the function is being called a lot in our codebase) than the following variant:

extension Array {
    public func firstResult<T>(_ transform: @escaping (Element) -> T?) -> T? {
        for element in self {
            if let result = transform(element) {
                return result
            }
        }

        return nil
    }
}

Is there anything wrong with the first version or is Swift simply not optimised for a functional style of programming?

Would probably be helpful to know what optimisation level and a typical transform. Or, better yet, provide some test code that people could use to reproduce your results.

Was this run in -O? Because without optimizations, Swift isn't going to be inlining the functional methods, and in turn isn't going to turn them into loops.

That being said, functional code, unless optimized, is going to be slower. The actual performance difference is going to vary on the actual uses.

Isn't the main difference in the fact that imperative version stops after finding first result while functional maps whole array and then returns first one? Or is compiler somehow smart enough to know that it can break after first non-nil result?

Compiler isn't smart enough, but Fryie is. He calls compactMap not on self, but on self.lazy, which is lazy and maps only elements you access

1 Like

Not sure if it is relevant for the performance question you're asking, but in neither of these implementations is the transform closure really escaping. In the first case it is seemingly required unless you want the compact map to actually walk the entire collection, and thus negating the whole idea of using .lazy. In the second case it isn't needed at all.

I said seemingly required, but there is a way to circumvent the requirement, by using withoutActuallyEscaping. According to the docs:

You can use this function to call an API that takes an escaping closure in a way that doesn’t allow the closure to escape in practice.

Thus, it can be rewritten to:

extension Array {
  public func firstResult<T>(_ transform: (Element) -> T?) -> T? {
    return withoutActuallyEscaping(transform) { escapableTransform in
      lazy.compactMap(escapableTransform).first
    }
  }
}

I'm not sure if it has noticable performance implications, but it should be able to avoid heap allocations, methinks.

2 Likes

I suspected something like that, but I was confused about how lazy.compactMap and first interact. Few playground experiments later and everything is clear. Thanks