Информация об изменениях

Сообщение Re[37]: MS забило на дотнет. Питону - да, сишарпу - нет? от 30.08.2021 13:00

Изменено 31.08.2021 19:13 vdimas

Re[37]: MS забило на дотнет. Питону - да, сишарпу - нет?
Здравствуйте, Sinclair, Вы писали:

V>>Еще раз, медленно — интероп в C# медленный.

V>>Требуется сокращать его до минимума.
S>Эмм, а чего там медленного?

Много предварительных телодвижений перед вызовом.
В дотнете вызов не несет с собой никакого контекста текущей VM, в отличии от интеропа в Node.js, где контекст подаётся прямым образом в аргументах.

В дотнете это позволяет вызывать нейтивные точки входа непосредственно в нейтивной сигнатуре, но требует сохранения контекста в стеке и способа разметки стека для возможности работы GC, даже если еще не было возврата из вызванной нейтивной процедуры и даже через всю черезполосицу вызовов, когда из нейтива вызывается опять дотнет и т.д.

И насколько я понимаю, нода и браузеры собирают мусор только после возврата из колбэков, т.е. когда JS-код не работает, то бишь, у них нет JS-стека и нет проблем с нейтивными вызовами.


S>Мне чисто так — в абстрактном смысле интересно. Я всегда полагал, что интероп в дотнете один из самых лучших на рынке. Особенно с версии 5.


В новых версиях языка добавилось много unsafe-фич, но unsafe и interop друг другу немного перпендикулярны.

Или имелось ввиду появление указателей на ф-ии?
Первым делом измерил, там в точности то же быстродействие получилось, что и через [DllImport("Lib")].

Т.е., вызов ф-ии по managed указателю мгновенный, а по unamanged — медленный, разница примерно в 17 раз.
            managedPtr(bar, ref tmp);
00007FFB821B7D5A  mov         rcx,qword ptr [rbp+10h]  
00007FFB821B7D5E  mov         qword ptr [rbp-10h],rcx  
00007FFB821B7D62  mov         rcx,2215AF02C80h  
00007FFB821B7D6C  mov         rcx,qword ptr [rcx]  
00007FFB821B7D6F  lea         rdx,[rbp-8]  
00007FFB821B7D73  mov         rax,qword ptr [rbp-10h]  
00007FFB821B7D77  call        rax  
            unmanagedPtr(bar, ref tmp);
00007FFB821B7D79  mov         rcx,qword ptr [rbp+18h]  
00007FFB821B7D7D  mov         qword ptr [rbp-18h],rcx  
00007FFB821B7D81  mov         rcx,2215AF02C80h  
00007FFB821B7D8B  mov         rcx,qword ptr [rcx]  
00007FFB821B7D8E  lea         rdx,[rbp-8]  
00007FFB821B7D92  mov         r10,qword ptr [rbp-18h]  
00007FFB821B7D96  mov         r11,22163442FA0h  
00007FFB821B7DA0  call        GenericPInvokeCalliHelper (07FFBE1D0A8D0h)


Тут можно пробежаться глазами, что происходит при вызове нейтивной ф-ии через указатель:
https://github.com/dotnet/runtime/blob/57bfe474518ab5b7cfe6bf7424a79ce3af9d6657/src/coreclr/vm/amd64/PInvokeStubs.asm#L29

Здесь немного результатов бенчмарков из списка:
http://www.rsdn.org/forum/flame.comp/8063452.1

Ключевое — BaseLine надо вычитать из других результатов, а не смотреть отношение к нему.
Подкорректированная сводка стоимости вызовов в попугаях при использовании:
Managed func ptr — 35;
делегат — 128;
интерфейс — 172;
DllImport/GetProcAddr — 586;
Обратный вызов через GetFunctionPointerForDelegate — 1180;
Указатель на управляемую ф-ию, преобразованный к неуправляемому и вызванный затем как вызов неуправляемой ф-ии — 7000-8000.

Итого, новый указатель на ф-ию для управляемого кода — киллер фича.
Быстрый, легковесный (в отличие от происходящего при создании экземпляра делегата), и самое главное — позволяет отвязаться от типа делегата, т.к. теперь функциональный тип определяется только сигнатурой.
(это о чём я говорил еще с первой версии дотнета, что вот здесь у них коровья лепёшка для разрабов)

