Performance issue with parsing large Data in parallel using Swift Concurrency

So, apropos of this other thread, marking parse(data:) as @_effects(readonly) fixes the performance behaviour. i.e. now the parallelised version runs basically N times faster (where N is the CPU core count, ignoring long tails for chunk count not being an even multiple of core count).

It looks like it's doing the right thing, in time profiles. i.e. it's still actually calling the method for every datum. It's just not wasting any time with unnecessary ARC traffic.

That underscored function modifier wasn't my first stop at trying to convince the compiler to stop temporarily retaining things, but it was the only thing I could find that worked (even unpacking the data first, using withUnsafeBytes, and passing only the UnsafeMutableRawPointer to parse(data:) didn't help).

Of course, the question is both (a) why is this seemingly necessary and (b) is this actually safe, even in this trivial benchmark?

I'm going to assume it's not safe for the real-world version of this code, so it's not really a solution.

Perhaps the specific question is: in the absence of that annotation, why does the compiler apparently think that parse(from:) can write to something which somehow means the caller has to retain the data? Is this some weird aliasing problem; does it think somehow parse(from:) is able to mutate the input array and potentially remove the current element out from under itself?

Addendum: also, with @_effects(readonly) in place, @_optimize(none) is no longer necessary for correctness as long as Record contains something derived from its Data argument (e.g. a joking "hash" as in the example below). Removing the optimisation inhibitor allows inlining again, which improves performance by more than an order of magnitude. Which is expected, but noteworthy, and it's good to confirm that @_effects(readonly) still works even with the optimiser enabled.

struct Record {
    let hash: UInt8
}

struct Parser {
    // Decode the payload of a message into a Record
    @_effects(readonly)
    static func parse(data: Data) -> Record {
        var hash: UInt8 = 0

        data.withUnsafeBytes {
            for byte in $0 {
                hash &+= byte
            }
        }

        return Record(hash: hash)
    }
}
1 Like