What (if any) are the benefits of using _read over a normal get?

sometimes i just want to expose a nested property as a computed property. if there any benefit to writing

var modules:Fasces.ModuleView
{
    _read 
    {
        yield self.fasces.modules
    }
}

over a plain old

var modules:Fasces.ModuleView
{
    self.fasces.modules
}

?

the ModuleView is a complex type (has a heap allocation), and self.fasces is a (private) stored property.

the fasces.modules property is a simple view getter:

extension Fasces 
{
    var modules:ModuleView 
    {
        .init(self.segments)
    }
}

get returns the property value, which generally requires a copy that can't be optimized out (unless the get can be inlined, which isn't always good for code size, and impossible across ABI boundaries). _read yields a reference to the property in place, which allows for the possibility of avoiding a copy in cases where the caller doesn't need one.

2 Likes

are there any downsides to using _read? (besides compiler stability)

Computed properties may have the opposite trade-offs: if they’re inventing a value to return, rather than referring to something already stored somewhere, then read requires the caller to make an otherwise-unnecessary copy if they want to persist the value while get does not.

(And we want stored and computed properties to look uniform across ABI boundaries.)

7 Likes

Invoking a _read that isn't inlined does require more code size at the call site, to begin and then resume the coroutine. For trivial types that fit in a register return, there's probably not much net benefit over get. If the property is truly computed—the get computes a value on the fly, which isn't in storage normally—then the read would need to allocate a temporary buffer to yield it, which the caller would have to copy anyway, and the _read would have to destroy when it's resumed.

5 Likes

should a type following the view pattern (like the one at the top of this thread) use _read or get?

Well, a view is not completely synthesised within the accessor - it wraps some underlying storage that already exists (providing another view of that storage):

struct SomeData {
  var storage: Storage

  var view: MyView {
    // Either:
    get { MyView(storage: self.storage) }
    // Or:
    _read { yield MyView(storage: self.storage) }
  }
}

struct MyView {
  var storage: Storage
}

Even if the MyView initialiser accepts the storage as a borrow parameter, unless you get lucky and everything is inlined (including the entire lifetime of the view), the storage will probably get retained anyway when it populates the view's stored property. So you might as well use get and return an owned value which the caller can release when they like.

One (the only?) situation where it would be beneficial to use _read is if you move SomeData.storage in to the view, and then move it back out again when the read access is finished. In that case you could guarantee the storage wouldn't need to be retained - it gets moved around, but not 'copied':

  var view: MyView {
    _read { 
      let theView = MyView(storage: move self.storage) 
      defer { self.storage = /* move as part of theView.deinit */ theView.storage }
      yield theView
    }
  }

But you'd need a mutating _read for that. Not only is it actually mutating self, but you need to ensure there are no overlapping accesses to the temporarily uninitialised self.storage. So that's what you would need to do in order to get any sort of benefit from a _read accessor, I think. If you're not doing this, I don't think there's any benefit over get.

Since this is a read-only access, the benefits of avoiding that retain aren't obvious - you aren't mutating, so there's no risk of COWs. But you pay for it with more complex code and possibly also coroutine overheads.

2 Likes

Okay, you use the word view and I automatically think it’s a SwiftUI view :sweat_smile::sweat_smile:

1 Like