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

# はじめに

以前の記事 [UnsafeAccessor でジェネリックが使えなかった話](https://qiita.com/abetakahiro123/items/325db7c06b747ea4b5b0) の続きです。

[.NET Runtime in .NET 9 Preview 4- Release Notes](https://github.com/dotnet/core/blob/main/release-notes/9.0/preview/preview4/runtime.md#unsafeaccessorattribute-supports-generic-parameters) によると、.NET 9 のプレビュー 4 で `UnsafeAccessor` がジェネリックに対応したようです。  
↓ のコードが有効になりました。

```cs
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>` は取れる](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.collectionsmarshal.asspan?view=net-8.0)ため概ね満足だったのですが、やはり配列や `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** はガベージコレクション回数を表します（少ないほどパフォーマンスがよい）。

<details>
<summary>テストコード</summary>

```cs
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();
            };
    }
}

```

</details>