[Pitch] Elide `some` in Swift 6

More random numbers for today. I added the column opening to my script where the existential is opened once outside the inner loop.

class underlier  <- calls/sec for argument type ->
let storage  existential     generic         opaque          opening
existential  180,092,272.35  487,780,665.79  489,594,135.56  5,316,311,553.
opaque       225,860,863.85  5,338,981,670.  5,322,045,425.  5,325,086,015.
concrete     227,004,424.47  5,824,208,845.  5,810,492,484.  5,324,748,000.

class underlier  <- calls/sec for argument type ->
var storage  existential     generic         opaque          opening
existential  184,689,333.57  486,358,144.22  488,900,752.41  5,330,500,095.
opaque       221,990,732.53  5,698,008,422.  5,707,700,891.  5,205,465,715.
concrete     222,325,502.50  5,707,700,891.  5,546,553,821.  5,199,980,163.

class underlier  <- calls/sec for inout argument type ->
var storage  existential     generic         opaque          opening
existential  486,643,113.64  485,837,035.14  482,159,328.65  457,771,011.02
opaque                       5,617,873,024.  5,733,840,054.  460,277,748.81
concrete                     5,797,241,188.  5,813,714,048.  459,033,516.64

struct underlier <- calls/sec for inout argument type ->
var storage  existential     generic         opaque          opening
existential  281,167,625.83  5,685,264,656.  5,810,492,484.  5,806,872,490.
opaque                       60,963,720,930  64,133,088,685  5,817,342,579.
concrete                     60,611,329,479  5,817,342,579.  5,299,518,605.

The thing that struck me about the first set of numbers was the cost of using existential storage. This was disappointing for someone who only a week ago was suggesting a strategy of not eliding storage to some at all.

