Example uses & benefits of `@_effects(readnone)`

In exploring this more, I noticed that the optimiser already does that even without the attribute, unless you annotate the function with @_optimize(none). When it's in the same file, at least. It seems the optimiser will look inside the function and act on the specifics of its implementation, even when the function isn't inlined.

Which wasn't exactly anticipated [by me] but is good to know - I had already hoped that @_effects(readnone) should be unnecessary outside of public module methods, and this seems to support that.

Right, and if I understand this older thread correctly, that's because readnone is really a low-level LLVM IR attribute which distinguishes register passing from memory passing - which presumably means arguments spilled to the stack would also break it?

It seems like - that older thread suggests that - readnone is impossible to use in a platform-portable and future-proof manner, because you can never really know the actual calling code that will be generated (e.g. some embedded or academic platform might not use any registers for argument passing, for example). Is that technically true?

Practically, I suppose it could be used with some confidence on existing platforms if you're very careful about the types and numbers of preceding parameters. Seems pretty fragile, though.

That earlier thread also noted that unspecialised generic functions pass the type metadata (pointers to witness tables, I assume?) on the stack, not in registers, so they also cannot use readnone. That's a pretty huge limitation for something that would otherwise be most useful for generic algorithms provided by 3rd party packages.

Real-world usage (e.g. involving generics, Self, etc)

Where does self play into readnone / readonly? I can't find a single example in any documentation or Swift Forum threads that use methods rather than free-standing functions.

For example, let's consider a non-trivial but very relevant example:

extension Comparable {
    @inlinable
    func clamped(_ range: borrowing PartialRangeFrom<Self>) -> Self {
        if self < range.lowerBound {
            range.lowerBound
        } else {
            self
        }
    }
}

That's great if it's inlined, but it might not be (and maybe for good reason - maybe it's only used generically and the overhead of the generics handling bloats it significantly). It seems like this would be good to mark as readnone so that the compiler can omit redundant calls, since it is plainly pure in the conventional sense.

But first-up, this is generic I guess since it's a default implementation on a protocol, so I guess readnone is flat-out banned?

Even if that's not the case, Self could be anything - a value type, a reference type, etc. It seems undefined what lowerBound does when invoked on range (type Self) - even just worrying about calling convention right now. Likewise the < operator, I suppose. So it seems like there's no way to know if everything is passed in registers - and therefore seemingly readnone is safe - or partly on the heap - and therefore readnone is not.

readonly

In contrast, readonly seems safe(ish) either way - assuming < and lowerBound are readonly too, of course.

But then that doesn't really buy you much, since it sounds like it merely lets the compiler omit the call if the result isn't used, which (IMO) isn't a common opportunity anyway. Given that, I'm struggling to imagine any situation in which it's worth using (given the downside that it's an underscored attribute with no guarantees going forward).

2 Likes