SE-0231 — Optional iteration

It doesn't have to check if the original collection is empty on every iteration. And according to my quick test the following simple .orEmpty() doesn't add any overhead in an optimized build.

Here is a quick implementation and microbenchmark
import QuartzCore

public struct FlattenedOptionalSequence<Base: Sequence>: IteratorProtocol, Sequence {
    let baseNext: () -> Base.Element?
    init(_ optionalSequence: Optional<Base>) {
        switch optionalSequence {
        case .none: baseNext = { return nil }
        case .some(let s):
            var baseIterator = s.makeIterator()
            baseNext = { return baseIterator.next() }
        }
    }
    public func makeIterator() -> FlattenedOptionalSequence<Base> { return self }
    public mutating func next() -> Base.Element? { return baseNext() }
}

extension Optional where Wrapped: Sequence {
    func orEmpty() -> FlattenedOptionalSequence<Wrapped> {
        return FlattenedOptionalSequence(self)
    }
}

func test() {
    let n = 10_000_000
    let seq: [Int] = (0 ..< n).map { _ in Int.random(in: .min ... .max) }

    var nonEmptyTestCount = 0
    while nonEmptyTestCount < 7 {

        let optSeq: [Int]? = Bool.random() ? seq : .none
        if optSeq == .none { continue }

        do {
            print("  Using if let s = optSeq { for e in s { ... } }:")
            var cs = 0
            let t0 = CACurrentMediaTime()
            if let s = optSeq {
                for e in s {
                    cs &+= e
                }
            }
            let t1 = CACurrentMediaTime()
            print("    ", t1 - t0, "seconds (checksum: \(cs))")
        }
        do {
            print("  Using for e in optSeq.orEmpty() { ... }")
            var cs = 0
            let t0 = CACurrentMediaTime()
            for e in optSeq.orEmpty() {
                cs &+= e
            }
            let t1 = CACurrentMediaTime()
            print("    ", t1 - t0, "seconds (checksum: \(cs))")
        }
        nonEmptyTestCount += 1
        print()
    }
    
}

test()

I haven't checked if:

            for e in optSeq.orEmpty() {
                cs &+= e
            }

is optimized into identical asm as:

            if let s = optSeq {
                for e in s {
                    cs &+= e
                }
            }

But I wouldn't be surprised if it was.


Typical output on my MBP late 2013, 2 GHz i7
  Using if let s = optSeq { for e in s { ... } }:
     0.006638151011429727 seconds (checksum: 2508382841143532506)
  Using for e in optSeq.orEmpty() { ... }
     0.005152345052920282 seconds (checksum: 2508382841143532506)

  Using if let s = optSeq { for e in s { ... } }:
     0.004637237987481058 seconds (checksum: 2508382841143532506)
  Using for e in optSeq.orEmpty() { ... }
     0.00462937809061259 seconds (checksum: 2508382841143532506)

  Using if let s = optSeq { for e in s { ... } }:
     0.004614591947756708 seconds (checksum: 2508382841143532506)
  Using for e in optSeq.orEmpty() { ... }
     0.004562856047414243 seconds (checksum: 2508382841143532506)

  Using if let s = optSeq { for e in s { ... } }:
     0.004583214991725981 seconds (checksum: 2508382841143532506)
  Using for e in optSeq.orEmpty() { ... }
     0.004558937973342836 seconds (checksum: 2508382841143532506)

  Using if let s = optSeq { for e in s { ... } }:
     0.0047228699550032616 seconds (checksum: 2508382841143532506)
  Using for e in optSeq.orEmpty() { ... }
     0.004625577945262194 seconds (checksum: 2508382841143532506)

  Using if let s = optSeq { for e in s { ... } }:
     0.00457263900898397 seconds (checksum: 2508382841143532506)
  Using for e in optSeq.orEmpty() { ... }
     0.004642735002562404 seconds (checksum: 2508382841143532506)

  Using if let s = optSeq { for e in s { ... } }:
     0.004757425980642438 seconds (checksum: 2508382841143532506)
  Using for e in optSeq.orEmpty() { ... }
     0.004532704944722354 seconds (checksum: 2508382841143532506)

Program ended with exit code: 0