There is a solution to this however. If you want to perform intensive processing on something in an existential: first, open it by calling a function with a some argument to get the generic form and perform your processing in that function. That this works is demonstrated in the forth column of the first and fourth row of results showing performance as if the value had never been in an existential (For some reason this optimisation doesn't work if you compile for iOS :man_shrugging:).

But my point is NONE OF THIS MATTERS; Modern CPUs are so incredibly fast. It is seldom that optimisation is worth pursuing. This "analysis" has thrown up a couple of other options I'd not have thought of before. Using let rather than var makes a difference. "Opening outside the loop" and passing around by reference using inout can make a difference too.

As it does not really affect real life performance and there are other optimisations one can apply, the belief that use of existentials is something to worry about seems unfounded to me.

2 Likes

Not all Swift developers target modern CPUs or work in an environment where optimizations don't matter. For a server-side application getting a 5% reduction in CPU usage when deployed to a big cluster amounts to a massive amount of money saved. From sustainability perspective that equals to a huge amount of kWh of electricity not consumed by data centers.

Similarly, on a small scale there's enough interest in writing Swift code targeting embedded devices. While existentials have certain requirements on Swift runtime, generics with opaque types are relatively easy to specialize and optimize away the runtime overhead. In a lot of scenarios that's a difference between not using Swift at all for a device where every kilobyte matters, and being able to use it while still having access to powerful generic abstractions.

I think dismissing these applications as niche is quite wrong. They may be small in absolute terms when compared to iOS development, but they still matter in the broader Swift ecosystem and should be taken into account.

12 Likes

We're loosing ourselves in the argument here. If you're developing for an embedded system and you think some types will make a difference you can do that using the new annotations. I'm here trying to mitigate a severe source break on the horizon and as this is the second thread on eliding to some the pitch authors seem interested in that too. just eliding to some isn't going fly however as it is reasonably easy to show, notably this experience above.

I feel we should elide to something though if we are not to experience a repeat of the bad old days of Swift 2 & 3 where it got a bit of a reputation for not being stable. I've proposed a hybrid approach where some eliding to any continues and only eliding to some in specific cases so I have to defend that against the prejudice that persists that existentials are somehow a code smell. I believe a bit of pragmatism at this point could save a lot of people a lot of time adopting Swift 6.

2 Likes

Not to dismiss what you've said, I've got a gut felling that switching from Swift to a different language like Java or Rust or C++ would make much more significant difference (be it positive or negative) in CPU usage / kWh consumption!

I don't think that switching to another language is on topic for this forum. Performance implications are discussed here in the context of generics and existentials and the way you write that out with some keyword specifically within Swift.

2 Likes
Sure

Just that reasoning sounded a bit odd, because if I am making an executive decision based on kWh consumption and it turns out that switching from existentials to generics can same me 5% but switching from Swift to, say, Rust can save me, say, 30% ā€“ I'd have to do the latter. And if I am not doing that, there are other significant factors keeping me from switching the language, likewise there might be other factors keeping me from switching from existentials to generics. The reasoning you presented would be along the lines of another one: "switching from UIKit to SwiftUI would raise global temperature due to an increased CPU usage ā€  so let's not do it".

ā€  - btw, that's true, and perhaps even measurable. Definitely so for bitcoin mining.

Iā€™m strongly against the idea of the bare protocol having different meanings depending on context. That just sounds like a recipe for confusion no matter how simple the rule for which context holds which meaning.

2 Likes

Just eliding to some isn't going to work (closures, containers) so if we could just get past this it would be great. The culture of Swift is not one of compromise alas. Is explaining to people why they should use some sometimes and any other times going to be any less confusing? The compiler knows best.

1 Like

Not only are those applications extremely niche, surely you can't be suggesting that the syntax of Swift should be optimised for "people who use Swift in embedded systems but haven't figured out that existentials are slower".

Should we make the language more confusing for every one forever, just because we can do a migration in such a way that performance is improved for that tiny fraction of a fraction of developers?

1 Like

I do disagree with both the niche line of argumentation and the cpus are so fast these days line of argumentation on principle, as they may be well intended, but paves a way to a place where most people donā€™t want to find themselves as I see it.

But to avoid getting bogged down on those side arguments and to get more straight back on topic of the pitch - I think the least confusing may then be to take the existing required any/some for Swift 6 and simply not elide.

As been pointed out earlier, it is a bit Houdini-like trickery to call this pitch non-source breaking and Iā€™m not sure I agree that calling a protocol of ā€œsomethingsā€ some Protocol would be that strange for a beginner, itā€™s a syntactical hint that protocols arenā€™t exactly like other types.

Then the progressive disclosure of later adding any less of a step actually, as you just add one more keyword (and code will consistently read the same).

Assuming tooling for helping with fixit:s for the migration to swift 6 could default push people to some might be the better long term take. Of course it is always nice to remove boilerplate when the code will read easier after the fact, but there is also something to be said for being explicit when thereā€™s risk of misunderstanding.

Your mileage, code base size, use of existentials, care about performance, fear of code churn and more, may vary.

17 Likes

the difference between existentials and generics is an important concept that i would expect any swift developer i work with to be familiar with.

2 Likes

Houdini aside, I am desperately interested in trying to tone down SE-0335 a little, with pragmatic eliding if need be for which I already have a joke implementation of sorts that nearly compiles Alamofire, the single most used Open Source Swift package by GitHub stars. Whether protocols should stand out is an interesting debate but I've always viewed how they looked just like a type as a feature. Java had interface List but did you really need to know it was implemented by ArrayList.

I've never agreed on this interpretation that making things explicit in itself serves "progressive disclosure". For me it was always meant that very complex things could be used in a naive way, choosing when to delve into the deeper truths when you had more time and experience. Not eliding places two very difficult to fully appreciate concepts (existentials and genetics) front and centre when you use a Protocol whether you like it or not (or your existing codebase splits that hair at that moment.) This is not progressive disclosure. I don't subscribe to the view that these two concepts are essential to know in order to write good code or that knowing them would make you a good programmer.

Other languages do not feel the need to surface this distinction and people get things done. All this debate on performance I am not trying to further but present data that it is simply a storm in a tea cup. Arguments about "just improve the tooling, fixits etc" do not compensate the library developers inundated with requests to add an explicit language version 5 to their package when Apple ships Swift 6. At last count this would involve 600 of the top 1000 Swift Packages.

Anyway, I'm getting a serious "read the room, John" vibe so I'll pack my bags and move on.

2 Likes

Weā€™ll, It was not the explicitness that i considered foremost really, but rather the fact that ā€œsomeā€ would only have a single spelling (instead of first learning naked protocols, then understanding when you should use naked, some or any) which would be extended with ā€œanyā€.

That is a view point I can understand - churn is painful - at the end of the day, the group of people finally making the call will surely feel that pain too and take it into consideration. I donā€™t think thereā€™s a stark right or wrong, itā€™s just a spectrum of trade offs (as I tried to allude to when I wrote YMMV).

I really donā€™t think thatā€™s warranted - itā€™s by having these discussions we can provide input to the powers that be to make a balanced decision. Iā€™m sure theyā€™ll look and consider the work you spent on that POC (and appreciate the time to provide the input). We all argue from where we stand with different needs, baggage (sourceā€¦) and experience. Many viewpoints will help make things better and even though I donā€™t agree with any argument someone makes (like your performance argument), I do agree with some of them (concern of code churn).

4 Likes

Some less microbenchmark-y but still anecdotal numbers: specializing one generic function in the Foundation overlay (via @_specialize, which inserts runtime type checks), was a 10% overall speedup for JSONDecoder.

2 Likes

Whatā€™s the practical difference between the runtime type-check introduced by @_specialize and is-checking an existential? Just the extra cost of boxing/unboxing the value?

1 Like

TBH I'm also curious about that. My assumption is "it's about the same, and the win is from specializing the rest of the function, not from avoiding dispatch overhead", but I haven't verified that assumption.

is and as carry a bunch of added overhead because they have to check subclassing, bridging, collection/Optional subtyping, and all the other various dynamic casting edge cases. @_specialize can do exact type checks in a way that's probably more efficient. That added overhead is mostly independently of whether existentials are involved or not, though.

Ultimately, though, I'd like to see us expose that kind of more expressive efficient type testing in the language too. It would be great if you could do things like

func foo<T>(x: T, y: T) -> T {
  if where T == Int {
    return x + y // we can assume x and y are Int here
  }

  if <U> where T == Array<U> {
    return x + y // we can assume x and y are some kind of Array here
  }

  if where T: Addable {
    return x.add(y) // we can assume x and y conform to Addable here
  }

  return x
}

without the overhead of the extraneous checks, the need for a value of a type to ask for properties of the type itself, or the implied wrapping of casting something as? P.

14 Likes

The way I imagine progressive disclosure working with explicit some is:

  1. Protocols and some together: this is a protocol, and you use it like this (with some). You need some because the protocol isnā€™t as type, itā€™s a constraint (or ā€œcontractā€) on types.
  2. Same-type constraints and explicit generic syntax together: what if you want to operate on two of the same kind of Equatable?
  3. Type erasure and any together: what if you need to store an arbitrary P/store a collection of P/return different kinds of P under different conditions? Hereā€™s the heavy tooling.

(And then 4. Whatā€™s with all these existentials of @objc protocols? Oh, theyā€™re actually quite different? How?)

4 Likes

I would suggest that for anything you do less than 100,000 times a second in an ā€œappā€ context, your optimisation target should be code size rather than throughout. Size matters to users, and affects install rates.

Code size also affects performance; if an entire routine can fit in L1 instruction cache, it can execute more quickly than code that requires fetching from L2 cache or main memory.

For this reason, and many others, I think optimization is always worth pursuing. But like all things in software engineering, itā€™s a question tradeoffs: developer time, flexibility, and performance.

8 Likes