Skip to content

Commit d8d6927

Browse files
[release/8.0] Improve performance of UnmanagedMemoryStream (#93812)
* Improve performance of UnmanagedMemoryStream UnmanagedMemoryStream used Interlocked operations to update its state to prevent tearing of 64-bit values on 32-bit platforms. This pattern is expensive in general and it was found to be prohibitively expensive on recent hardware.. This change removes the expensive Interlocked operations and addresses the tearing issues in alternative way: - The _length field is converted to nuint that is guaranteed to be updated atomically. - Writes to _length field are volatile to guaranteed the unininitialized memory cannot be read. - The _position field remains long and it has a risk of tearing. It is not a problem since tearing of this field cannot lead to buffer overruns. Fixes #93624 * Add comment --------- Co-authored-by: Jan Kotas <[email protected]>
1 parent e1da68d commit d8d6927

File tree

1 file changed

+63
-63
lines changed

1 file changed

+63
-63
lines changed

src/libraries/System.Private.CoreLib/src/System/IO/UnmanagedMemoryStream.cs

+63-63
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,9 @@ namespace System.IO
2323
* of the UnmanagedMemoryStream.
2424
* 3) You clean up the memory when appropriate. The UnmanagedMemoryStream
2525
* currently will do NOTHING to free this memory.
26-
* 4) All calls to Write and WriteByte may not be threadsafe currently.
27-
*
28-
* It may become necessary to add in some sort of
29-
* DeallocationMode enum, specifying whether we unmap a section of memory,
30-
* call free, run a user-provided delegate to free the memory, etc.
31-
* We'll suggest user write a subclass of UnmanagedMemoryStream that uses
32-
* a SafeHandle subclass to hold onto the memory.
33-
*
26+
* 4) This type is not thread safe. However, the implementation should prevent buffer
27+
* overruns or returning uninitialized memory when Reads and Writes are called
28+
* concurrently in thread unsafe manner.
3429
*/
3530

3631
/// <summary>
@@ -40,10 +35,10 @@ public class UnmanagedMemoryStream : Stream
4035
{
4136
private SafeBuffer? _buffer;
4237
private unsafe byte* _mem;
43-
private long _length;
44-
private long _capacity;
45-
private long _position;
46-
private long _offset;
38+
private nuint _capacity;
39+
private nuint _offset;
40+
private nuint _length; // nuint to guarantee atomic access on 32-bit platforms
41+
private long _position; // long to allow seeking to any location beyond the length of the stream.
4742
private FileAccess _access;
4843
private bool _isOpen;
4944
private CachedCompletedInt32Task _lastReadTask; // The last successful task returned from ReadAsync
@@ -123,10 +118,10 @@ protected void Initialize(SafeBuffer buffer, long offset, long length, FileAcces
123118
}
124119
}
125120

126-
_offset = offset;
121+
_offset = (nuint)offset;
127122
_buffer = buffer;
128-
_length = length;
129-
_capacity = length;
123+
_length = (nuint)length;
124+
_capacity = (nuint)length;
130125
_access = access;
131126
_isOpen = true;
132127
}
@@ -171,8 +166,8 @@ protected unsafe void Initialize(byte* pointer, long length, long capacity, File
171166

172167
_mem = pointer;
173168
_offset = 0;
174-
_length = length;
175-
_capacity = capacity;
169+
_length = (nuint)length;
170+
_capacity = (nuint)capacity;
176171
_access = access;
177172
_isOpen = true;
178173
}
@@ -259,7 +254,7 @@ public override long Length
259254
get
260255
{
261256
EnsureNotClosed();
262-
return Interlocked.Read(ref _length);
257+
return (long)_length;
263258
}
264259
}
265260

@@ -271,7 +266,7 @@ public long Capacity
271266
get
272267
{
273268
EnsureNotClosed();
274-
return _capacity;
269+
return (long)_capacity;
275270
}
276271
}
277272

@@ -283,14 +278,14 @@ public override long Position
283278
get
284279
{
285280
if (!CanSeek) ThrowHelper.ThrowObjectDisposedException_StreamClosed(null);
286-
return Interlocked.Read(ref _position);
281+
return _position;
287282
}
288283
set
289284
{
290285
ArgumentOutOfRangeException.ThrowIfNegative(value);
291286
if (!CanSeek) ThrowHelper.ThrowObjectDisposedException_StreamClosed(null);
292287

293-
Interlocked.Exchange(ref _position, value);
288+
_position = value;
294289
}
295290
}
296291

