How to check two `Array` instances for identity equality in constant time?

That's great, but it does not in fact matter. Given a Swift type, the asymptotic time complexity of bitwise comparing two of its instances is O(1) whether or not we stop comparing bytes at the first mismatch.

Asymptotic complexity analysis is concerned about the growth rate of functions. The big omicron notation O(...) gives us an upper bound for how fast a function can possibly grow. An O(1) result is the best possible outcome -- it means that the function does not at all grow "with its input": its output stays within some constant range, no matter what it input is. The function in this case is the time complexity of an algorithm presented as a piece of Swift code. (Time complexity is measured by -- say -- counting the number of CPU operations executed while running it.)

For any given concrete Swift type, Jonathan's code performs at most MemoryLayout<Self>.size units of work, each of a constant time. MemoryLayout<Self>.size is itself a constant value. This makes the asymptotic time complexity of the overall algorithm O(1) -- even in the worst case, the code's time complexity remains completely independent of what values we pass into it.

O(1) is already the best possible outcome of asymptotic complexity analysis; as far as the model is concerned, it cannot be improved upon. Stopping the comparison at the first mismatching byte does not change this result -- it's still O(1).

We can reasonably argue about whether asymptotic analysis is an impractical oversimplification, and whether it is useful at all, given how often it is misunderstood. But O(1) and O(n) have specific meanings; it is highly counterproductive to mix up one with the other.

That's great, but we are not talking about memcmp in general. I'm talking about the asymptotic time complexity of bitwise comparing the representations of two instances of a given Swift type. That is a constant time operation, whether or not we stop at the first mismatching byte, and no matter if choose to do the comparison using memcmp, URBP.elementsEqual or if we roll our own loop.

The time complexity of memcmp is O(n) memory accesses, where n is the size_t argument. When memcmp is used to compare the bitwise representations of two instances of a Swift type T, n is MemoryLayout<T>.size. That is a constant value.

It does not matter if the constant is 1 or 8_000_000_000; it is still a constant.

The size passed to memcmp is not variable. It does not change depending on what two instances we choose to compare. It is always the same. It is unvarying, fixed, immutable. It gives us a constant upper bound.

It does not matter that Jonathan's code defines a generic function -- complexity analysis is concerned about the behavior of a single generic instantiation at a time.

(If y'all think comparing the byte representations of two instances of a Swift type is a "linear-time" operation, then so is passing a value to any Swift function -- because it can involve loading its full representation into specific registers or copying it into a temporary stack location. That and the withUnsafeBytes(of:) invocations in Jonathan's code often result in binaries that make several full copies of the input arguments. A clever enough optimizer might get rid of those, but I do not think ours is clever enough for that -- at least not in general.)

2 Likes