Improve Crossgen2 performance via sorting improvements (#35870)
authorDavid Wrighton <davidwr@microsoft.com>
Wed, 6 May 2020 17:35:48 +0000 (10:35 -0700)
committerGitHub <noreply@github.com>
Wed, 6 May 2020 17:35:48 +0000 (10:35 -0700)
* Improve Crossgen2 performance
- Crossgen2 makes extensive use of sorting to achieve determinism
- Replace most signifcant sort operations with a parallel mergesort
  - These sorts use rather expensive comparison functions and as such the standard sort algorithm which is tuned more to reduce memory usage than reduce comparisons is not ideal, and merge sort works better
  - Primarily sorting is performed during the previously single threaded portions of the product. Adding parallelism yields easy wins
  - In a large benchmark overall compilation performance improved from 106 seconds to 82 seconds
- Implementation makes extensive use of generics to share the implementation across the various apis that are used for sorting in crossgen2
- Add Detection on of duplicates in sort algorithm
- Perf result compiling a large set of assemblies
  - Using standard sorts 121.6 seconds
  - Using parallel merge sort 96.9 seconds

15 files changed:
src/coreclr/src/tools/Common/Sorting/ArrayAccessor.cs [new file with mode: 0644]
src/coreclr/src/tools/Common/Sorting/ICompareAsEqualAction.cs [new file with mode: 0644]
src/coreclr/src/tools/Common/Sorting/ISortableDataStructureAccessor.cs [new file with mode: 0644]
src/coreclr/src/tools/Common/Sorting/ListAccessor.cs [new file with mode: 0644]
src/coreclr/src/tools/Common/Sorting/MergeSort.cs [new file with mode: 0644]
src/coreclr/src/tools/Common/Sorting/MergeSortCore.cs [new file with mode: 0644]
src/coreclr/src/tools/crossgen2/ILCompiler.DependencyAnalysisFramework/DependencyAnalyzer.cs
src/coreclr/src/tools/crossgen2/ILCompiler.DependencyAnalysisFramework/ILCompiler.DependencyAnalysisFramework.csproj
src/coreclr/src/tools/crossgen2/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ArrayOfEmbeddedDataNode.cs
src/coreclr/src/tools/crossgen2/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/GCRefMapNode.cs
src/coreclr/src/tools/crossgen2/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/HeaderNode.cs
src/coreclr/src/tools/crossgen2/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InliningInfoNode.cs
src/coreclr/src/tools/crossgen2/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/MethodWithGCInfo.cs
src/coreclr/src/tools/crossgen2/ILCompiler.ReadyToRun/Compiler/ReadyToRunTableManager.cs
src/coreclr/src/tools/crossgen2/ILCompiler.ReadyToRun/ObjectWriter/SectionBuilder.cs

diff --git a/src/coreclr/src/tools/Common/Sorting/ArrayAccessor.cs b/src/coreclr/src/tools/Common/Sorting/ArrayAccessor.cs
new file mode 100644 (file)
index 0000000..376e452
--- /dev/null
@@ -0,0 +1,38 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+
+namespace ILCompiler.Sorting.Implementation
+{
+    internal struct ArrayAccessor<T> : ISortableDataStructureAccessor<T, T[]>
+    {
+        public void Copy(T[] source, int sourceIndex, T[] target, int destIndex, int length)
+        {
+            Array.Copy(source, sourceIndex, target, destIndex, length);
+        }
+
+        public T GetElement(T[] dataStructure, int i)
+        {
+            return dataStructure[i];
+        }
+
+        public int GetLength(T[] dataStructure)
+        {
+            return dataStructure.Length;
+        }
+
+        public void SetElement(T[] dataStructure, int i, T value)
+        {
+            dataStructure[i] = value;
+        }
+
+        public void SwapElements(T[] dataStructure, int i, int i2)
+        {
+            T temp = dataStructure[i];
+            dataStructure[i] = dataStructure[i + 1];
+            dataStructure[i + 1] = temp;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/coreclr/src/tools/Common/Sorting/ICompareAsEqualAction.cs b/src/coreclr/src/tools/Common/Sorting/ICompareAsEqualAction.cs
new file mode 100644 (file)
index 0000000..8307aac
--- /dev/null
@@ -0,0 +1,28 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Diagnostics;
+
+namespace ILCompiler
+{
+    internal interface ICompareAsEqualAction
+    {
+        void CompareAsEqual();
+    }
+
+    internal struct RequireTotalOrderAssert : ICompareAsEqualAction
+    {
+        public void CompareAsEqual()
+        {
+            Debug.Assert(false);
+        }
+    }
+
+    internal struct AllowDuplicates : ICompareAsEqualAction
+    {
+        public void CompareAsEqual()
+        {
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/coreclr/src/tools/Common/Sorting/ISortableDataStructureAccessor.cs b/src/coreclr/src/tools/Common/Sorting/ISortableDataStructureAccessor.cs
new file mode 100644 (file)
index 0000000..8b2d1ba
--- /dev/null
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace ILCompiler
+{
+    internal interface ISortableDataStructureAccessor<T, TDataStructure>
+    {
+        T GetElement(TDataStructure dataStructure, int i);
+        void SetElement(TDataStructure dataStructure, int i, T value);
+        void SwapElements(TDataStructure dataStructure, int i, int i2);
+        void Copy(TDataStructure source, int sourceIndex, T[] target, int destIndex, int length);
+        void Copy(T[] source, int sourceIndex, TDataStructure target, int destIndex, int length);
+        int GetLength(TDataStructure dataStructure);
+    }
+}
\ No newline at end of file
diff --git a/src/coreclr/src/tools/Common/Sorting/ListAccessor.cs b/src/coreclr/src/tools/Common/Sorting/ListAccessor.cs
new file mode 100644 (file)
index 0000000..75d97de
--- /dev/null
@@ -0,0 +1,47 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+
+namespace ILCompiler.Sorting.Implementation
+{
+    internal struct ListAccessor<T> : ISortableDataStructureAccessor<T, List<T>>
+    {
+        public void Copy(List<T> source, int sourceIndex, T[] target, int destIndex, int length)
+        {
+            source.CopyTo(sourceIndex, target, destIndex, length);
+        }
+
+        public void Copy(T[] source, int sourceIndex, List<T> target, int destIndex, int length)
+        {
+            for (int i = 0; i < length; i++)
+            {
+                target[i + destIndex] = source[i + sourceIndex];
+            }
+        }
+
+        public T GetElement(List<T> dataStructure, int i)
+        {
+            return dataStructure[i];
+        }
+
+        public int GetLength(List<T> dataStructure)
+        {
+            return dataStructure.Count;
+        }
+
+        public void SetElement(List<T> dataStructure, int i, T value)
+        {
+            dataStructure[i] = value;
+        }
+
+        public void SwapElements(List<T> dataStructure, int i, int i2)
+        {
+            T temp = dataStructure[i];
+            dataStructure[i] = dataStructure[i + 1];
+            dataStructure[i + 1] = temp;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/coreclr/src/tools/Common/Sorting/MergeSort.cs b/src/coreclr/src/tools/Common/Sorting/MergeSort.cs
new file mode 100644 (file)
index 0000000..f05d32e
--- /dev/null
@@ -0,0 +1,76 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using ILCompiler.Sorting.Implementation;
+
+namespace ILCompiler
+{
+    public static class MergeSortApi
+    {
+        // Parallel sorting api which will sort in parallel when appropriate
+        public static void MergeSort<T>(this List<T> listToSort, Comparison<T> comparison)
+        {
+            MergeSortCore<T, List<T>, ListAccessor<T>, ComparisonWrapper<T>, RequireTotalOrderAssert>.ParallelSortApi(listToSort, new ComparisonWrapper<T>(comparison));
+        }
+
+        // Parallel sorting api which will sort in parallel when appropriate
+        public static void MergeSortAllowDuplicates<T>(this List<T> listToSort, Comparison<T> comparison)
+        {
+            MergeSortCore<T, List<T>, ListAccessor<T>, ComparisonWrapper<T>, AllowDuplicates>.ParallelSortApi(listToSort, new ComparisonWrapper<T>(comparison));
+        }
+
+        // Parallel sorting api which will sort in parallel when appropriate
+        public static void MergeSort<T>(this List<T> listToSort, IComparer<T> comparer)
+        {
+            MergeSortCore<T, List<T>, ListAccessor<T>, IComparer<T>, RequireTotalOrderAssert>.ParallelSortApi(listToSort, comparer);
+        }
+
+        // Parallel sorting api which will sort in parallel when appropriate
+        public static void MergeSortAllowDuplicates<T>(this List<T> listToSort, IComparer<T> comparer)
+        {
+            MergeSortCore<T, List<T>, ListAccessor<T>, IComparer<T>, AllowDuplicates>.ParallelSortApi(listToSort, comparer);
+        }
+
+        // Parallel sorting api which will sort in parallel when appropriate
+        public static void MergeSort<T>(this T[] arrayToSort, Comparison<T> comparison)
+        {
+            MergeSortCore<T, T[], ArrayAccessor<T>, ComparisonWrapper<T>, RequireTotalOrderAssert>.ParallelSortApi(arrayToSort, new ComparisonWrapper<T>(comparison));
+        }
+
+        // Parallel sorting api which will sort in parallel when appropriate
+        public static void MergeSortAllowDuplicates<T>(this T[] arrayToSort, Comparison<T> comparison)
+        {
+            MergeSortCore<T, T[], ArrayAccessor<T>, ComparisonWrapper<T>, AllowDuplicates>.ParallelSortApi(arrayToSort, new ComparisonWrapper<T>(comparison));
+        }
+
+        // Parallel sorting api which will sort in parallel when appropriate
+        public static void MergeSort<T>(this T[] arrayToSort, IComparer<T> comparer)
+        {
+            MergeSortCore<T, T[], ArrayAccessor<T>, IComparer<T>, RequireTotalOrderAssert>.ParallelSortApi(arrayToSort, comparer);
+        }
+
+        // Parallel sorting api which will sort in parallel when appropriate
+        public static void MergeSortAllowDuplicates<T>(this T[] arrayToSort, IComparer<T> comparer)
+        {
+            MergeSortCore<T, T[], ArrayAccessor<T>, IComparer<T>, AllowDuplicates>.ParallelSortApi(arrayToSort, comparer);
+        }
+
+
+        // Internal helper struct used to enable use of Comparison<T> delegates instead of IComparer<T> instances
+        private struct ComparisonWrapper<T> : IComparer<T>
+        {
+            Comparison<T> _comparison;
+            public ComparisonWrapper(Comparison<T> comparison)
+            {
+                _comparison = comparison;
+            }
+            int IComparer<T>.Compare(T x, T y)
+            {
+                return _comparison(x, y);
+            }
+        }
+    }
+}
diff --git a/src/coreclr/src/tools/Common/Sorting/MergeSortCore.cs b/src/coreclr/src/tools/Common/Sorting/MergeSortCore.cs
new file mode 100644 (file)
index 0000000..1a204be
--- /dev/null
@@ -0,0 +1,130 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace ILCompiler.Sorting.Implementation
+{
+    internal static class MergeSortCore<T, TDataStructure, TDataStructureAccessor, TComparer, TCompareAsEqualAction>
+        where TDataStructureAccessor:ISortableDataStructureAccessor<T, TDataStructure>
+        where TComparer:IComparer<T>
+        where TCompareAsEqualAction : ICompareAsEqualAction
+    {
+        internal const int ParallelSortThreshold = 4000; // Number empirically measured by compiling
+                                                         // a large composite binary
+
+        public static void ParallelSortApi(TDataStructure arrayToSort, TComparer comparer)
+        {
+            TDataStructureAccessor accessor = default(TDataStructureAccessor);
+            if (accessor.GetLength(arrayToSort) < ParallelSortThreshold)
+            {
+                // If the array is sufficiently small as to not need parallel sorting
+                // call the sequential algorithm directly, as the parallel api will create
+                // an unnecessary Task object to wait on
+                SequentialSort(arrayToSort, 0, accessor.GetLength(arrayToSort), comparer);
+            }
+            ParallelSort(arrayToSort, 0, accessor.GetLength(arrayToSort), comparer).Wait();
+        }
+
+        // Parallelized merge sort algorithm. Uses Task infrastructure to spread sort across available resources
+        private static async Task ParallelSort(TDataStructure arrayToSort, int index, int length, TComparer comparer)
+        {
+            if (length < ParallelSortThreshold)
+            {
+                SequentialSort(arrayToSort, index, length, comparer);
+            }
+            else
+            {
+                TDataStructureAccessor accessor = default(TDataStructureAccessor);
+                int halfLen = length / 2;
+
+                TaskCompletionSource<bool> rightSortComplete = new System.Threading.Tasks.TaskCompletionSource<bool>();
+                _ = Task.Run(async () =>
+                {
+                    await ParallelSort(arrayToSort, index + halfLen, length - halfLen, comparer);
+                    rightSortComplete.SetResult(true);
+                });
+
+                T[] localCopyOfHalfOfArray = new T[halfLen];
+                accessor.Copy(arrayToSort, index, localCopyOfHalfOfArray, 0, halfLen);
+                await MergeSortCore<T, T[], ArrayAccessor<T>, TComparer, TCompareAsEqualAction>.ParallelSort(localCopyOfHalfOfArray, 0, halfLen, comparer);
+                await rightSortComplete.Task;
+                Merge(localCopyOfHalfOfArray, arrayToSort, index, halfLen, length, comparer);
+            }
+        }
+
+        // Normal non-parallel merge sort
+        // Allocates length/2 in scratch space
+        private static void SequentialSort(TDataStructure arrayToSort, int index, int length, TComparer comparer)
+        {
+            TDataStructureAccessor accessor = default(TDataStructureAccessor);
+            T[] scratchSpace = new T[accessor.GetLength(arrayToSort) / 2];
+            MergeSortHelper(arrayToSort, index, length, comparer, scratchSpace);
+        }
+
+        // Non-parallel merge sort, used once the region to be sorted is small enough
+        // scratchSpace must be at least length/2 in size
+        private static void MergeSortHelper(TDataStructure arrayToSort, int index, int length, TComparer comparer, T[] scratchSpace)
+        {
+            if (length <= 1)
+            {
+                return;
+            }
+            TDataStructureAccessor accessor = default(TDataStructureAccessor);
+            if (length == 2)
+            {
+                if (comparer.Compare(accessor.GetElement(arrayToSort, index), accessor.GetElement(arrayToSort, index + 1)) > 0)
+                {
+                    accessor.SwapElements(arrayToSort, index, index + 1);
+                }
+                return;
+            }
+
+            int halfLen = length / 2;
+            MergeSortHelper(arrayToSort, index, halfLen, comparer, scratchSpace);
+            MergeSortHelper(arrayToSort, index + halfLen, length - halfLen, comparer, scratchSpace);
+            accessor.Copy(arrayToSort, index, scratchSpace, 0, halfLen);
+            Merge(scratchSpace, arrayToSort, index, halfLen, length, comparer);
+        }
+
+        // Shared merge algorithm used in both parallel and sequential variants of the mergesort
+        private static void Merge(T[] localCopyOfHalfOfArray, TDataStructure arrayToSort, int index, int halfLen, int length, TComparer comparer)
+        {
+            TDataStructureAccessor accessor = default(TDataStructureAccessor);
+            int leftHalfIndex = 0;
+            int rightHalfIndex = index + halfLen;
+            int rightHalfEnd = index + length;
+            for (int i = 0; i < length; i++)
+            {
+                if (leftHalfIndex == halfLen)
+                {
+                    // All of the remaining elements must be from the right half, and thus must already be in position
+                    break;
+                }
+                if (rightHalfIndex == rightHalfEnd)
+                {
+                    // Copy remaining elements from the local copy
+                    accessor.Copy(localCopyOfHalfOfArray, leftHalfIndex, arrayToSort, index + i, length - i);
+                    break;
+                }
+
+                int comparisonResult = comparer.Compare(localCopyOfHalfOfArray[leftHalfIndex], accessor.GetElement(arrayToSort, rightHalfIndex));
+                if (comparisonResult == 0)
+                {
+                    default(TCompareAsEqualAction).CompareAsEqual();
+                }
+                if (comparisonResult <= 0)
+                {
+                    accessor.SetElement(arrayToSort, i + index, localCopyOfHalfOfArray[leftHalfIndex++]);
+                }
+                else
+                {
+                    accessor.SetElement(arrayToSort, i + index, accessor.GetElement(arrayToSort, rightHalfIndex++));
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
index c7bba81..ffd9293 100644 (file)
@@ -311,7 +311,7 @@ namespace ILCompiler.DependencyAnalysisFramework
                 } while (_markStack.Count != 0);
 
                 if (_resultSorter != null)
-                    _markedNodes.Sort(_resultSorter);
+                    _markedNodes.MergeSortAllowDuplicates(_resultSorter);
 
                 _markedNodesFinal = _markedNodes.ToImmutableArray();
                 _markedNodes = null;
index 64c4f94..1b71574 100644 (file)
     <Compile Include="IDependencyNode.cs" />
     <Compile Include="NoLogStrategy.cs" />
     <Compile Include="PerfEventSource.cs" />
+    <Compile Include="..\..\Common\Sorting\ArrayAccessor.cs">
+      <Link>Sorting\ArrayAccessor.cs</Link>
+    </Compile>
+    <Compile Include="..\..\Common\Sorting\ICompareAsEqualAction.cs">
+      <Link>Sorting\ICompareAsEqualAction.cs</Link>
+    </Compile>
+    <Compile Include="..\..\Common\Sorting\ISortableDataStructureAccessor.cs">
+      <Link>Sorting\ISortableDataStructureAccessor.cs</Link>
+    </Compile>
+    <Compile Include="..\..\Common\Sorting\ListAccessor.cs">
+      <Link>Sorting\ListAccessor.cs</Link>
+    </Compile>
+    <Compile Include="..\..\Common\Sorting\MergeSort.cs">
+      <Link>Sorting\MergeSort.cs</Link>
+    </Compile>
+    <Compile Include="..\..\Common\Sorting\MergeSortCore.cs">
+      <Link>Sorting\MergeSortCore.cs</Link>
+    </Compile>
+
   </ItemGroup>
 </Project>
index 4287b78..7cfec07 100644 (file)
@@ -88,7 +88,7 @@ namespace ILCompiler.DependencyAnalysis
             builder.RequireInitialPointerAlignment();
 
             if (_sorter != null)
-                _nestedNodesList.Sort(_sorter);
+                _nestedNodesList.MergeSort(_sorter);
 
             builder.AddSymbol(StartSymbol);
 
index 1a7c9cc..8488c27 100644 (file)
@@ -60,7 +60,7 @@ namespace ILCompiler.DependencyAnalysis.ReadyToRun
                     definedSymbols: new ISymbolDefinitionNode[] { this });
             }
 
-            _methods.Sort(new CompilerComparer());
+            _methods.MergeSort(new CompilerComparer());
             GCRefMapBuilder builder = new GCRefMapBuilder(factory.Target, relocsOnly);
             builder.Builder.RequireInitialAlignment(4);
             builder.Builder.AddSymbol(this);
index f157900..4f70333 100644 (file)
@@ -105,7 +105,7 @@ namespace ILCompiler.DependencyAnalysis.ReadyToRun
 
             // Don't bother sorting if we're not emitting the contents
             if (!relocsOnly)
-                _items.Sort((x, y) => Comparer<int>.Default.Compare((int)x.Id, (int)y.Id));
+                _items.MergeSort((x, y) => Comparer<int>.Default.Compare((int)x.Id, (int)y.Id));
 
             // ReadyToRunHeader.Flags
             builder.EmitInt((int)_flags);
index f22b5af..94718e3 100644 (file)
@@ -112,7 +112,7 @@ namespace ILCompiler.DependencyAnalysis.ReadyToRun
                 }
 
                 List<EcmaMethod> sortedInliners = new List<EcmaMethod>(inlineeWithInliners.Value);
-                sortedInliners.Sort((a, b) =>
+                sortedInliners.MergeSort((a, b) =>
                 {
                     if (a == b)
                         return 0;
index 2d01e86..ccf912f 100644 (file)
@@ -124,7 +124,7 @@ namespace ILCompiler.DependencyAnalysis.ReadyToRun
                 return null;
             }
 
-            fixupCells.Sort(FixupCell.Comparer);
+            fixupCells.MergeSortAllowDuplicates(FixupCell.Comparer);
 
             // Deduplicate fixupCells
             int j = 0;
index d2ad6b3..805d162 100644 (file)
@@ -110,14 +110,14 @@ namespace ILCompiler
 
                     foreach (var perModuleData in perModuleDatas)
                     {
-                        perModuleData.MethodsGenerated.Sort(sortHelper);
-                        perModuleData.GenericMethodsGenerated.Sort(sortHelper);
+                        perModuleData.MethodsGenerated.MergeSort(sortHelper);
+                        perModuleData.GenericMethodsGenerated.MergeSort(sortHelper);
                         _completeSortedMethods.AddRange(perModuleData.MethodsGenerated);
                         _completeSortedMethods.AddRange(perModuleData.GenericMethodsGenerated);
                         _completeSortedGenericMethods.AddRange(perModuleData.GenericMethodsGenerated);
                     }
-                    _completeSortedMethods.Sort(sortHelper);
-                    _completeSortedGenericMethods.Sort(sortHelper);
+                    _completeSortedMethods.MergeSort(sortHelper);
+                    _completeSortedGenericMethods.MergeSort(sortHelper);
                     _sortedMethods = true;
                 }
             }
index cfa1dec..4e6855e 100644 (file)
@@ -705,7 +705,7 @@ namespace ILCompiler.PEWriter
         /// <param name="location">RVA and file location of the .edata section</param>
         private BlobBuilder SerializeExportSection(SectionLocation sectionLocation)
         {
-            _exportSymbols.Sort((es1, es2) => StringComparer.Ordinal.Compare(es1.Name, es2.Name));
+            _exportSymbols.MergeSort((es1, es2) => StringComparer.Ordinal.Compare(es1.Name, es2.Name));
             
             BlobBuilder builder = new BlobBuilder();