はじめに
以前の記事 UnsafeAccessor でジェネリックが使えなかった話 の続きです。
.NET Runtime in .NET 9 Preview 4- Release Notes によると、.NET 9 のプレビュー 4 で UnsafeAccessor
がジェネリックに対応したようです。
↓ のコードが有効になりました。
static class ListUnsafeAccessor<T>
{
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_items")]
static extern ref T[] _items(List<T> list);
internal static Memory<T> AsMemory(List<T>? list) => list is null ? default : _items(list).AsMemory(0, list.Count);
}
// 使用法
List<int> list = [1, 2, 3];
Assert.Equal(list.ToArray(), ListUnsafeAccessor<int>.AsMemory(list));
Assert.Equal([], ListUnsafeAccessor<int>.AsMemory(null).Span);
かゆいところ
フレームワーク機能で List<T>
から Span<T>
は取れるため概ね満足だったのですが、やはり配列や Memory<T>
を取れると便利だなと思うこともありました。既存の方法だと式木(System.Linq.Expressions
)を使うのがバランスがいいかと思っていたのですが、これは要するにリフレクションのため実行時のコストがあるのと、AoT と相性がよくない問題がありました。今回、それが解消しました。
パフォーマンス比較
結論:どれも変わらん
Test | Score | % | CG0 |
---|---|---|---|
Performance_AsSpan_CollectionsMarshal | 100,225 | 100.0% | 0 |
Performance_List_foreach | 90,026 | 89.8% | 0 |
Performance_AsMemory_UnsafeAccessor | 94,111 | 93.9% | 0 |
Performance_AsMemory_ReflectionAccessor | 85,934 | 85.7% | 0 |
Performance_AsMemory_ExpressionAccessor | 86,283 | 86.1% | 0 |
実行環境: Windows11 x64 .NET Runtime 9.0.0
Score は高いほどパフォーマンスがよいです。
GC0 はガベージコレクション回数を表します(少ないほどパフォーマンスがよい)。
テストコード
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using Xunit;
public class _UnsafeAccessorGenericTest
{
static class ListUnsafeAccessor<T>
{
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_items")]
static extern ref T[] _items(List<T> list);
internal static Memory<T> AsMemory(List<T>? list) => list is null ? default : _items(list).AsMemory(0, list.Count);
}
void ListUnsafeAccessorの使用法()
{
List<int> list = [1, 2, 3];
Assert.Equal(list.ToArray(), ListUnsafeAccessor<int>.AsMemory(list));
Assert.Equal([], ListUnsafeAccessor<int>.AsMemory(null).Span);
}
static class ListReflectionAccessor<T>
{
private static readonly FieldInfo _itemsField = typeof(List<T>).GetField("_items", BindingFlags.NonPublic | BindingFlags.Instance)!;
private static T[] _items(List<T> list) => (T[])_itemsField.GetValue(list)!;
internal static Memory<T> AsMemory(List<T>? list) => list is null ? default : _items(list).AsMemory(0, list.Count);
}
static class ListExpressionAccessor<T>
{
private static readonly Func<List<T>, T[]> _itemsFunc;
static ListExpressionAccessor()
{
var param = Expression.Parameter(typeof(List<T>), "list");
var field = Expression.Field(param, typeof(List<T>).GetField("_items", BindingFlags.NonPublic | BindingFlags.Instance)!);
_itemsFunc = Expression.Lambda<Func<List<T>, T[]>>(field, param).Compile();
}
private static T[] _items(List<T> list) => _itemsFunc(list);
internal static Memory<T> AsMemory(List<T>? list) => list is null ? default : _items(list).AsMemory(0, list.Count);
}
[Fact]
void AsMemory_UnsafeAccessor()
{
List<int> list = [1, 2, 3];
Assert.Equal(list.ToArray(), ListUnsafeAccessor<int>.AsMemory(list));
Assert.Equal([], ListUnsafeAccessor<int>.AsMemory(null).Span);
}
[Fact]
void AsMemory_ReflectionAccessor()
{
List<int> list = [1, 2, 3];
Assert.Equal(list.ToArray(), ListReflectionAccessor<int>.AsMemory(list));
Assert.Equal([], ListReflectionAccessor<int>.AsMemory(null).Span);
}
[Fact]
void AsMemory_ExpressionAccessor()
{
List<int> list = [1, 2, 3];
Assert.Equal(list.ToArray(), ListExpressionAccessor<int>.AsMemory(list));
Assert.Equal([], ListExpressionAccessor<int>.AsMemory(null).Span);
}
static List<int> CreateTestList()
{
var list = new List<int>(10);
for (int i = 0; i < 10; i++)
list.Add(i);
return list;
}
static Action Performance_AsSpan_CollectionsMarshal()
{
var list = CreateTestList();
return () =>
{
var span = System.Runtime.InteropServices.CollectionsMarshal.AsSpan(list);
foreach (var n in span)
n.GetHashCode();
};
}
static Action Performance_List_foreach()
{
var list = CreateTestList();
return () =>
{
foreach (var n in list)
n.GetHashCode();
};
}
static Action Performance_AsMemory_UnsafeAccessor()
{
var list = CreateTestList();
return () =>
{
var span = ListUnsafeAccessor<int>.AsMemory(list).Span;
foreach (var n in span)
n.GetHashCode();
};
}
static Action Performance_AsMemory_ReflectionAccessor()
{
var list = CreateTestList();
return () =>
{
var span = ListReflectionAccessor<int>.AsMemory(list).Span;
foreach (var n in span)
n.GetHashCode();
};
}
static Action Performance_AsMemory_ExpressionAccessor()
{
var list = CreateTestList();
return () =>
{
var span = ListExpressionAccessor<int>.AsMemory(list).Span;
foreach (var n in span)
n.GetHashCode();
};
}
}