Performance hit for marking a Swift struct method as mutating?

I had a method on a struct which was, due to code changes, marked mutating, when in fact it no longer mutated the struct at all.

I was surprised that removing the keyword "mutating" made things a bit slower.

Why would simply adding the word "mutating" make calling a method on a struct be faster?

(I edited this after it was posted. Originally, it was that adding mutating slowed it down. It was the other way around in fact.) The curious thing here is that the code in question is not mutating at all; and there shouldn't be a reason that marking it as mutating or not actually changes performance.

That's the issue here, not whether it's slower or faster. It really ought to be about the same, either way.

Maybe the compiler doesn't have to add exclusivity locks to allow writing during a multi-threaded context (because self is now read-only during the method)?

It turns out that in the case I was looking at, there was copying of some data going on. for some reason, adding mutating made it do a memcpy, where without mutating it was doing the copying explicitly inline (or possibly the other way around). So in the end, it's close to the same code, and one version just happens to be a bit faster than the other.

In other words, nothing very interesting to see, just a small change in the output code with a timing difference that's in the noise. So any future readers, just ignore this thread!

It’s still slightly disconcerting to me that purely adding mutating would make code faster. If you’re able to put a reduced self-contained example into bugs.swift.org. It may be of interest to @Erik_Eckstein and co.

Thank you for your feedback. Please see
https://bugs.swift.org/browse/SR-9608

which shows the code that exhibits this. I made a standalone project here:
$ git clone git@github.com:davidbaraff/ptrTest2.git

Non-mutating methods pass self by value, which means it must be copied. Mutating methods pass self as an inout value, which means the method receives a pointer to a mutable value materialized in memory. So there might be less copying in that case.

1 Like

Any chance of the compiler being able to switch to pass by reference if it can prove there won’t be any new mutations and would likely be faster?

1 Like

Just curious: It this somehow related to Fastest way to get (const) pointer to struct for inter-operability with C/C++, where it was observed that withUnsafePointer(to: someInstanceVariable) makes a copy, whereas withUnsafePointer(to: &someInstanceVariable) avoids the copy?

Martin R, yes this is directly related to "Fastest way to get (const) pointer to struct for inter-operability with C/C++”, in the sense I discovered it while working on that issue. The sample code I wrote is doing a copy simply because in the non-mutating case, it cannot pass a pointer to the its internal data, and in the case when the internal data is a var, as we already observed, the withUnsafePointer optimization doesn’t kick in (not that it does anyway with let either right now)...

Swift already switches to pass-by-reference when a value is large. The compiler does not always catch opportunities to avoid copying into new temporary buffers between function calls, though. The case from "Fastest way to get (const) pointer to struct for inter-operability with C/C++" is in fact an instance of this—withUnsafePointer is just a regular function that takes its argument indirectly, but because of some phase ordering issues in the compiler, an avoidable copy gets emitted.

1 Like

What defines "large"? It's clearly an implementation detail, but I still think it could be helpful for particularly performance concerned users to be aware of.

1 Like

Larger than three registers IIRC.

2 Likes