1

First things first:

  • I know what Span<T> and Memory<T> are

  • I know why Span<T> must reside on the Stack only

  • I (conceptionally) know what struct tearing is

What remains unclear to me: Isn't struct tearing also an issue for Memory<T>? As far as I understood, basically every type bigger than WORD-size can/will be affected by that. Even further, when such a type can be used in a multithreaded reader-writer-scenario it could lead to race conditions as described in the link below.

To get to the point: Wouldn't this example also rise the issue of an potentially inconsistent Memory<T> object when used instead of Span<T>:

internal class Buffer {
    Memory<byte> _memory = new byte[1024];

    public void Resize(int newSize) {
        _memory = new byte[newSize]; // Will this update atomically?
    }

    public byte this[int index] => _memory.Span[index]; // Won't this also possibly see partial update?
}

According to the implementation of CoreFX Memory<T> also sequentially lays out a (managed object) reference, its length and an index. Where's the difference to Span<T> I'm missing, that makes Memory<T> suitable for those scenarios?

Peter Wurzinger
  • 391
  • 2
  • 9
  • 2
    [See this comment?](https://source.dot.net/#System.Private.CoreLib/shared/System/Memory.cs,417) [And this one?](https://source.dot.net/#System.Private.CoreLib/shared/System/Memory.cs,347) – canton7 Feb 26 '19 at 17:44

1 Answers1

2

From reading the comments in Memory<T>, it looks like it can absolutely be torn.

However, there seem to be two places where this actually matters: Memory<T>.Pin() and Memory<T>.Span.

The important thing to note is that (as far as I can work out) we don't care about tearing in a way which means we still point to somewhere in the object we refer to -- although our caller might get some strange data that it wasn't expecting, that's safe in the sense that they won't get an AccessViolationException. They will just have a race condition which produces unexpected results, as a consequence of having unsynchronized threaded access to a field.


Memory<T>.Span gets a Span<T> from the Memory<T>. It has this comment:

If the Memory or ReadOnlyMemory instance is torn, this property getter has undefined behavior. We try to detect this condition and throw an exception, but it's possible that a torn struct might appear to us to be valid, and we'll return an undesired span. Such a span is always guaranteed at least to be in-bounds when compared with the original Memory instance, so using the span won't AV the process.

So, we can absolutely have a torn Memory<T>, and then try to create a Span<T> from it. In this case, there's a check in the code which throws an exception if the Memory<T> has torn in such a way that it now refers to some memory outside of the object referred to by the Memory<T>.

If it has torn in a way that it still refers to somewhere in the original object, then that's OK - our caller might not be reading the thing it was expecting to read, but at least they won't get an AccessViolationException, which is what we're trying to avoid.

Note that Span<T> is unable to implement this same check (even if it wanted to). Memory<T> keeps references to the object, the start offset, and the length. Span<T> only keeps references to some memory address inside the object and the length.


Memory<T>.Pin() is an unsafe method, which has this comment:

It's possible that the below logic could result in an AV if the struct is torn. This is ok since the caller is expecting to use raw pointers, we're not required to keep this as safe as the other Span-based APIs.

Again, we can tear in a way that we no longer refer to somewhere inside the object we refer to. However this method is unsafe, and we don't care.

canton7
  • 37,633
  • 3
  • 64
  • 77
  • Thanks for your answer. I'm going to accept it, since it absolutely answers the question whether `Memory` also suffers from struct tearing or not. What I still don't fully understand is: The [docs](https://github.com/dotnet/corefxlab/blob/master/docs/specs/span.md#spant-will-be-stack-only) seem to (partially) justify the existence of `Memory` with `Span` being too vulnerable to tearing and therefore being made stack-only. Since it does not completely work around this issue, the tradeoff of having a separate type seems a little bit odd to me. – Peter Wurzinger Feb 26 '19 at 19:14
  • @Peter It works around the issue of a tear resulting in an AccessViolationException. I think that's the main concern. You can still have a tear resulting in something that wasn't what you expected. However, everything I've read about Span being stack-only was that it needs to use an interior pointer to be as fast as an array access (a major goal), but interior pointers are incredibly expensive for the GC (it needs to hunt around for the beginning of the object), and making Span stack-only is a good way to limit the number of them. That, and it lets Span point to stackalloc'd arrays. – canton7 Feb 26 '19 at 19:48
  • @Peter think about an array. You can mess up as badly as you like, but the runtime won't let you read beyond the end of the array. You can read the wrong element, but the the runtime makes sure that, whatever you're reading, it's an array element. A torn Span would let you read beyond the end of an array. If the memory you read was managed by the runtime, I think you'd just get garbage, not an exception. If Memory tears you can read the wrong array element, but its check stops you reading past the end of the array. – canton7 Feb 26 '19 at 20:07
  • @Peter so the aim is not to stop a tear from producing an invalid struct which reads the wrong array element: the aim is to stop you reading arbitrary memory locations which might not be part of any array! – canton7 Feb 26 '19 at 20:08
  • Thanks a lot for the clarification, things start to brighten up :-) Allow me one last question: *"A torn Span would let you read beyond the end of an array."* Couldn't this also be the case for `Memory`? The "only" difference - on a **very** high level - is, that `Memory` stores a managed object reference instead of a plain pointer and factors a `Span` instance for access. So in a scenario where `Memory` is torn, wouldn't `memory.Span[memory.Length]` also throw an AV? – Peter Wurzinger Feb 27 '19 at 09:41
  • 2
    @PeterWurzinger from my reading of the code in `Memory.Span`, because the `Memory` has a reference to object, it knows how big the object is (see `lengthOfUnderlyingSpan`). There's a test which does `if (_index + _length > lengthOfUnderlyingSpan) throw ...`. So if it's torn in such a way that `_index + _length` would let you read beyond the end of your array, it throws. `Span` can't do this, as it doesn't have a reference to the underlying object; only an interior pointer. – canton7 Feb 28 '19 at 09:08