Optimize GC.AllocateUninitializedArray and use it in StringBuilder (dotnet/coreclr...
authorAdam Sitnik <adam.sitnik@gmail.com>
Thu, 24 Oct 2019 16:45:57 +0000 (18:45 +0200)
committerGitHub <noreply@github.com>
Thu, 24 Oct 2019 16:45:57 +0000 (18:45 +0200)
* use GC.AllocateUninitializedArray for allocating internal char buffers

* force inlining of AllocateUninitializedArray to have no perf hit on the small buffers hot path

* insrease the threshold from 256 to 2048 bytes

* use Unsafe.As instead of a cast

* remove the size precondition from AllocateNewArray method, the called AllocateSzArray is responsible for handling negative size

Commit migrated from https://github.com/dotnet/coreclr/commit/c382edf50b2aadfc4f39d16fc53ea65ed35331e3

src/coreclr/src/System.Private.CoreLib/src/System/GC.cs
src/coreclr/src/vm/comutilnative.cpp
src/coreclr/tests/src/GC/API/GC/AllocateUninitializedArray.cs
src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs

index a01b73f..84169e8 100644 (file)
@@ -17,9 +17,7 @@ using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
 using System.Diagnostics;
 using System.Collections.Generic;
-#if !DEBUG
 using Internal.Runtime.CompilerServices;
-#endif
 
 namespace System
 {
@@ -651,8 +649,11 @@ namespace System
             }
         }
 
-        // Skips zero-initialization of the array if possible. If T contains object references,
-        // the array is always zero-initialized.
+        /// <summary>
+        /// Skips zero-initialization of the array if possible.
+        /// If T contains object references, the array is always zero-initialized.
+        /// </summary>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)] // forced to ensure no perf drop for small memory buffers (hot path)
         internal static T[] AllocateUninitializedArray<T>(int length)
         {
             if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
@@ -660,23 +661,20 @@ namespace System
                 return new T[length];
             }
 
-            if (length < 0)
-                ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.lengths, 0, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
-#if DEBUG
-            // in DEBUG arrays of any length can be created uninitialized
-#else
-            // otherwise small arrays are allocated using `new[]` as that is generally faster.
-            //
-            // The threshold was derived from various simulations.
-            // As it turned out the threshold depends on overal pattern of all allocations and is typically in 200-300 byte range.
-            // The gradient around the number is shallow (there is no perf cliff) and the exact value of the threshold does not matter a lot.
-            // So it is 256 bytes including array header.
-            if (Unsafe.SizeOf<T>() * length < 256 - 3 * IntPtr.Size)
+            // for debug builds we always want to call AllocateNewArray to detect AllocateNewArray bugs
+#if !DEBUG
+            // small arrays are allocated using `new[]` as that is generally faster.
+            if (length < 2048 / Unsafe.SizeOf<T>())
             {
                 return new T[length];
             }
 #endif
-            return (T[])AllocateNewArray(typeof(T[]).TypeHandle.Value, length, zeroingOptional: true);
+            // kept outside of the small arrays hot path to have inlining without big size growth
+            return AllocateNewUninitializedArray(length);
+
+            // remove the local function when https://github.com/dotnet/coreclr/issues/5329 is implemented
+            T[] AllocateNewUninitializedArray(int length)
+                => Unsafe.As<T[]>(AllocateNewArray(typeof(T[]).TypeHandle.Value, length, zeroingOptional: true));
         }
     }
 }
