Skip to content

Commit

Permalink
Hard code segment constants for common type sizes
Browse files Browse the repository at this point in the history
* Fix calculation of array sizes
* Allow for inlining for common sizes on .NET Framework
* Reduces inline code size on .NET prior to version 8 (where lzcnt constant folding is first implemented)
  • Loading branch information
sharwell committed Apr 21, 2023
1 parent 0f8a8ed commit 807b383
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

#nullable disable

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Microsoft.CodeAnalysis.Collections.Internal;
using Roslyn.Utilities;
using Xunit;
Expand All @@ -12,6 +17,89 @@ namespace Microsoft.CodeAnalysis.UnitTests.Collections
{
public class SegmentedArrayHelperTests
{
[StructLayout(LayoutKind.Sequential, Size = 2)]
private struct Size2 { }

[StructLayout(LayoutKind.Sequential, Size = 4)]
private struct Size4 { }

[StructLayout(LayoutKind.Sequential, Size = 8)]
private struct Size8 { }

[StructLayout(LayoutKind.Sequential, Size = 12)]
private struct Size12 { }

[StructLayout(LayoutKind.Sequential, Size = 16)]
private struct Size16 { }

[StructLayout(LayoutKind.Sequential, Size = 24)]
private struct Size24 { }

[StructLayout(LayoutKind.Sequential, Size = 28)]
private struct Size28 { }

[StructLayout(LayoutKind.Sequential, Size = 32)]
private struct Size32 { }

[StructLayout(LayoutKind.Sequential, Size = 40)]
private struct Size40 { }

public static IEnumerable<object[]> ExplicitSizeTypes
{
get
{
yield return new object[] { typeof(Size2) };
yield return new object[] { typeof(Size4) };
yield return new object[] { typeof(Size8) };
yield return new object[] { typeof(Size12) };
yield return new object[] { typeof(Size16) };
yield return new object[] { typeof(Size24) };
yield return new object[] { typeof(Size28) };
yield return new object[] { typeof(Size32) };
yield return new object[] { typeof(Size40) };
}
}

[Theory]
[MemberData(nameof(ExplicitSizeTypes))]
public void ExplicitSizesAreCorrect(Type type)
{
var unsafeSizeOfMethod = typeof(Unsafe).GetMethod(nameof(Unsafe.SizeOf)).MakeGenericMethod(type);
var size = (int)unsafeSizeOfMethod.Invoke(null, null);
Assert.Equal(int.Parse(type.Name[4..]), size);
}

[Theory]
[MemberData(nameof(ExplicitSizeTypes))]
public void GetSegmentSize(Type type)
{
var getSegmentSizeMethod = typeof(SegmentedArrayHelper).GetMethod(nameof(SegmentedArrayHelper.GetSegmentSize), BindingFlags.Static | BindingFlags.NonPublic).MakeGenericMethod(type);
var unsafeSizeOfMethod = typeof(Unsafe).GetMethod(nameof(Unsafe.SizeOf)).MakeGenericMethod(type);
Assert.Equal(SegmentedArrayHelper.TestAccessor.CalculateSegmentSize((int)unsafeSizeOfMethod.Invoke(null, null)), (int)getSegmentSizeMethod.Invoke(null, null));
}

[Theory]
[MemberData(nameof(ExplicitSizeTypes))]
public void GetSegmentShift(Type type)
{
var getSegmentSizeMethod = typeof(SegmentedArrayHelper).GetMethod(nameof(SegmentedArrayHelper.GetSegmentSize), BindingFlags.Static | BindingFlags.NonPublic).MakeGenericMethod(type);
var getSegmentShiftMethod = typeof(SegmentedArrayHelper).GetMethod(nameof(SegmentedArrayHelper.GetSegmentShift), BindingFlags.Static | BindingFlags.NonPublic).MakeGenericMethod(type);
var unsafeSizeOfMethod = typeof(Unsafe).GetMethod(nameof(Unsafe.SizeOf)).MakeGenericMethod(type);
var segmentSize = SegmentedArrayHelper.TestAccessor.CalculateSegmentSize((int)unsafeSizeOfMethod.Invoke(null, null));
Assert.Equal(SegmentedArrayHelper.TestAccessor.CalculateSegmentShift(segmentSize), (int)getSegmentShiftMethod.Invoke(null, null));
}

[Theory]
[MemberData(nameof(ExplicitSizeTypes))]
public void GetOffsetMask(Type type)
{
var getSegmentSizeMethod = typeof(SegmentedArrayHelper).GetMethod(nameof(SegmentedArrayHelper.GetSegmentSize), BindingFlags.Static | BindingFlags.NonPublic).MakeGenericMethod(type);
var getOffsetMaskMethod = typeof(SegmentedArrayHelper).GetMethod(nameof(SegmentedArrayHelper.GetOffsetMask), BindingFlags.Static | BindingFlags.NonPublic).MakeGenericMethod(type);
var unsafeSizeOfMethod = typeof(Unsafe).GetMethod(nameof(Unsafe.SizeOf)).MakeGenericMethod(type);
var segmentSize = SegmentedArrayHelper.TestAccessor.CalculateSegmentSize((int)unsafeSizeOfMethod.Invoke(null, null));
Assert.Equal(SegmentedArrayHelper.TestAccessor.CalculateOffsetMask(segmentSize), (int)getOffsetMaskMethod.Invoke(null, null));
}

[Theory]
[InlineData(1)]
[InlineData(2)]
Expand Down
93 changes: 53 additions & 40 deletions src/Dependencies/Collections/Internal/SegmentedArrayHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,52 +19,70 @@ internal static class SegmentedArrayHelper
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int GetSegmentSize<T>()
{
return Unsafe.SizeOf<T>() switch
{
// Hard code common values since not all versions of .NET support constant-folding the calculation.
// Values are validated against the reference implementation in CalculateSegmentSize in unit tests.
4 => 16384,
8 => 8192,
12 => 4096,
16 => 4096,
24 => 2048,
28 => 2048,
32 => 2048,
40 => 2048,
#if NETCOREAPP3_0_OR_NEWER
return InlineCalculateSegmentSize(Unsafe.SizeOf<T>());
_ => InlineCalculateSegmentSize(Unsafe.SizeOf<T>()),
#else
if (Unsafe.SizeOf<T>() == Unsafe.SizeOf<object>())
{
return ReferenceTypeSegmentHelper.SegmentSize;
}
else
{
return ValueTypeSegmentHelper<T>.SegmentSize;
}
_ => ValueTypeSegmentHelper<T>.SegmentSize,
#endif
};
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int GetSegmentShift<T>()
{
return Unsafe.SizeOf<T>() switch
{
// Hard code common values since not all versions of .NET support constant-folding the calculation.
// Values are validated against the reference implementation in CalculateSegmentShift in unit tests.
4 => 14,
8 => 13,
12 => 12,
16 => 12,
24 => 11,
28 => 11,
32 => 11,
40 => 11,
#if NETCOREAPP3_0_OR_NEWER
return InlineCalculateSegmentShift(Unsafe.SizeOf<T>());
_ => InlineCalculateSegmentShift(Unsafe.SizeOf<T>()),
#else
if (Unsafe.SizeOf<T>() == Unsafe.SizeOf<object>())
{
return ReferenceTypeSegmentHelper.SegmentShift;
}
else
{
return ValueTypeSegmentHelper<T>.SegmentShift;
}
_ => ValueTypeSegmentHelper<T>.SegmentShift,
#endif
};
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int GetOffsetMask<T>()
{
return Unsafe.SizeOf<T>() switch
{
// Hard code common values since not all versions of .NET support constant-folding the calculation.
// Values are validated against the reference implementation in CalculateOffsetMask in unit tests.
4 => 16383,
8 => 8191,
12 => 4095,
16 => 4095,
24 => 2047,
28 => 2047,
32 => 2047,
40 => 2047,
#if NETCOREAPP3_0_OR_NEWER
return InlineCalculateOffsetMask(Unsafe.SizeOf<T>());
_ => InlineCalculateOffsetMask(Unsafe.SizeOf<T>()),
#else
if (Unsafe.SizeOf<T>() == Unsafe.SizeOf<object>())
{
return ReferenceTypeSegmentHelper.OffsetMask;
}
else
{
return ValueTypeSegmentHelper<T>.OffsetMask;
}
_ => ValueTypeSegmentHelper<T>.OffsetMask,
#endif
};
}

/// <summary>
Expand Down Expand Up @@ -94,7 +112,7 @@ private static int CalculateSegmentSize(int elementSize)
static int ArraySize(int elementSize, int segmentSize)
{
// Array object header, plus space for the elements
return (2 * IntPtr.Size) + (elementSize * segmentSize);
return (2 * IntPtr.Size + 8) + (elementSize * segmentSize);
}
}

Expand Down Expand Up @@ -141,7 +159,7 @@ private static int InlineCalculateSegmentShift(int elementSize)
// Default Large Object Heap size threshold
// https://github.com/dotnet/runtime/blob/c9d69e38d0e54bea5d188593ef6c3b30139f3ab1/src/coreclr/src/gc/gc.h#L111
const uint Threshold = 85000;
return System.Numerics.BitOperations.Log2((uint)((Threshold / elementSize) - (2 * Unsafe.SizeOf<object>())));
return System.Numerics.BitOperations.Log2((uint)((Threshold / elementSize) - (2 * Unsafe.SizeOf<object>() + 8)));
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand All @@ -156,25 +174,20 @@ internal static class TestAccessor
public static int CalculateSegmentSize(int elementSize)
=> SegmentedArrayHelper.CalculateSegmentSize(elementSize);

public static int CalculateSegmentShift(int elementSize)
=> SegmentedArrayHelper.CalculateSegmentShift(elementSize);

public static int CalculateOffsetMask(int elementSize)
=> SegmentedArrayHelper.CalculateOffsetMask(elementSize);
}
public static int CalculateSegmentShift(int segmentSize)
=> SegmentedArrayHelper.CalculateSegmentShift(segmentSize);

private static class ReferenceTypeSegmentHelper
{
public static readonly int SegmentSize = CalculateSegmentSize(Unsafe.SizeOf<object>());
public static readonly int SegmentShift = CalculateSegmentShift(SegmentSize);
public static readonly int OffsetMask = CalculateOffsetMask(SegmentSize);
public static int CalculateOffsetMask(int segmentSize)
=> SegmentedArrayHelper.CalculateOffsetMask(segmentSize);
}

#if !NETCOREAPP3_0_OR_NEWER
private static class ValueTypeSegmentHelper<T>
{
public static readonly int SegmentSize = CalculateSegmentSize(Unsafe.SizeOf<T>());
public static readonly int SegmentShift = CalculateSegmentShift(SegmentSize);
public static readonly int OffsetMask = CalculateOffsetMask(SegmentSize);
}
#endif
}
}

0 comments on commit 807b383

Please sign in to comment.