LoginSignup
4
3

UnsafeAccessor でジェネリックが使えなかった話

Last updated at Posted at 2024-02-12

はじめに

UnsafeAccessor を活用して List<T> の内部配列を取れないものかと思ったのですが、ジェネリックな型を含む場合は使えないようでした。実行時 System.BadImageFormatException(Invalid usage of UnsafeAccessorAttribute.) になります。

追記: ↓ の方法でやりたいことはできるようです。

List<int> list = [1, 2, 3];
var span = System.Runtime.InteropServices.CollectionsMarshal.AsSpan(list);

UnsafeAccessor とは

using System.Runtime.CompilerServices;

class TestClass
{
    private int _value;
    public int Value { get => this._value; set => this._value = value; }
}

[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_value")]
extern static ref int _value(TestClass value);

var test = new TestClass();

_value(test) = 1;

List<T> の内部配列を取りたい

List<T> はよく使う型なのですが、Span<T> を取りたいときに配列のコピー(ToArray())を作らないといけないもどかしさがあります。内部配列を取得できればこれを省略できそうです。

extern メソッドでは、ジェネリック型を指定するとうまくいかないようです。
それもそのはずで、ジェネリック型は実行時に型を決定しますが、extern はコンパイル時に型を決定します(うろ覚え)。
ジェネリック型でない、具体的な型(例: int)を与えるとうまくいきます。

class A<T>
{
    [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_items")]
    internal static extern ref T[] _items(List<T> list);
}

[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_items")]
internal static extern ref int[] _items(List<int> list);

 var list = new List<int>();
A<int>._items(list); // こちらは例外
_items(list); // こちらはいける

おわりに

List<T> の内部配列は取れないこともないですが、型ごとに取得メソッドを定義する必要があり、あまり実用的ではなさそうです。

おまけ UnsafeAccessor のパフォーマンス比較

テストコード
using System.Linq.Expressions;
using System.Runtime.CompilerServices;

class TestClass
{
    private int _value;
    public int Value { get => this._value; set => this._value = value; }
}

[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_value")]
extern static ref int _value(TestClass value);

static Action AssignProperty()
{
    var test = new TestClass();

    return () => test.Value++;
}

static Action AssignUnsafeAccessor()
{
    var test = new TestClass();

    return () => _value(test)++;
}

static Action AssignExpression()
{
    var test = new TestClass();
    var info = test.GetType().GetField("_value", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;

    Func<TestClass, int> getter;
    {
        var p = Expression.Parameter(typeof(TestClass));
        getter = Expression.Lambda<Func<TestClass, int>>(Expression.Field(p, info), p).Compile();
    }

    Action<TestClass, int> setter;
    {
        var p1 = Expression.Parameter(typeof(TestClass));
        var p2 = Expression.Parameter(typeof(int));
        setter = Expression.Lambda<Action<TestClass, int>>(Expression.Assign(Expression.Field(p1, info), p2), p1, p2).Compile();
    }

    return () => setter(test, getter(test) + 1);
}

static Action AssignReflection()
{
    var test = new TestClass();
    var info = test.GetType().GetField("_value", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;

    return () =>
    {
        int value = (int)info.GetValue(test)!;
        info.SetValue(test, value + 1);
    };
}
Test Score % CG0
AssignProperty 2,566,308 100.0% 0
AssignUnsafeAccessor 2,557,564 99.7% 0
AssignExpression 2,578,716 100.5% 0
AssignReflection 857,693 33.4% 4
  • UnsafeAccessor はプロパティと遜色ないパフォーマンスです
  • 式木もプロパティと遜色ないパフォーマンスです。これは意外でした。初期化処理分を計測していないため、実際はもう少しパフォーマンスは落ちると思います
  • リフレクションはややパフォーマンスは落ちますが、昔に比べると一桁くらい良くなってる気がします。BOX 化の影響でガベージが発生します

実行環境: Windows11 x64 .NET Runtime 8.0.0
Score は高いほどパフォーマンスがよいです。
GC0 はガベージコレクション回数を表します(少ないほどパフォーマンスがよい)。

4
3
2

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
4
3