Use local array in ConcurrentQueue<T> for small perf improvement (#21192)
authorStephen Toub <stoub@microsoft.com>
Tue, 27 Nov 2018 14:27:06 +0000 (09:27 -0500)
committerGitHub <noreply@github.com>
Tue, 27 Nov 2018 14:27:06 +0000 (09:27 -0500)
Enqueue/TryDequeue/TryPeek are all repeatedly accessing the same array from a readonly field.  Changing them to instead access that same array but from a cached local results in an ~10% throughput boost on a microbenchmark that does uncontended reads/writes of objects from/to the queue. (I also tried using a ref local to point directly to the target slot in the array, but that actually resulted in a measurable regression.)

src/System.Private.CoreLib/shared/System/Collections/Concurrent/ConcurrentQueueSegment.cs

index 24a172f..c706fae 100644 (file)
@@ -128,6 +128,8 @@ namespace System.Collections.Concurrent
         /// <summary>Tries to dequeue an element from the queue.</summary>
         public bool TryDequeue(out T item)
         {
+            Slot[] slots = _slots;
+            
             // Loop in case of contention...
             var spinner = new SpinWait();
             while (true)
@@ -137,7 +139,7 @@ namespace System.Collections.Concurrent
                 int slotsIndex = currentHead & _slotsMask;
 
                 // Read the sequence number for the head position.
-                int sequenceNumber = Volatile.Read(ref _slots[slotsIndex].SequenceNumber);
+                int sequenceNumber = Volatile.Read(ref slots[slotsIndex].SequenceNumber);
 
                 // We can dequeue from this slot if it's been filled by an enqueuer, which
                 // would have left the sequence number at pos+1.
@@ -156,14 +158,14 @@ namespace System.Collections.Concurrent
                     {
                         // Successfully reserved the slot.  Note that after the above CompareExchange, other threads
                         // trying to dequeue from this slot will end up spinning until we do the subsequent Write.
-                        item = _slots[slotsIndex].Item;
+                        item = slots[slotsIndex].Item;
                         if (!Volatile.Read(ref _preservedForObservation))
                         {
                             // If we're preserving, though, we don't zero out the slot, as we need it for
                             // enumerations, peeking, ToArray, etc.  And we don't update the sequence number,
                             // so that an enqueuer will see it as full and be forced to move to a new segment.
-                            _slots[slotsIndex].Item = default(T);
-                            Volatile.Write(ref _slots[slotsIndex].SequenceNumber, currentHead + _slots.Length);
+                            slots[slotsIndex].Item = default(T);
+                            Volatile.Write(ref slots[slotsIndex].SequenceNumber, currentHead + slots.Length);
                         }
                         return true;
                     }
@@ -208,6 +210,8 @@ namespace System.Collections.Concurrent
                 Interlocked.MemoryBarrier();
             }
 
+            Slot[] slots = _slots;
+
             // Loop in case of contention...
             var spinner = new SpinWait();
             while (true)
@@ -217,14 +221,14 @@ namespace System.Collections.Concurrent
                 int slotsIndex = currentHead & _slotsMask;
 
                 // Read the sequence number for the head position.
-                int sequenceNumber = Volatile.Read(ref _slots[slotsIndex].SequenceNumber);
+                int sequenceNumber = Volatile.Read(ref slots[slotsIndex].SequenceNumber);
 
                 // We can peek from this slot if it's been filled by an enqueuer, which
                 // would have left the sequence number at pos+1.
                 int diff = sequenceNumber - (currentHead + 1);
                 if (diff == 0)
                 {
-                    result = resultUsed ? _slots[slotsIndex].Item : default(T);
+                    result = resultUsed ? slots[slotsIndex].Item : default(T);
                     return true;
                 }
                 else if (diff < 0)
@@ -261,6 +265,8 @@ namespace System.Collections.Concurrent
         /// </summary>
         public bool TryEnqueue(T item)
         {
+            Slot[] slots = _slots;
+
             // Loop in case of contention...
             var spinner = new SpinWait();
             while (true)
@@ -270,7 +276,7 @@ namespace System.Collections.Concurrent
                 int slotsIndex = currentTail & _slotsMask;
 
                 // Read the sequence number for the tail position.
-                int sequenceNumber = Volatile.Read(ref _slots[slotsIndex].SequenceNumber);
+                int sequenceNumber = Volatile.Read(ref slots[slotsIndex].SequenceNumber);
 
                 // The slot is empty and ready for us to enqueue into it if its sequence
                 // number matches the slot.
@@ -289,8 +295,8 @@ namespace System.Collections.Concurrent
                     {
                         // Successfully reserved the slot.  Note that after the above CompareExchange, other threads
                         // trying to return will end up spinning until we do the subsequent Write.
-                        _slots[slotsIndex].Item = item;
-                        Volatile.Write(ref _slots[slotsIndex].SequenceNumber, currentTail + 1);
+                        slots[slotsIndex].Item = item;
+                        Volatile.Write(ref slots[slotsIndex].SequenceNumber, currentTail + 1);
                         return true;
                     }
                 }