Skip to content

Commit 3a43523

Browse files
jkotasgithub-actions
authored and
github-actions
committed
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
1 parent 11ad607 commit 3a43523

File tree

1 file changed

+53
-63
lines changed

1 file changed

+53
-63
lines changed

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

+53-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,8 @@ 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+
long len = (long)Volatile.Read(ref _length);
372366
long n = Math.Min(len - pos, buffer.Length);
373367
if (n <= 0)
374368
{
@@ -407,7 +401,7 @@ internal int ReadCore(Span<byte> buffer)
407401
}
408402
}
409403

410-
Interlocked.Exchange(ref _position, pos + n);
404+
_position = pos + n;
411405
return nInt;
412406
}
413407

@@ -484,11 +478,11 @@ public override int ReadByte()
484478
EnsureNotClosed();
485479
EnsureReadable();
486480

487-
long pos = Interlocked.Read(ref _position); // Use a local to avoid a race condition
488-
long len = Interlocked.Read(ref _length);
481+
long pos = _position; // Use a local to avoid a race condition
482+
long len = (long)Volatile.Read(ref _length);
489483
if (pos >= len)
490484
return -1;
491-
Interlocked.Exchange(ref _position, pos + 1);
485+
_position = pos + 1;
492486
int result;
493487
if (_buffer != null)
494488
{
@@ -529,35 +523,33 @@ public override long Seek(long offset, SeekOrigin loc)
529523
{
530524
EnsureNotClosed();
531525

526+
long newPosition;
532527
switch (loc)
533528
{
534529
case SeekOrigin.Begin:
535-
if (offset < 0)
530+
newPosition = offset;
531+
if (newPosition < 0)
536532
throw new IOException(SR.IO_SeekBeforeBegin);
537-
Interlocked.Exchange(ref _position, offset);
538533
break;
539534

540535
case SeekOrigin.Current:
541-
long pos = Interlocked.Read(ref _position);
542-
if (offset + pos < 0)
536+
newPosition = _position + offset;
537+
if (newPosition < 0)
543538
throw new IOException(SR.IO_SeekBeforeBegin);
544-
Interlocked.Exchange(ref _position, offset + pos);
545539
break;
546540

547541
case SeekOrigin.End:
548-
long len = Interlocked.Read(ref _length);
549-
if (len + offset < 0)
542+
newPosition = (long)_length + offset;
543+
if (newPosition < 0)
550544
throw new IOException(SR.IO_SeekBeforeBegin);
551-
Interlocked.Exchange(ref _position, len + offset);
552545
break;
553546

554547
default:
555548
throw new ArgumentException(SR.Argument_InvalidSeekOrigin);
556549
}
557550

558-
long finalPos = Interlocked.Read(ref _position);
559-
Debug.Assert(finalPos >= 0, "_position >= 0");
560-
return finalPos;
551+
_position = newPosition;
552+
return newPosition;
561553
}
562554

563555
/// <summary>
@@ -573,22 +565,22 @@ public override void SetLength(long value)
573565
EnsureNotClosed();
574566
EnsureWriteable();
575567

576-
if (value > _capacity)
568+
if (value > (long)_capacity)
577569
throw new IOException(SR.IO_FixedCapacity);
578570

579-
long pos = Interlocked.Read(ref _position);
580-
long len = Interlocked.Read(ref _length);
571+
long len = (long)_length;
581572
if (value > len)
582573
{
583574
unsafe
584575
{
585576
NativeMemory.Clear(_mem + len, (nuint)(value - len));
586577
}
587578
}
588-
Interlocked.Exchange(ref _length, value);
589-
if (pos > value)
579+
Volatile.Write(ref _length, (nuint)value); // volatile to prevent reading of uninitialized memory
580+
581+
if (_position > value)
590582
{
591-
Interlocked.Exchange(ref _position, value);
583+
_position = value;
592584
}
593585
}
594586

@@ -625,16 +617,16 @@ internal unsafe void WriteCore(ReadOnlySpan<byte> buffer)
625617
EnsureNotClosed();
626618
EnsureWriteable();
627619

628-
long pos = Interlocked.Read(ref _position); // Use a local to avoid a race condition
629-
long len = Interlocked.Read(ref _length);
620+
long pos = _position; // Use a local to avoid a race condition
621+
long len = (long)_length;
630622
long n = pos + buffer.Length;
631623
// Check for overflow
632624
if (n < 0)
633625
{
634626
throw new IOException(SR.IO_StreamTooLong);
635627
}
636628

637-
if (n > _capacity)
629+
if (n > (long)_capacity)
638630
{
639631
throw new NotSupportedException(SR.IO_FixedCapacity);
640632
}
@@ -648,16 +640,16 @@ internal unsafe void WriteCore(ReadOnlySpan<byte> buffer)
648640
NativeMemory.Clear(_mem + len, (nuint)(pos - len));
649641
}
650642

651-
// set length after zeroing memory to avoid race condition of accessing unzeroed memory
643+
// set length after zeroing memory to avoid race condition of accessing uninitialized memory
652644
if (n > len)
653645
{
654-
Interlocked.Exchange(ref _length, n);
646+
Volatile.Write(ref _length, (nuint)n); // volatile to prevent reading of uninitialized memory
655647
}
656648
}
657649

658650
if (_buffer != null)
659651
{
660-
long bytesLeft = _capacity - pos;
652+
long bytesLeft = (long)_capacity - pos;
661653
if (bytesLeft < buffer.Length)
662654
{
663655
throw new ArgumentException(SR.Arg_BufferTooSmall);
@@ -682,8 +674,7 @@ internal unsafe void WriteCore(ReadOnlySpan<byte> buffer)
682674
Buffer.Memmove(ref *(_mem + pos), ref MemoryMarshal.GetReference(buffer), (nuint)buffer.Length);
683675
}
684676

685-
Interlocked.Exchange(ref _position, n);
686-
return;
677+
_position = n;
687678
}
688679

689680
/// <summary>
@@ -754,16 +745,16 @@ public override void WriteByte(byte value)
754745
EnsureNotClosed();
755746
EnsureWriteable();
756747

757-
long pos = Interlocked.Read(ref _position); // Use a local to avoid a race condition
758-
long len = Interlocked.Read(ref _length);
748+
long pos = _position; // Use a local to avoid a race condition
749+
long len = (long)_length;
759750
long n = pos + 1;
760751
if (pos >= len)
761752
{
762753
// Check for overflow
763754
if (n < 0)
764755
throw new IOException(SR.IO_StreamTooLong);
765756

766-
if (n > _capacity)
757+
if (n > (long)_capacity)
767758
throw new NotSupportedException(SR.IO_FixedCapacity);
768759

769760
// Check to see whether we are now expanding the stream and must
@@ -779,8 +770,7 @@ public override void WriteByte(byte value)
779770
}
780771
}
781772

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

@@ -810,7 +800,7 @@ public override void WriteByte(byte value)
810800
_mem[pos] = value;
811801
}
812802
}
813-
Interlocked.Exchange(ref _position, n);
803+
_position = n;
814804
}
815805
}
816806
}

0 commit comments

Comments
 (0)