@@ -308,11 +303,10 @@ public unsafe byte* PositionPointer
308303
EnsureNotClosed();
309304

310305
// Use a temp to avoid a race
311-
long pos = Interlocked.Read(ref _position);
312-
if (pos > _capacity)
306+
long pos = _position;
307+
if (pos > (long)_capacity)
313308
throw new IndexOutOfRangeException(SR.IndexOutOfRange_UMSPosition);
314-
byte* ptr = _mem + pos;
315-
return ptr;
309+
return _mem + pos;
316310
}
317311
set
318312
{
@@ -327,7 +321,7 @@ public unsafe byte* PositionPointer
327321
if (newPosition < 0)
328322
throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_UnmanagedMemStreamLength);
329323

330-
Interlocked.Exchange(ref _position, newPosition);
324+
_position = newPosition;
331325
}
332326
}
333327

@@ -367,8 +361,13 @@ internal int ReadCore(Span<byte> buffer)
367361

368362
// Use a local variable to avoid a race where another thread
369363
// changes our position after we decide we can read some bytes.
370-
long pos = Interlocked.Read(ref _position);
371-
long len = Interlocked.Read(ref _length);
364+
long pos = _position;
365+
366+
// Use a volatile read to prevent reading of the uninitialized memory. This volatile read
367+
// and matching volatile write that set _length avoids reordering of NativeMemory.Clear
368+
// operations with reading of the buffer below.
369+
long len = (long)Volatile.Read(ref _length);
370+
372371
long n = Math.Min(len - pos, buffer.Length);
373372
if (n <= 0)
374373
{
@@ -407,7 +406,7 @@ internal int ReadCore(Span<byte> buffer)
407406
}
408407
}
409408

410-
Interlocked.Exchange(ref _position, pos + n);
409+
_position = pos + n;
411410
return nInt;
412411
}
413412

@@ -484,11 +483,16 @@ public override int ReadByte()
484483
EnsureNotClosed();
485484
EnsureReadable();
486485

487-
long pos = Interlocked.Read(ref _position); // Use a local to avoid a race condition
488-
long len = Interlocked.Read(ref _length);
486+
long pos = _position; // Use a local to avoid a race condition
487+
488+
// Use a volatile read to prevent reading of the uninitialized memory. This volatile read
489+
// and matching volatile write that set _length avoids reordering of NativeMemory.Clear
490+
// operations with reading of the buffer below.
491+
long len = (long)Volatile.Read(ref _length);
492+
489493
if (pos >= len)
490494
return -1;
491-
Interlocked.Exchange(ref _position, pos + 1);
495+
_position = pos + 1;
492496
int result;
493497
if (_buffer != null)
494498
{
@@ -529,35 +533,33 @@ public override long Seek(long offset, SeekOrigin loc)
529533
{
530534
EnsureNotClosed();
531535

536+
long newPosition;
532537
switch (loc)
533538
{
534539
case SeekOrigin.Begin:
535-
if (offset < 0)
540+
newPosition = offset;
541+
if (newPosition < 0)
536542
throw new IOException(SR.IO_SeekBeforeBegin);
537-
Interlocked.Exchange(ref _position, offset);
538543
break;
539544

540545
case SeekOrigin.Current:
541-
long pos = Interlocked.Read(ref _position);
542-
if (offset + pos < 0)
546+
newPosition = _position + offset;
547+
if (newPosition < 0)
543548
throw new IOException(SR.IO_SeekBeforeBegin);
544-
Interlocked.Exchange(ref _position, offset + pos);
545549
break;
546550

547551
case SeekOrigin.End:
548-
long len = Interlocked.Read(ref _length);
549-
if (len + offset < 0)
552+
newPosition = (long)_length + offset;
553+
if (newPosition < 0)
550554
throw new IOException(SR.IO_SeekBeforeBegin);
551-
Interlocked.Exchange(ref _position, len + offset);
552555
break;
553556

554557
default:
555558
throw new ArgumentException(SR.Argument_InvalidSeekOrigin);
556559
}
557560

558-
long finalPos = Interlocked.Read(ref _position);
559-
Debug.Assert(finalPos >= 0, "_position >= 0");
560-
return finalPos;
561+
_position = newPosition;
562+
return newPosition;
561563
}
562564

563565
/// <summary>
@@ -573,22 +575,22 @@ public override void SetLength(long value)
573575
EnsureNotClosed();
574576
EnsureWriteable();
575577

576-
if (value > _capacity)
578+
if (value > (long)_capacity)
577579
throw new IOException(SR.IO_FixedCapacity);
578580

579-
long pos = Interlocked.Read(ref _position);
580-
long len = Interlocked.Read(ref _length);
581+
long len = (long)_length;
581582
if (value > len)
582583
{
583584
unsafe
584585
{
585586
NativeMemory.Clear(_mem + len, (nuint)(value - len));
586587
}
587588
}
588-
Interlocked.Exchange(ref _length, value);
589-
if (pos > value)
589+
Volatile.Write(ref _length, (nuint)value); // volatile to prevent reading of uninitialized memory
590+
591+
if (_position > value)
590592
{
591-
Interlocked.Exchange(ref _position, value);
593+
_position = value;
592594
}
593595
}
594596

