A roadmap for improving Swift performance predictability: ARC improvements and ownership control

Swift has been very deliberate in avoiding unsigned integers in API. This clearly has the unfortunate side effect of introducing unnecessary checks for negative values in code that will never see them. Does the compiler (or LLVM) have the ability to track statically-proven bounds? Surfacing that ability in the language (e.g. var startIndex: @nonnegative Int) might get the best of both worlds: the ergonomics of signed indexes with the performance characteristics of unisgned codegen.

I believe that avoiding unsigned integers has been a serious mistake, personally. Iā€™d rather we just fixed that in a future major release.

3 Likes

Sure, but for specialised types like UnsafeBoundsCheckedBufferPointer, it isn't an issue. It is only used as a generic collection, so no callers even care what the index type is.

I've benchmarked it extensively (it is used everywhere in WebURL; we almost never use the standard library's UnsafeBufferPointer), and it does not perform any worse, despite adding bounds checking. That's a result of how it is designed and very careful coding in custom collection algorithms to ensure the compiler can reason about the checks.

I'd like all developers to benefit from that, so I think it is worth considering adding it (or something like it) to the standard library as an alternative to UBP.

Anyway, we probably shouldnā€™t get too far off track in this thread.

Thereā€™s currently significant effort being put into UnsafePointer and friends. If the standard library can add bounds checking to the existing types without impacting performance, why introduce another type, forcing clients to decide which one to use and convert between them when someone else has concluded differently?

I agree that the idea of overhauling Swiftā€™s standard library to use unsigned integers is likely out of scope for this discussion, but I am interested to know whether itā€™s feasible to add attributes (like @nonnegative) that hook up to LLVM features which are currently unexploited. If, as @Karl argues, superfluous bounds checks are such a critical bottleneck, then it might be worth factoring into the roadmap.

Or heck, maybe an attribute isnā€™t necessary, and hooking into such functionality would silently benefit all non-resilient and @usableFromInline code.

Are these checks equivalent?

    let x: Int = 1
    let check1 = x >= 0 && x <= 100
    let check2 = UInt(bitPattern: x) <= 100

If they are then signed index bounds checking could be as fast as unsigned, no?

Yes but that only lets you eliminate the trap for the lower bound if you know the lower bound is zero. Thatā€™s true for Array, but not for ArraySlice, yet both have Int indexes.

Wouldn't the relevant check for array slice be i >= a && i <= b regardless of whether indices are signed or unsigned?

Yes, thatā€™s why you canā€™t eliminate the first branch for ArraySlice. My understanding of @Karlā€™s point is that straightforward Swift code generates a lot of branches/traps. The cast-to-UInt trick works, but itā€™s not really keeping with the theme of this thread, which outlines compiler changes and language features to produce more efficient codegen from straightforward Swift code.

This is getting away from the topic at hand, I think. However, there is the underscored public method _assumeNonNegative (with a TODO eventually to remove the underscore) and it would be interesting for folks to try it out in these scenarios.

2 Likes

Oh god, please donā€™t use an attribute where a protocol would do. Anyone using a signed integer should expect a signed integer.

Yeah my intention was just to point out to the team that there is more than just ARC which affects our ability to reason about the performance of our code. In hindsight, I should have perhaps made a separate thread; I apologise.

4 Likes

This looks like poor implementation. It shouldn't require surface language changes to make stored property initialization expressions more efficient than that.

This is, ultimately, the same class of problems as excessive ARC. Using moves, consuming/nonconsuming, and accessor coroutines to eliminate copies should eliminate value witness calls too.

C and C++ have both had concepts of "constant expression" since their inception. What C++ added recently was the ability to make user-defined functions usable as constant expressions too. We could have an ad-hoc "raise an error if this global initializer can't be optimized into a static initializer" attribute, though it would be hard to define what you can safely use as a static initializer, without future optimizer changes breaking that, without a formal definition of what a constant expression is.

7 Likes

This will be a huge win for us.

With read and modify accessors, and inout, will mutating for-loops be possible on MutableCollectionss?

for &particle in particles {
   particle.position = integrate(particle)
}

I do a lot of realtime simulation and I am frequently surprised at the amount of ARC traffic when I profile our code in Instruments (it's possible to eliminate much of it, but not always). These performance improvements are very welcome and it should be possible to write predictably performant code at the first implementation. Wonderful.

1 Like

is it correct to have shared and inout to the same object at a time?
for e.g.:

func testBorrowing(_ arg1: __shared Int, _ arg2: inout Int) {
....
}
var x = 0
testBorrowing(x, &x)

Rust has borrowing rule: either only one mut ref or any number of immut ref to the same object at a time during the lifetime of the object.

No, Swift borrowing works the same way. Swift ARC, however, is effectively a borrow checker that inserts copies rather than raise errors, so your call will pass a copy of x, and the inout reference to x, in order to avoid the borrow check failure. Using noImplicitCopy or move-only types would prevent this, and make this an error.

7 Likes

__shared should be considered inherently incorrect, as it is not actually stable.

Wait, does this mean you could basically opt into a form of Rust-style ownership management with that attribute? Thatā€™s very interesting.

I imagine production-ready Rust inter-op is not particularly high on the to-do list, but maybe it isnā€™t that far off.

I heard Joe say this a while back, and it remains the best explanation of Rustā€™s borrow check Iā€™ve heard.

I had the same question: is @noImplicitCopy on a reference type essentially isomorphic (perhaps modulo some fussy details) to Rustā€™s borrow checker?

The pitch, indeed, sounds like a Christmas present. It is a delight to read.

Essentially, it seems to boil down to just two effects:

  1. Make ownership of reference counted types explicit, to prevent ARC from doing its default thing.
  2. Opt-in to take control over default value semantics rules to prevent or induce copying.

What I'm worried about is the amount of new keywords and attributes added to the language. For a newcomer to Swift this may be overwhelming. Can we consider reusing the same or similar terms for listed application, e.g. we have inout, then we can have in and out in some places, or similar?

My point is that in a bid to avoid confusion maybe it's worth to use the same language terms for different cases which are expected to behave in a similar way by a programmer.

3 Likes