И да, сейчас получить указатель непосредственным образом можно только на статические методы.
Ниже в сырцах можно подсмотреть, как получить указатель на экземплярный метод и как вызывать затем.

Например, экземплярный делегат для каждого экземпляра объекта надо создавать каждый раз новый.
А тут достаточно в некоей своей структуре сохранить пару — указатель на метод и ссылку на экземпляр.

В дотнете для борьбы с таким сценарием обычно создают статический прокси-метод.
(К сожалению, сей простой паттерн не задействовали для машинки async-метода, на каждый вызов создаётся унутре делегат — тяжелое наследение .Net Framework, проклятая индустрией индюшатина... )) )

  Сырцы бенчмарка, каждый файл образует свой проект
using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace ClassLibrary1 {

public interface IBar {
    void Foo(ref ulong value);
}

public class Bar : IBar {

    [MethodImpl(MethodImplOptions.NoInlining)]
    public void Foo(ref ulong value) {
        value++;
    }
}

public static class Helper {
    [MethodImpl(MethodImplOptions.NoInlining)]
    public static IBar GetIBar() => new Bar();

    public static MethodInfo GetFooMethod(IBar bar) => bar.GetType().GetMethod("Foo")!;

    public static IntPtr GetFooAddress(IBar bar) => GetFooMethod(bar).MethodHandle.GetFunctionPointer();
}

public abstract class MethodWrapper {
    public abstract void Foo(ref ulong value);
}

public sealed class Baseline : MethodWrapper {
    [MethodImpl(MethodImplOptions.NoInlining)]
    public override void Foo(ref ulong value) {
        value++;
    }
}

public sealed class WrapperOverInterface : MethodWrapper {
    private readonly IBar _bar;

    public WrapperOverInterface(IBar bar) => _bar = bar;

    [MethodImpl(MethodImplOptions.NoInlining)]
    public override void Foo(ref ulong value) {
        _bar.Foo(ref value);
    }
}

public sealed class WrapperOverDelegate : MethodWrapper {

    private readonly FooDelegate _fn;

    public WrapperOverDelegate(IBar bar) => _fn = ((Bar)bar).Foo;

    [MethodImpl(MethodImplOptions.NoInlining)]
    public override void Foo(ref ulong value) {
        _fn(ref value);
    }

    private delegate void FooDelegate(ref ulong value);
}

public sealed unsafe class WrapperOverPtrFromDelegate : MethodWrapper {

    private readonly FooDelegate _delegate; // prevent GC cleaning
    private readonly delegate* unmanaged[Cdecl]<ref ulong, void> _fn;

    public WrapperOverPtrFromDelegate(IBar bar) {
        _delegate = ((Bar)bar).Foo;
        _fn = (delegate* unmanaged[Cdecl]<ref ulong, void>)Marshal.GetFunctionPointerForDelegate(_delegate);
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    public override void Foo(ref ulong value) {
        _fn(ref value);
    }

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    private delegate void FooDelegate(ref ulong value);
}

public sealed class WrapperOverDelegateFromMethodInfo : MethodWrapper {

    private readonly FooDelegate _fn;

    public WrapperOverDelegateFromMethodInfo(IBar bar)
        => _fn = (FooDelegate)Delegate.CreateDelegate(typeof(FooDelegate), bar, Helper.GetFooMethod(bar));

    [MethodImpl(MethodImplOptions.NoInlining)]
    public override void Foo(ref ulong value) {
        _fn(ref value);
    }

    private delegate void FooDelegate(ref ulong value);
}

public sealed class WrapperOverDelegateFromPtr : MethodWrapper {
    private readonly IBar _bar;
    private readonly FooDelegateFromPtr _fn;

    public WrapperOverDelegateFromPtr(IBar bar) {
        _bar = bar;
        _fn = Marshal.GetDelegateForFunctionPointer<FooDelegateFromPtr>(Helper.GetFooAddress(bar));
    }

    public override void Foo(ref ulong value) {
        _fn(_bar, ref value);
    }

    private delegate void FooDelegateFromPtr(IBar @this, ref ulong value);
}

public sealed unsafe class WrapperOverManagedFunPtr : MethodWrapper {
    private readonly IBar _bar;
    private readonly delegate* managed<IBar, ref ulong, void> _fn;

    public WrapperOverManagedFunPtr(IBar bar) {
        _bar = bar;
        _fn = (delegate* managed<IBar, ref ulong, void>)Helper.GetFooAddress(bar);
    }

    public override void Foo(ref ulong value) {
        _fn(_bar, ref value);
    }
}

public sealed unsafe class WrapperOverUnmanagedFunPtr : MethodWrapper {
    private readonly IBar _bar;
    private readonly delegate* unmanaged<IBar, ref ulong, void> _fn;

    public WrapperOverUnmanagedFunPtr(IBar bar) {
        _bar = bar;
        _fn = (delegate* unmanaged<IBar, ref ulong, void>)Helper.GetFooAddress(bar);
    }

    public override void Foo(ref ulong value) {
        _fn(_bar, ref value);
    }
}

public sealed class WrapperOverDllImport : MethodWrapper {
    [DllImport("SomeDll.dll")]
    private static extern void FooNative(ref ulong value);

    public override void Foo(ref ulong value) {
        FooNative(ref value);
    }
}

public sealed unsafe class WrapperOverDllGetProcAddr : MethodWrapper {
    private readonly delegate* unmanaged[Cdecl]<ref ulong, void> _fn;

    public WrapperOverDllGetProcAddr() {
        var hModule = NativeLibrary.Load("SomeDll.dll");
        _fn = (delegate* unmanaged[Cdecl]<ref ulong, void>)NativeLibrary.GetExport(hModule, "FooNative");
    }
        
    public override void Foo(ref ulong value) {
        _fn(ref value);
    }
}

}


#define WIN32_LEAN_AND_MEAN
#include <windows.h>

BOOL APIENTRY DllMain(HMODULE hModule,
                      DWORD ul_reason_for_call,
                      LPVOID lpReserved
) {
    switch(ul_reason_for_call) {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

extern "C"
__declspec(dllexport) void __stdcall FooNative(UINT64 & value) {
    value++;
}


using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using ClassLibrary1;

#nullable enable

namespace ConsoleApp27 {

[DisassemblyDiagnoser]
public class MethodCallTest {

    public static IEnumerable<CallMethod> Arguments() {
        IBar bar = Helper.GetIBar();

        return new CallMethod[] {
            new("Baseline", new Baseline()),
            new("Interface", new WrapperOverInterface(bar)),
            new("Delegate", new WrapperOverDelegate(bar)),
            new("Delegate from MI", new WrapperOverDelegateFromMethodInfo(bar)),
            new("Delegate from ptr", new WrapperOverDelegateFromPtr(bar)),
            new("Ptr from delegate", new WrapperOverPtrFromDelegate(bar)),
            new("Managed func ptr", new WrapperOverManagedFunPtr(bar)),
            new("Unmanaged func ptr", new WrapperOverUnmanagedFunPtr(bar)),
            new("DllImport", new WrapperOverDllImport()),
            new("DllGetProcAddr", new WrapperOverDllGetProcAddr())
        };
    }

    [ArgumentsSource(nameof(Arguments))]
    [Benchmark]
    [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
    public ulong CallTest(CallMethod m) {
        var fn = m.Func;
        ulong sum = 0;

        for(var i = 0; i < 100000; i++)
            fn.Foo(ref sum);

        return sum;
    }

    public record CallMethod(string Name, MethodWrapper Func) {
        public override string ToString() => Name;
    }
}

internal static class Program {
    private static void Main() {
        BenchmarkRunner.Run<MethodCallTest>();
    }
}

}


S>И как вы ухитряетесь его сократить при помощи СompilerServices.Unsafe?


Часть нейтивной функциональности переношу в дотнет, избавляясь от интеропа.
СompilerServices.Unsafe позволяет получать управляемые ссылки из, грубо, void*, чего до него не было.
Или же приводить одну управляемую ссылку в другую, например, ссылку на byte на ссылку на SomeStruct.
Кароч, позволяет реинтерпретировать память по моему усмотрению.


V>>Я именно тебе приводил не так давно тесты вызова interop или ф-ий через unsafe-указатели — результаты катастрофические.

V>>В обоих случаях.
S>Что-то сходу не могу найти тех тестов.

Так я и не тебе приводил.
Ссылку дал выше.


S>И, главное, как нода-то ухитряется сделать интероп быстрее?


Никак.
"Ополовинить" имелось ввиду отрезать/снизить почти вдвое надобность в интеропе.
Re[37]: MS забило на дотнет. Питону - да, сишарпу - нет?
Здравствуйте, Sinclair, Вы писали:

V>>Еще раз, медленно — интероп в C# медленный.

V>>Требуется сокращать его до минимума.
S>Эмм, а чего там медленного?

Много предварительных телодвижений перед вызовом.
В дотнете вызов не несет с собой никакого контекста текущей VM, в отличии от интеропа в Node.js, где контекст подаётся прямым образом в аргументах.

В дотнете это позволяет вызывать нейтивные точки входа непосредственно в нейтивной сигнатуре, но требует сохранения контекста в стеке и способа разметки стека для возможности работы GC, даже если еще не было возврата из вызванной нейтивной процедуры и даже через всю черезполосицу вызовов, когда из нейтива вызывается опять дотнет и т.д.

И насколько я понимаю, нода и браузеры собирают мусор только после возврата из колбэков, т.е. когда JS-код не работает, то бишь, у них нет JS-стека и нет проблем с нейтивными вызовами.


S>Мне чисто так — в абстрактном смысле интересно. Я всегда полагал, что интероп в дотнете один из самых лучших на рынке. Особенно с версии 5.


В новых версиях языка добавилось много unsafe-фич, но unsafe и interop друг другу немного перпендикулярны.

Или имелось ввиду появление указателей на ф-ии?
Первым делом измерил, там в точности то же быстродействие получилось, что и через [DllImport("Lib")].

Т.е., вызов ф-ии по managed указателю мгновенный, а по unamanged — медленный, разница примерно в 17 раз.
            managedPtr(bar, ref tmp);
00007FFB821B7D5A  mov         rcx,qword ptr [rbp+10h]  
00007FFB821B7D5E  mov         qword ptr [rbp-10h],rcx  
00007FFB821B7D62  mov         rcx,2215AF02C80h  
00007FFB821B7D6C  mov         rcx,qword ptr [rcx]  
00007FFB821B7D6F  lea         rdx,[rbp-8]  
00007FFB821B7D73  mov         rax,qword ptr [rbp-10h]  
00007FFB821B7D77  call        rax  
            unmanagedPtr(bar, ref tmp);
00007FFB821B7D79  mov         rcx,qword ptr [rbp+18h]  
00007FFB821B7D7D  mov         qword ptr [rbp-18h],rcx  
00007FFB821B7D81  mov         rcx,2215AF02C80h  
00007FFB821B7D8B  mov         rcx,qword ptr [rcx]  
00007FFB821B7D8E  lea         rdx,[rbp-8]  
00007FFB821B7D92  mov         r10,qword ptr [rbp-18h]  
00007FFB821B7D96  mov         r11,22163442FA0h  
00007FFB821B7DA0  call        GenericPInvokeCalliHelper (07FFBE1D0A8D0h)


Тут можно пробежаться глазами, что происходит при вызове нейтивной ф-ии через указатель:
https://github.com/dotnet/runtime/blob/57bfe474518ab5b7cfe6bf7424a79ce3af9d6657/src/coreclr/vm/amd64/PInvokeStubs.asm#L29

Здесь немного результатов бенчмарков из списка:
http://www.rsdn.org/forum/flame.comp/8063452.1

Ключевое — BaseLine надо вычитать из других результатов, а не смотреть отношение к нему.
Подкорректированная сводка стоимости вызовов в попугаях при использовании:
Managed func ptr — 35;
делегат — 128;
интерфейс — 172;
DllImport/GetProcAddr — 586;
Обратный вызов через GetFunctionPointerForDelegate — 1180;
Указатель на управляемую ф-ию, преобразованный к неуправляемому и вызванный затем как вызов неуправляемой ф-ии — 7000-8000.

Итого, новый указатель на ф-ию для управляемого кода — киллер фича.
Быстрый, легковесный (в отличие от происходящего при создании экземпляра делегата), и самое главное — позволяет отвязаться от типа делегата, т.к. теперь функциональный тип определяется только сигнатурой.
(это о чём я говорил еще с первой версии дотнета, что вот здесь у них коровья лепёшка для разрабов)

И да, сейчас получить указатель непосредственным образом можно только на статические методы.
Ниже в сырцах можно подсмотреть, как получить указатель на экземплярный метод и как вызывать затем.

Например, экземплярный делегат для каждого экземпляра объекта надо создавать каждый раз новый.
А тут достаточно в некоей своей структуре сохранить пару — указатель на метод и ссылку на экземпляр.

В дотнете для борьбы с таким сценарием обычно создают статический прокси-метод.
(К сожалению, сей простой паттерн не задействовали для машинки async-метода, на каждый вызов создаётся унутре делегат — тяжелое наследение .Net Framework, проклятая индустрией индюшатина... )) )

  Сырцы бенчмарка, каждый файл образует свой проект
using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace ClassLibrary1 {

public interface IBar {
    void Foo(ref ulong value);
}

public class Bar : IBar {

    [MethodImpl(MethodImplOptions.NoInlining)]
    public void Foo(ref ulong value) {
        value++;
    }
}

public static class Helper {
    [MethodImpl(MethodImplOptions.NoInlining)]
    public static IBar GetIBar() => new Bar();

    public static MethodInfo GetFooMethod(IBar bar) => bar.GetType().GetMethod("Foo")!;

    public static IntPtr GetFooAddress(IBar bar) => GetFooMethod(bar).MethodHandle.GetFunctionPointer();
}

public abstract class MethodWrapper {
    public abstract void Foo(ref ulong value);
}

public sealed class Baseline : MethodWrapper {
    [MethodImpl(MethodImplOptions.NoInlining)]
    public override void Foo(ref ulong value) {
        value++;
    }
}

public sealed class WrapperOverInterface : MethodWrapper {
    private readonly IBar _bar;

    public WrapperOverInterface(IBar bar) => _bar = bar;

    [MethodImpl(MethodImplOptions.NoInlining)]
    public override void Foo(ref ulong value) {
        _bar.Foo(ref value);
    }
}

public sealed class WrapperOverDelegate : MethodWrapper {

    private readonly FooDelegate _fn;

    public WrapperOverDelegate(IBar bar) => _fn = ((Bar)bar).Foo;

    [MethodImpl(MethodImplOptions.NoInlining)]
    public override void Foo(ref ulong value) {
        _fn(ref value);
    }

    private delegate void FooDelegate(ref ulong value);
}

public sealed unsafe class WrapperOverPtrFromDelegate : MethodWrapper {

    private readonly FooDelegate _delegate; // prevent GC cleaning
    private readonly delegate* unmanaged[Cdecl]<ref ulong, void> _fn;

    public WrapperOverPtrFromDelegate(IBar bar) {
        _delegate = ((Bar)bar).Foo;
        _fn = (delegate* unmanaged[Cdecl]<ref ulong, void>)Marshal.GetFunctionPointerForDelegate(_delegate);
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    public override void Foo(ref ulong value) {
        _fn(ref value);
    }

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    private delegate void FooDelegate(ref ulong value);
}

public sealed class WrapperOverDelegateFromMethodInfo : MethodWrapper {

    private readonly FooDelegate _fn;

    public WrapperOverDelegateFromMethodInfo(IBar bar)
        => _fn = (FooDelegate)Delegate.CreateDelegate(typeof(FooDelegate), bar, Helper.GetFooMethod(bar));

    [MethodImpl(MethodImplOptions.NoInlining)]
    public override void Foo(ref ulong value) {
        _fn(ref value);
    }

    private delegate void FooDelegate(ref ulong value);
}

public sealed class WrapperOverDelegateFromPtr : MethodWrapper {
    private readonly IBar _bar;
    private readonly FooDelegateFromPtr _fn;

    public WrapperOverDelegateFromPtr(IBar bar) {
        _bar = bar;
        _fn = Marshal.GetDelegateForFunctionPointer<FooDelegateFromPtr>(Helper.GetFooAddress(bar));
    }

    public override void Foo(ref ulong value) {
        _fn(_bar, ref value);
    }

    private delegate void FooDelegateFromPtr(IBar @this, ref ulong value);
}

public sealed unsafe class WrapperOverManagedFunPtr : MethodWrapper {
    private readonly IBar _bar;
    private readonly delegate* managed<IBar, ref ulong, void> _fn;

    public WrapperOverManagedFunPtr(IBar bar) {
        _bar = bar;
        _fn = (delegate* managed<IBar, ref ulong, void>)Helper.GetFooAddress(bar);
    }

    public override void Foo(ref ulong value) {
        _fn(_bar, ref value);
    }
}

public sealed unsafe class WrapperOverUnmanagedFunPtr : MethodWrapper {
    private readonly IBar _bar;
    private readonly delegate* unmanaged<IBar, ref ulong, void> _fn;

    public WrapperOverUnmanagedFunPtr(IBar bar) {
        _bar = bar;
        _fn = (delegate* unmanaged<IBar, ref ulong, void>)Helper.GetFooAddress(bar);
    }

    public override void Foo(ref ulong value) {
        _fn(_bar, ref value);
    }
}

public sealed class WrapperOverDllImport : MethodWrapper {
    [DllImport("SomeDll.dll")]
    private static extern void FooNative(ref ulong value);

    public override void Foo(ref ulong value) {
        FooNative(ref value);
    }
}

public sealed unsafe class WrapperOverDllGetProcAddr : MethodWrapper {
    private readonly delegate* unmanaged[Cdecl]<ref ulong, void> _fn;

    public WrapperOverDllGetProcAddr() {
        var hModule = NativeLibrary.Load("SomeDll.dll");
        _fn = (delegate* unmanaged[Cdecl]<ref ulong, void>)NativeLibrary.GetExport(hModule, "FooNative");
    }
        
    public override void Foo(ref ulong value) {
        _fn(ref value);
    }
}

}


#define WIN32_LEAN_AND_MEAN
#include <windows.h>

BOOL APIENTRY DllMain(HMODULE hModule,
                      DWORD ul_reason_for_call,
                      LPVOID lpReserved
) {
    switch(ul_reason_for_call) {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

extern "C"
__declspec(dllexport) void __stdcall FooNative(UINT64 & value) {
    value++;
}


using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using ClassLibrary1;

#nullable enable

namespace ConsoleApp27 {

[DisassemblyDiagnoser]
public class MethodCallTest {

    public static IEnumerable<CallMethod> Arguments() {
        IBar bar = Helper.GetIBar();

        return new CallMethod[] {
            new("Baseline", new Baseline()),
            new("Interface", new WrapperOverInterface(bar)),
            new("Delegate", new WrapperOverDelegate(bar)),
            new("Delegate from MI", new WrapperOverDelegateFromMethodInfo(bar)),
            new("Delegate from ptr", new WrapperOverDelegateFromPtr(bar)),
            new("Ptr from delegate", new WrapperOverPtrFromDelegate(bar)),
            new("Managed func ptr", new WrapperOverManagedFunPtr(bar)),
            new("Unmanaged func ptr", new WrapperOverUnmanagedFunPtr(bar)),
            new("DllImport", new WrapperOverDllImport()),
            new("DllGetProcAddr", new WrapperOverDllGetProcAddr())
        };
    }

    [ArgumentsSource(nameof(Arguments))]
    [Benchmark]
    [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
    public ulong CallTest(CallMethod m) {
        var fn = m.Func;
        ulong sum = 0;

        for(var i = 0; i < 100000; i++)
            fn.Foo(ref sum);

        return sum;
    }

    public record CallMethod(string Name, MethodWrapper Func) {
        public override string ToString() => Name;
    }
}

internal static class Program {
    private static void Main() {
        BenchmarkRunner.Run<MethodCallTest>();
    }
}

}


S>И как вы ухитряетесь его сократить при помощи СompilerServices.Unsafe?


Часть нейтивной функциональности переношу в дотнет, избавляясь от интеропа.
СompilerServices.Unsafe позволяет получать управляемые ссылки из, грубо, void*, чего до него не было.
Или же приводить одну управляемую ссылку в другую, например, ссылку на byte на ссылку на SomeStruct.
Кароч, позволяет реинтерпретировать память по моему усмотрению.


V>>Я именно тебе приводил не так давно тесты вызова interop или ф-ий через unsafe-указатели — результаты катастрофические.

V>>В обоих случаях.
S>Что-то сходу не могу найти тех тестов.

Так я и не тебе приводил.
Ссылку дал выше.


S>И, главное, как нода-то ухитряется сделать интероп быстрее?


Ноде не надо заботиться о фреймах стека для GC, поэтому, вызывает нейтивные ф-ии напрямую.

"Ополовинить интероп" в C# имелось ввиду отрезать/снизить почти вдвое надобность в интеропе.