@@ -625,16 +627,16 @@ internal unsafe void WriteCore(ReadOnlySpan<byte> buffer)
625627
EnsureNotClosed();
626628
EnsureWriteable();
627629

628-
long pos = Interlocked.Read(ref _position); // Use a local to avoid a race condition
629-
long len = Interlocked.Read(ref _length);
630+
long pos = _position; // Use a local to avoid a race condition
631+
long len = (long)_length;
630632
long n = pos + buffer.Length;
631633
// Check for overflow
632634
if (n < 0)
633635
{
634636
throw new IOException(SR.IO_StreamTooLong);
635637
}
636638

637-
if (n > _capacity)
639+
if (n > (long)_capacity)
638640
{
639641
throw new NotSupportedException(SR.IO_FixedCapacity);
640642
}
@@ -648,16 +650,16 @@ internal unsafe void WriteCore(ReadOnlySpan<byte> buffer)
648650
NativeMemory.Clear(_mem + len, (nuint)(pos - len));
649651
}
650652

651-
// set length after zeroing memory to avoid race condition of accessing unzeroed memory
653+
// set length after zeroing memory to avoid race condition of accessing uninitialized memory
652654
if (n > len)
653655
{
654-
Interlocked.Exchange(ref _length, n);
656+
Volatile.Write(ref _length, (nuint)n); // volatile to prevent reading of uninitialized memory
655657
}
656658
}
657659

658660
if (_buffer != null)
659661
{
660-
long bytesLeft = _capacity - pos;
662+
long bytesLeft = (long)_capacity - pos;
661663
if (bytesLeft < buffer.Length)
662664
{
663665
throw new ArgumentException(SR.Arg_BufferTooSmall);
@@ -682,8 +684,7 @@ internal unsafe void WriteCore(ReadOnlySpan<byte> buffer)
682684
Buffer.Memmove(ref *(_mem + pos), ref MemoryMarshal.GetReference(buffer), (nuint)buffer.Length);
683685
}
684686

685-
Interlocked.Exchange(ref _position, n);
686-
return;
687+
_position = n;
687688
}
688689

689690
/// <summary>
@@ -754,16 +755,16 @@ public override void WriteByte(byte value)
754755
EnsureNotClosed();
755756
EnsureWriteable();
756757

757-
long pos = Interlocked.Read(ref _position); // Use a local to avoid a race condition
758-
long len = Interlocked.Read(ref _length);
758+
long pos = _position; // Use a local to avoid a race condition
759+
long len = (long)_length;
759760
long n = pos + 1;
760761
if (pos >= len)
761762
{
762763
// Check for overflow
763764
if (n < 0)
764765
throw new IOException(SR.IO_StreamTooLong);
765766

766-
if (n > _capacity)
767+
if (n > (long)_capacity)
767768
throw new NotSupportedException(SR.IO_FixedCapacity);
768769

769770
// Check to see whether we are now expanding the stream and must
@@ -779,8 +780,7 @@ public override void WriteByte(byte value)
779780
}
780781
}
781782

782-
// set length after zeroing memory to avoid race condition of accessing unzeroed memory
783-
Interlocked.Exchange(ref _length, n);
783+
Volatile.Write(ref _length, (nuint)n); // volatile to prevent reading of uninitialized memory
784784
}
785785
}
786786

@@ -810,7 +810,7 @@ public override void WriteByte(byte value)
810810
_mem[pos] = value;
811811
}
812812
}
813-
Interlocked.Exchange(ref _position, n);
813+
_position = n;
814814
}
815815
}
816816
}

0 commit comments

Comments
 (0)