Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modify Capacity growth rate to be linear instead of doubling on resize. #75708

Closed
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Collections.Internal;
using Xunit;

namespace Microsoft.CodeAnalysis.UnitTests.Collections
Expand All @@ -24,9 +25,9 @@ public static IEnumerable<object[]> TestLengthsAndSegmentCounts
yield return new object[] { 1, segmentsToAdd };
yield return new object[] { 10, segmentsToAdd };
yield return new object[] { 100, segmentsToAdd };
yield return new object[] { SegmentedArray<object>.TestAccessor.SegmentSize / 2, segmentsToAdd };
yield return new object[] { SegmentedArray<object>.TestAccessor.SegmentSize, segmentsToAdd };
yield return new object[] { SegmentedArray<object>.TestAccessor.SegmentSize * 2, segmentsToAdd };
yield return new object[] { SegmentedArray<T>.TestAccessor.SegmentSize / 2, segmentsToAdd };
yield return new object[] { SegmentedArray<T>.TestAccessor.SegmentSize, segmentsToAdd };
yield return new object[] { SegmentedArray<T>.TestAccessor.SegmentSize * 2, segmentsToAdd };
yield return new object[] { 100000, segmentsToAdd };
}
}
Expand Down Expand Up @@ -63,25 +64,26 @@ public void Capacity_MatchesSizeRequested(int initialCapacity, int requestedCapa
[MemberData(nameof(TestLengthsAndSegmentCounts))]
public void Capacity_ReusesSegments(int initialCapacity, int segmentCountToAdd)
{
var elementCountToAdd = segmentCountToAdd * SegmentedArray<object>.TestAccessor.SegmentSize;
var elementCountToAdd = segmentCountToAdd * SegmentedArray<T>.TestAccessor.SegmentSize;

var segmented = new SegmentedList<object>(initialCapacity);
var segmented = new SegmentedList<T>(initialCapacity);

var oldSegments = SegmentedCollectionsMarshal.AsSegments(segmented.GetTestAccessor().Items);
var oldSegmentCount = oldSegments.Length;
var oldSegmentCount = (segmented.Capacity + SegmentedArrayHelper.GetSegmentSize<T>() - 1) >> SegmentedArrayHelper.GetSegmentShift<T>();

segmented.Capacity = initialCapacity + elementCountToAdd;

var resizedSegments = SegmentedCollectionsMarshal.AsSegments(segmented.GetTestAccessor().Items);
var resizedSegmentCount = resizedSegments.Length;

Assert.Equal(oldSegmentCount + segmentCountToAdd, resizedSegmentCount);
var resizedSegmentCount = (segmented.Capacity + SegmentedArrayHelper.GetSegmentSize<T>() - 1) >> SegmentedArrayHelper.GetSegmentShift<T>();

for (var i = 0; i < oldSegmentCount - 1; i++)
Assert.Same(resizedSegments[i], oldSegments[i]);

for (var i = oldSegmentCount - 1; i < resizedSegmentCount - 1; i++)
Assert.Equal(resizedSegments[i].Length, SegmentedArray<object>.TestAccessor.SegmentSize);
Assert.Equal(resizedSegments[i].Length, SegmentedArray<T>.TestAccessor.SegmentSize);

for (var i = resizedSegmentCount; i < resizedSegments.Length; i++)
Assert.Null(resizedSegments[i]);

Assert.NotSame(resizedSegments[resizedSegmentCount - 1], oldSegments[oldSegmentCount - 1]);
Assert.Equal(resizedSegments[resizedSegmentCount - 1].Length, oldSegments[oldSegmentCount - 1].Length);
Expand All @@ -93,7 +95,7 @@ public void Capacity_InOnlySingleSegment(
[CombinatorialValues(1, 2, 10, 100)] int initialCapacity,
[CombinatorialValues(1, 2, 10, 100)] int addItemCount)
{
var segmented = new SegmentedList<object>(initialCapacity);
var segmented = new SegmentedList<T>(initialCapacity);

var oldSegments = SegmentedCollectionsMarshal.AsSegments(segmented.GetTestAccessor().Items);

Expand Down Expand Up @@ -136,5 +138,54 @@ public void EnsureCapacity_MatchesSizeWithLargeCapacityRequest(int segmentCount)
list.EnsureCapacity(requestedCapacity);
Assert.Equal(requestedCapacity, list.Capacity);
}

[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(4)]
public void EnsureCapacity_FullSegmentGrowsBySegment(int segmentCount)
{
var elementCount = segmentCount * SegmentedArray<T>.TestAccessor.SegmentSize;
var list = new SegmentedList<T>(elementCount);

Assert.Equal(elementCount, list.Capacity);

list.EnsureCapacity(elementCount + 1);
Assert.Equal((segmentCount + 1) * SegmentedArray<T>.TestAccessor.SegmentSize, list.Capacity);
}

[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(2)]
[InlineData(4)]
public void EnsureCapacity_HalfSegmentGrowsToMaxSegmentSize(int segmentCount)
{
var elementCount = segmentCount * SegmentedArray<T>.TestAccessor.SegmentSize + SegmentedArray<T>.TestAccessor.SegmentSize / 2;
var list = new SegmentedList<T>(elementCount);

Assert.Equal(elementCount, list.Capacity);

list.EnsureCapacity(elementCount + 1);
Assert.Equal((segmentCount + 1) * SegmentedArray<T>.TestAccessor.SegmentSize, list.Capacity);
}

[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(4)]
public void EnsureCapacity_FullSegmentDoublesUnderlyingArraySize(int segmentCount)
{
var elementCount = segmentCount * SegmentedArray<T>.TestAccessor.SegmentSize;
var list = new SegmentedList<T>(elementCount);
var segments = SegmentedCollectionsMarshal.AsSegments(list.GetTestAccessor().Items);

Assert.Equal(segmentCount, segments.Length);

list.EnsureCapacity(elementCount + 1);
segments = SegmentedCollectionsMarshal.AsSegments(list.GetTestAccessor().Items);

Assert.Equal(2 * segmentCount, segments.Length);
}
}
}
34 changes: 27 additions & 7 deletions src/Dependencies/Collections/SegmentedList`1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,13 @@ public int Capacity
var oldSegmentCount = segments.Length;
var newSegmentCount = (value + SegmentedArrayHelper.GetSegmentSize<T>() - 1) >> SegmentedArrayHelper.GetSegmentShift<T>();

// Grow the array of segments, if necessary
Array.Resize(ref segments, newSegmentCount);
if (newSegmentCount > segments.Length)
{
// Grow the array of segments, if necessary. Note that this array may end up having null entries
// at it's end when this method completes. Minimally doubling the outer array size allows amortized
// linear cost growth properties.
Array.Resize(ref segments, Math.Max(newSegmentCount, segments.Length * 2));
Copy link
Member

@sharwell sharwell Nov 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 I don't like this being on the general Capacity.set path, since it could penalize users who are setting a capacity specifically because they know the exact size of the resulting collection. It would be preferable to have this only occur on the Grow(int) path.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense, let me move this to Grow

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried to move this to Grow and not modify the Capacity setter, but it ended up needing to duplicate pretty much all the logic in the Capacity setter. So, I created a helper method that both places can call into that takes in a flag to specify whether the doubling is desired for the segment array.

}

// Resize all segments to full segment size from the last old segment to the next to last
// new segment.
Expand Down Expand Up @@ -512,12 +517,27 @@ internal void Grow(int capacity)
{
Debug.Assert(_items.Length < capacity);

var newCapacity = _items.Length == 0 ? DefaultCapacity : 2 * _items.Length;
int newCapacity;

if (_items.Length < SegmentedArrayHelper.GetSegmentSize<T>() / 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
{
// Specify a capacity increase such that the last segment's capacity will be the maximum
// segment size. If it is already at that size, this indicates a new segment is to be
// added of the maximum segment size.
var lastSegmentLength = _items.Length & SegmentedArrayHelper.GetOffsetMask<T>();
newCapacity = (_items.Length - lastSegmentLength) + SegmentedArrayHelper.GetSegmentSize<T>();

// 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;
// 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.
Expand Down
25 changes: 9 additions & 16 deletions src/Tools/IdeCoreBenchmarks/SegmentedListBenchmarks_Add.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,28 +38,21 @@ private void AddToList<T>(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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// 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; }

[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>(T item)
{
var count = SegmentCount * SegmentedArrayHelper.GetSegmentSize<T>();
if (AddExtraItem)
count++;

var array = new SegmentedList<T>();
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; }
}
}