index 76bfa61..001a592 100644 (file)
@@ -1131,7 +1131,6 @@ FCIMPL3(Object*, GCInterface::AllocateNewArray, void* arrayTypeHandle, INT32 len
 {
     CONTRACTL {
         FCALL_CHECK;
-        PRECONDITION(length >= 0);
     } CONTRACTL_END;
 
     OBJECTREF pRet = NULL;
index 450fb6d..28dcd96 100644 (file)
@@ -76,15 +76,37 @@ public class Test {
 
         // negative size
         {
+            int GetNegativeValue() => -1;
+            int negativeSize = GetNegativeValue();
+            Type expectedExceptionType = null;
+
+            try
+            {
+                GC.KeepAlive(new byte[negativeSize]);
+
+                Console.WriteLine("Scenario 5 Expected exception (new operator)!");
+                return 1;
+            }
+            catch (Exception newOperatorEx)
+            {
+                expectedExceptionType = newOperatorEx.GetType();
+            }
+
             try
             {
                 var arr = AllocUninitialized<byte>.Call(-1);
 
-                Console.WriteLine("Scenario 5 Expected exception!");
+                Console.WriteLine("Scenario 5 Expected exception (GC.AllocateUninitializedArray)!");
                 return 1;
             }
-            catch (ArgumentOutOfRangeException)
+            catch (Exception allocUninitializedEx) when (allocUninitializedEx.GetType() == expectedExceptionType)
             {
+                // OK
+            }
+            catch (Exception other)
+            {
+                Console.WriteLine($"Scenario 5 Expected exception type mismatch: expected {expectedExceptionType}, but got {other.GetType()}!");
+                return 1;
             }
         }
 
index 89c0f65..f9c5fb4 100644 (file)
@@ -144,7 +144,7 @@ namespace System.Text
             }
             capacity = Math.Max(capacity, length);
 
-            m_ChunkChars = new char[capacity];
+            m_ChunkChars = GC.AllocateUninitializedArray<char>(capacity);
             m_ChunkLength = length;
 
             unsafe
@@ -182,7 +182,7 @@ namespace System.Text
             }
 
             m_MaxCapacity = maxCapacity;
-            m_ChunkChars = new char[capacity];
+            m_ChunkChars = GC.AllocateUninitializedArray<char>(capacity);
         }
 
         private StringBuilder(SerializationInfo info, StreamingContext context)
@@ -242,7 +242,7 @@ namespace System.Text
 
             // Assign
             m_MaxCapacity = persistedMaxCapacity;
-            m_ChunkChars = new char[persistedCapacity];
+            m_ChunkChars = GC.AllocateUninitializedArray<char>(persistedCapacity);
             persistedString.CopyTo(0, m_ChunkChars, 0, persistedString.Length);
             m_ChunkLength = persistedString.Length;
             m_ChunkPrevious = null;
@@ -314,7 +314,7 @@ namespace System.Text
                 if (Capacity != value)
                 {
                     int newLen = value - m_ChunkOffset;
-                    char[] newArray = new char[newLen];
+                    char[] newArray = GC.AllocateUninitializedArray<char>(newLen);
                     Array.Copy(m_ChunkChars, 0, newArray, 0, m_ChunkLength);
                     m_ChunkChars = newArray;
                 }
@@ -479,7 +479,7 @@ namespace System.Text
                         {
                             // We crossed a chunk boundary when reducing the Length. We must replace this middle-chunk with a new larger chunk,
                             // to ensure the capacity we want is preserved.
-                            char[] newArray = new char[newLen];
+                            char[] newArray = GC.AllocateUninitializedArray<char>(newLen);
                             Array.Copy(chunk.m_ChunkChars, 0, newArray, 0, chunk.m_ChunkLength);
                             m_ChunkChars = newArray;
                         }
@@ -2423,7 +2423,7 @@ namespace System.Text
             }
 
             // Allocate the array before updating any state to avoid leaving inconsistent state behind in case of out of memory exception
-            char[] chunkChars = new char[newBlockLength];
+            char[] chunkChars = GC.AllocateUninitializedArray<char>(newBlockLength);
 
             // Move all of the data from this chunk to a new one, via a few O(1) pointer adjustments.
             // Then, have this chunk point to the new one as its predecessor.
@@ -2569,7 +2569,7 @@ namespace System.Text
             Debug.Assert(size > 0);
             Debug.Assert(maxCapacity > 0);
 
-            m_ChunkChars = new char[size];
+            m_ChunkChars = GC.AllocateUninitializedArray<char>(size);
             m_MaxCapacity = maxCapacity;
             m_ChunkPrevious = previousBlock;
             if (previousBlock != null)