From 5fea8f064334a7972ca218bf1823e2477a192afd Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Tue, 5 Nov 2024 13:14:01 -0800 Subject: [PATCH 1/4] Modify segmented list to grow by growth rate. A growth rate of 2x matches the current code behavior. A growth rate of just over 1 matches the growth rate (1 segment) in the other PR (https://github.com/dotnet/roslyn/pull/75708). All growth rates benchmarked: 1.000001, 1.1, 1.25, 1.5, 2 Obviously, the single segment growth rate is a non-starter without allowing null segments (which is the approach the other PR took). The 2x rate matches current behavior, and is really only measured as a baseline. From my reading of this chart, it looks like 1.1 is the best of these choices. --- .../Collections/SegmentedList`1.cs | 36 +++++++-- .../SegmentedListBenchmarks_Add.cs | 25 +++--- ...egmentedListBenchmarks_Add_SegmentCount.cs | 76 +++++++++++++++++++ 3 files changed, 116 insertions(+), 21 deletions(-) create mode 100644 src/Tools/IdeCoreBenchmarks/SegmentedListBenchmarks_Add_SegmentCount.cs diff --git a/src/Dependencies/Collections/SegmentedList`1.cs b/src/Dependencies/Collections/SegmentedList`1.cs index 5a7ffb920292..4a34c683f356 100644 --- a/src/Dependencies/Collections/SegmentedList`1.cs +++ b/src/Dependencies/Collections/SegmentedList`1.cs @@ -42,6 +42,8 @@ internal class SegmentedList : IList, IList, IReadOnlyList private static readonly SegmentedArray s_emptyArray = new(0); private static IEnumerator? s_emptyEnumerator; + public static double SegmentGrowthRate { get; set; } = 2.0; + // Constructs a SegmentedList. The list is initially empty and has a capacity // of zero. Upon adding the first element to the list the capacity is // increased to DefaultCapacity, and then increased in multiples of two @@ -512,12 +514,36 @@ internal void Grow(int capacity) { Debug.Assert(_items.Length < capacity); - var newCapacity = _items.Length == 0 ? DefaultCapacity : 2 * _items.Length; + int newCapacity; - // Allow the list to grow to maximum possible capacity (~2G elements) before encountering overflow. - // Note that this check works even when _items.Length overflowed thanks to the (uint) cast - if ((uint)newCapacity > MaxLength) - newCapacity = MaxLength; + if (_items.Length < SegmentedArrayHelper.GetSegmentSize() / 2) + { + // The array isn't near the maximum segment size. If the array is empty, the new capacity + // should be DefaultCapacity. Otherwise, the new capacity should be double the current array size. + newCapacity = _items.Length == 0 ? DefaultCapacity : _items.Length * 2; + } + else + { + var lastSegmentLength = _items.Length & SegmentedArrayHelper.GetOffsetMask(); + if (lastSegmentLength > 0) + { + // The last segment isn't fully sized, increase the new capacity such that it will be. + newCapacity = (_items.Length - lastSegmentLength) + SegmentedArrayHelper.GetSegmentSize(); + } + else + { + // The last segment is fully sized, increase the number of segments by the desired growth factor + var oldSegmentCount = (_items.Length + SegmentedArrayHelper.GetSegmentSize() - 1) >> SegmentedArrayHelper.GetSegmentShift(); + var newSegmentCount = (int)Math.Ceiling(oldSegmentCount * SegmentGrowthRate); + + newCapacity = SegmentedArrayHelper.GetSegmentSize() * newSegmentCount; + } + + // Allow the list to grow to maximum possible capacity (~2G elements) before encountering overflow. + // Note that this check works even when _items.Length overflowed thanks to the (uint) cast + if ((uint)newCapacity > MaxLength) + newCapacity = MaxLength; + } // If the computed capacity is still less than specified, set to the original argument. // Capacities exceeding Array.MaxLength will be surfaced as OutOfMemoryException by Array.Resize. diff --git a/src/Tools/IdeCoreBenchmarks/SegmentedListBenchmarks_Add.cs b/src/Tools/IdeCoreBenchmarks/SegmentedListBenchmarks_Add.cs index ff9629c2302a..5c5d77bbfd22 100644 --- a/src/Tools/IdeCoreBenchmarks/SegmentedListBenchmarks_Add.cs +++ b/src/Tools/IdeCoreBenchmarks/SegmentedListBenchmarks_Add.cs @@ -38,28 +38,21 @@ private void AddToList(T item) array.Add(item); } - private struct LargeStruct + private struct MediumStruct { public int i1 { get; set; } public int i2 { get; set; } public int i3 { get; set; } public int i4 { get; set; } public int i5 { get; set; } - public int i6 { get; set; } - public int i7 { get; set; } - public int i8 { get; set; } - public int i9 { get; set; } - public int i10 { get; set; } - public int i11 { get; set; } - public int i12 { get; set; } - public int i13 { get; set; } - public int i14 { get; set; } - public int i15 { get; set; } - public int i16 { get; set; } - public int i17 { get; set; } - public int i18 { get; set; } - public int i19 { get; set; } - public int i20 { get; set; } + } + + private struct LargeStruct + { + public MediumStruct s1 { get; set; } + public MediumStruct s2 { get; set; } + public MediumStruct s3 { get; set; } + public MediumStruct s4 { get; set; } } private struct EnormousStruct diff --git a/src/Tools/IdeCoreBenchmarks/SegmentedListBenchmarks_Add_SegmentCount.cs b/src/Tools/IdeCoreBenchmarks/SegmentedListBenchmarks_Add_SegmentCount.cs new file mode 100644 index 000000000000..6cfb5e761c13 --- /dev/null +++ b/src/Tools/IdeCoreBenchmarks/SegmentedListBenchmarks_Add_SegmentCount.cs @@ -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 BenchmarkDotNet.Attributes; +using Microsoft.CodeAnalysis.Collections; +using Microsoft.CodeAnalysis.Collections.Internal; + +[MemoryDiagnoser] +public class SegmentedListBenchmarks_Add_SegmentCounts +{ + [Params(16, 256, 4096, 65536)] + public int SegmentCount { get; set; } + + [ParamsAllValues] + public bool AddExtraItem { get; set; } + + [Params(1.000001, 1.1, 1.25, 1.5, 2)] + public double SegmentGrowthRate { get; set; } + + [Benchmark] + public void AddObjectToList() + => AddToList(new object()); + + [Benchmark] + public void AddLargeStructToList() + => AddToList(new LargeStruct()); + + [Benchmark] + public void AddEnormousStructToList() + => AddToList(new EnormousStruct()); + + private void AddToList(T item) + { + SegmentedList.SegmentGrowthRate = SegmentGrowthRate; + + var count = SegmentCount * SegmentedArrayHelper.GetSegmentSize(); + if (AddExtraItem) + count++; + + var array = new SegmentedList(); + for (var i = 0; i < count; i++) + array.Add(item); + } + + private struct MediumStruct + { + public int i1 { get; set; } + public int i2 { get; set; } + public int i3 { get; set; } + public int i4 { get; set; } + public int i5 { get; set; } + } + + private struct LargeStruct + { + public MediumStruct s1 { get; set; } + public MediumStruct s2 { get; set; } + public MediumStruct s3 { get; set; } + public MediumStruct s4 { get; set; } + } + + private struct EnormousStruct + { + public LargeStruct s1 { get; set; } + public LargeStruct s2 { get; set; } + public LargeStruct s3 { get; set; } + public LargeStruct s4 { get; set; } + public LargeStruct s5 { get; set; } + public LargeStruct s6 { get; set; } + public LargeStruct s7 { get; set; } + public LargeStruct s8 { get; set; } + public LargeStruct s9 { get; set; } + public LargeStruct s10 { get; set; } + } +} From 60c89f5aca568bb7e525171204a2ad6e2aac8aef Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Tue, 5 Nov 2024 15:31:05 -0800 Subject: [PATCH 2/4] Modify benchmark to just run against 1.125 and 1.25 and change the semantics to indicate the shift amount, not the growth rate --- .../Collections/SegmentedList`1.cs | 6 +-- ...egmentedListBenchmarks_Add_SegmentCount.cs | 37 ++++++++++++++++--- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/Dependencies/Collections/SegmentedList`1.cs b/src/Dependencies/Collections/SegmentedList`1.cs index 4a34c683f356..ef31990b693f 100644 --- a/src/Dependencies/Collections/SegmentedList`1.cs +++ b/src/Dependencies/Collections/SegmentedList`1.cs @@ -42,7 +42,7 @@ internal class SegmentedList : IList, IList, IReadOnlyList private static readonly SegmentedArray s_emptyArray = new(0); private static IEnumerator? s_emptyEnumerator; - public static double SegmentGrowthRate { get; set; } = 2.0; + public static int SegmentGrowthShiftValue { get; set; } = 3; // Constructs a SegmentedList. The list is initially empty and has a capacity // of zero. Upon adding the first element to the list the capacity is @@ -532,9 +532,9 @@ internal void Grow(int capacity) } else { - // The last segment is fully sized, increase the number of segments by the desired growth factor + // The last segment is fully sized, increase the number of segments by at least the desired growth factor (1.125) var oldSegmentCount = (_items.Length + SegmentedArrayHelper.GetSegmentSize() - 1) >> SegmentedArrayHelper.GetSegmentShift(); - var newSegmentCount = (int)Math.Ceiling(oldSegmentCount * SegmentGrowthRate); + var newSegmentCount = oldSegmentCount + Math.Max(1, oldSegmentCount >> SegmentGrowthShiftValue); newCapacity = SegmentedArrayHelper.GetSegmentSize() * newSegmentCount; } diff --git a/src/Tools/IdeCoreBenchmarks/SegmentedListBenchmarks_Add_SegmentCount.cs b/src/Tools/IdeCoreBenchmarks/SegmentedListBenchmarks_Add_SegmentCount.cs index 6cfb5e761c13..e81dee9e3cbf 100644 --- a/src/Tools/IdeCoreBenchmarks/SegmentedListBenchmarks_Add_SegmentCount.cs +++ b/src/Tools/IdeCoreBenchmarks/SegmentedListBenchmarks_Add_SegmentCount.cs @@ -10,13 +10,40 @@ public class SegmentedListBenchmarks_Add_SegmentCounts { [Params(16, 256, 4096, 65536)] - public int SegmentCount { get; set; } + public int RoughSegmentCount { get; set; } [ParamsAllValues] public bool AddExtraItem { get; set; } - [Params(1.000001, 1.1, 1.25, 1.5, 2)] - public double SegmentGrowthRate { get; set; } + [Params(2, 3)] + public int SegmentGrowthShiftValue { get; set; } + + private int _actualSegmentCount; + + [IterationSetup] + public void IterationSetup() + { + if (SegmentGrowthShiftValue == 2) + { + _actualSegmentCount = RoughSegmentCount switch + { + 16 => 18, + 256 => 293, + 4096 => 4241, + 65536 => 61697 + }; + } + else if (SegmentGrowthShiftValue == 3) + { + _actualSegmentCount = RoughSegmentCount switch + { + 16 => 18, + 256 => 289, + 4096 => 4282, + 65536 => 64247 + }; + } + } [Benchmark] public void AddObjectToList() @@ -32,9 +59,9 @@ public void AddEnormousStructToList() private void AddToList(T item) { - SegmentedList.SegmentGrowthRate = SegmentGrowthRate; + SegmentedList.SegmentGrowthShiftValue = SegmentGrowthShiftValue; - var count = SegmentCount * SegmentedArrayHelper.GetSegmentSize(); + var count = _actualSegmentCount * SegmentedArrayHelper.GetSegmentSize(); if (AddExtraItem) count++; From 46275e7e93f033c35acd6d77d3c5185a928f1d1f Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Thu, 7 Nov 2024 07:36:58 -0800 Subject: [PATCH 3/4] Cleanup code needed for benchmark. Move code to ensure last segment is properly size to execute in other cases. --- .../Collections/SegmentedList`1.cs | 39 ++++--- ...egmentedListBenchmarks_Add_SegmentCount.cs | 103 ------------------ 2 files changed, 22 insertions(+), 120 deletions(-) delete mode 100644 src/Tools/IdeCoreBenchmarks/SegmentedListBenchmarks_Add_SegmentCount.cs diff --git a/src/Dependencies/Collections/SegmentedList`1.cs b/src/Dependencies/Collections/SegmentedList`1.cs index ef31990b693f..f00c76a6897d 100644 --- a/src/Dependencies/Collections/SegmentedList`1.cs +++ b/src/Dependencies/Collections/SegmentedList`1.cs @@ -42,8 +42,6 @@ internal class SegmentedList : IList, IList, IReadOnlyList private static readonly SegmentedArray s_emptyArray = new(0); private static IEnumerator? s_emptyEnumerator; - public static int SegmentGrowthShiftValue { get; set; } = 3; - // Constructs a SegmentedList. The list is initially empty and has a capacity // of zero. Upon adding the first element to the list the capacity is // increased to DefaultCapacity, and then increased in multiples of two @@ -514,7 +512,7 @@ internal void Grow(int capacity) { Debug.Assert(_items.Length < capacity); - int newCapacity; + var newCapacity = 0; if (_items.Length < SegmentedArrayHelper.GetSegmentSize() / 2) { @@ -524,20 +522,32 @@ internal void Grow(int capacity) } else { - var lastSegmentLength = _items.Length & SegmentedArrayHelper.GetOffsetMask(); - if (lastSegmentLength > 0) - { - // The last segment isn't fully sized, increase the new capacity such that it will be. - newCapacity = (_items.Length - lastSegmentLength) + SegmentedArrayHelper.GetSegmentSize(); - } - else + // If the last segment is fully sized, increase the number of segments by the desired growth rate + if (0 == (_items.Length & SegmentedArrayHelper.GetOffsetMask())) { - // The last segment is fully sized, increase the number of segments by at least the desired growth factor (1.125) + // This value determines the growth rate of the number of segments to use. + // For a value of 3, this means the segment count will grow at a rate of + // 1 + (1 >> 3) or 12.5% + const int segmentGrowthShiftValue = 3; + var oldSegmentCount = (_items.Length + SegmentedArrayHelper.GetSegmentSize() - 1) >> SegmentedArrayHelper.GetSegmentShift(); - var newSegmentCount = oldSegmentCount + Math.Max(1, oldSegmentCount >> SegmentGrowthShiftValue); + var newSegmentCount = oldSegmentCount + Math.Max(1, oldSegmentCount >> segmentGrowthShiftValue); newCapacity = SegmentedArrayHelper.GetSegmentSize() * newSegmentCount; } + } + + // If the computed capacity is less than specified, set to the original argument. + // Capacities exceeding Array.MaxLength will be surfaced as OutOfMemoryException by Array.Resize. + if (newCapacity < capacity) + newCapacity = capacity; + + if (newCapacity > SegmentedArrayHelper.GetSegmentSize()) + { + // If the last segment isn't fully sized, increase the new capacity such that it will be. + var lastSegmentLength = newCapacity & SegmentedArrayHelper.GetOffsetMask(); + if (lastSegmentLength > 0) + newCapacity = (newCapacity - lastSegmentLength) + SegmentedArrayHelper.GetSegmentSize(); // Allow the list to grow to maximum possible capacity (~2G elements) before encountering overflow. // Note that this check works even when _items.Length overflowed thanks to the (uint) cast @@ -545,11 +555,6 @@ internal void Grow(int capacity) newCapacity = MaxLength; } - // If the computed capacity is still less than specified, set to the original argument. - // Capacities exceeding Array.MaxLength will be surfaced as OutOfMemoryException by Array.Resize. - if (newCapacity < capacity) - newCapacity = capacity; - Capacity = newCapacity; } diff --git a/src/Tools/IdeCoreBenchmarks/SegmentedListBenchmarks_Add_SegmentCount.cs b/src/Tools/IdeCoreBenchmarks/SegmentedListBenchmarks_Add_SegmentCount.cs deleted file mode 100644 index e81dee9e3cbf..000000000000 --- a/src/Tools/IdeCoreBenchmarks/SegmentedListBenchmarks_Add_SegmentCount.cs +++ /dev/null @@ -1,103 +0,0 @@ -// 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 BenchmarkDotNet.Attributes; -using Microsoft.CodeAnalysis.Collections; -using Microsoft.CodeAnalysis.Collections.Internal; - -[MemoryDiagnoser] -public class SegmentedListBenchmarks_Add_SegmentCounts -{ - [Params(16, 256, 4096, 65536)] - public int RoughSegmentCount { get; set; } - - [ParamsAllValues] - public bool AddExtraItem { get; set; } - - [Params(2, 3)] - public int SegmentGrowthShiftValue { get; set; } - - private int _actualSegmentCount; - - [IterationSetup] - public void IterationSetup() - { - if (SegmentGrowthShiftValue == 2) - { - _actualSegmentCount = RoughSegmentCount switch - { - 16 => 18, - 256 => 293, - 4096 => 4241, - 65536 => 61697 - }; - } - else if (SegmentGrowthShiftValue == 3) - { - _actualSegmentCount = RoughSegmentCount switch - { - 16 => 18, - 256 => 289, - 4096 => 4282, - 65536 => 64247 - }; - } - } - - [Benchmark] - public void AddObjectToList() - => AddToList(new object()); - - [Benchmark] - public void AddLargeStructToList() - => AddToList(new LargeStruct()); - - [Benchmark] - public void AddEnormousStructToList() - => AddToList(new EnormousStruct()); - - private void AddToList(T item) - { - SegmentedList.SegmentGrowthShiftValue = SegmentGrowthShiftValue; - - var count = _actualSegmentCount * SegmentedArrayHelper.GetSegmentSize(); - if (AddExtraItem) - count++; - - var array = new SegmentedList(); - for (var i = 0; i < count; i++) - array.Add(item); - } - - private struct MediumStruct - { - public int i1 { get; set; } - public int i2 { get; set; } - public int i3 { get; set; } - public int i4 { get; set; } - public int i5 { get; set; } - } - - private struct LargeStruct - { - public MediumStruct s1 { get; set; } - public MediumStruct s2 { get; set; } - public MediumStruct s3 { get; set; } - public MediumStruct s4 { get; set; } - } - - private struct EnormousStruct - { - public LargeStruct s1 { get; set; } - public LargeStruct s2 { get; set; } - public LargeStruct s3 { get; set; } - public LargeStruct s4 { get; set; } - public LargeStruct s5 { get; set; } - public LargeStruct s6 { get; set; } - public LargeStruct s7 { get; set; } - public LargeStruct s8 { get; set; } - public LargeStruct s9 { get; set; } - public LargeStruct s10 { get; set; } - } -} From ae26734a01b29aba39ba081d4fe45879217ab977 Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Thu, 7 Nov 2024 07:46:53 -0800 Subject: [PATCH 4/4] Fix unit test after behavior change where we changed the last segment size --- .../List/SegmentedList.Generic.Tests.Capacity.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Compilers/Core/CodeAnalysisTest/Collections/List/SegmentedList.Generic.Tests.Capacity.cs b/src/Compilers/Core/CodeAnalysisTest/Collections/List/SegmentedList.Generic.Tests.Capacity.cs index 1eb3fad3c808..ec2347a570b8 100644 --- a/src/Compilers/Core/CodeAnalysisTest/Collections/List/SegmentedList.Generic.Tests.Capacity.cs +++ b/src/Compilers/Core/CodeAnalysisTest/Collections/List/SegmentedList.Generic.Tests.Capacity.cs @@ -134,7 +134,11 @@ public void EnsureCapacity_MatchesSizeWithLargeCapacityRequest(int segmentCount) var requestedCapacity = 2 * elementCount + 10; list.EnsureCapacity(requestedCapacity); - Assert.Equal(requestedCapacity, list.Capacity); + + var lastSegmentLength = requestedCapacity % SegmentedArray.TestAccessor.SegmentSize; + var expectedCapacity = (requestedCapacity - lastSegmentLength) + SegmentedArray.TestAccessor.SegmentSize; + + Assert.Equal(expectedCapacity, list.Capacity); } } }