2
1

UnsafeAccessor でジェネリックが使えるようになった話

Posted at

はじめに

以前の記事 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();
            };
    }
}

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1