Самый быстрый способ скопировать blittable структуру в неуправляемую область памяти (IntPtr)

У меня есть функция, аналогичная следующей:

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetVariable<T>(T newValue) where T : struct {
    // I know by this point that T is blittable (i.e. only unmanaged value types)

    // varPtr is a void*, and is where I want to copy newValue to
    *varPtr = newValue; // This won't work, but is basically what I want to do
}

Я видел Marshal.StructureToIntPtr(), но он кажется довольно медленным, и это чувствительный к производительности код. Если бы я знал тип T Я мог бы просто объявить varPtr как T*, но... ну, нет.

В любом случае, я ищу самый быстрый способ сделать это. "Безопасность" не имеет значения: к этому моменту в коде я знаю, что размер структуры T будет точно соответствовать памяти, на которую указывает varPtr,

4 ответа

Решение

Один из ответов - переопределить нативный memcpy вместо этого в C#, используя те же приемы оптимизации, которые пытается сделать нативный memcpy. Вы можете видеть, что Microsoft делает это в своем собственном источнике. Смотрите файл Buffer.cs в справочном источнике Microsoft:

     // This is tricky to get right AND fast, so lets make it useful for the whole Fx.
     // E.g. System.Runtime.WindowsRuntime!WindowsRuntimeBufferExtensions.MemCopy uses it.
     internal unsafe static void Memcpy(byte* dest, byte* src, int len) {

        // This is portable version of memcpy. It mirrors what the hand optimized assembly versions of memcpy typically do.
        // Ideally, we would just use the cpblk IL instruction here. Unfortunately, cpblk IL instruction is not as efficient as
        // possible yet and so we have this implementation here for now.

        switch (len)
        {
        case 0:
            return;
        case 1:
            *dest = *src;
            return;
        case 2:
            *(short *)dest = *(short *)src;
            return;
        case 3:
            *(short *)dest = *(short *)src;
            *(dest + 2) = *(src + 2);
            return;
        case 4:
            *(int *)dest = *(int *)src;
            return;
        ...

Интересно отметить, что они изначально реализуют memcpy для всех размеров до 512; большинство размеров используют приемы сглаживания указателей, чтобы заставить VM выдавать инструкции, которые работают с разными размерами. Только в 512 они наконец-то начинают использовать нативный memcpy:

        // P/Invoke into the native version for large lengths
        if (len >= 512)
        {
            _Memcpy(dest, src, len);
            return;
        }

Предположительно, нативный memcpy еще быстрее, поскольку его можно оптимизировать вручную, чтобы использовать инструкции SSE/MMX для выполнения копирования.

По предложению BenVoigt я попробовал несколько вариантов. Для всех этих тестов я скомпилировал архитектуру Any CPU с использованием стандартной сборки VS2013 Release и провел тест вне среды IDE. Перед каждым тестом измеряли методы DoTestA() а также DoTestB() были запущены несколько раз, чтобы позволить разогрев JIT.


Во-первых, я сравнил Marshal.StructToPtr байтовому циклу с различными размерами структуры. Я показал код ниже, используя SixtyFourByteStruct:

private unsafe static void DoTestA() {
    fixed (SixtyFourByteStruct* fixedStruct = &structToCopy) {
        byte* structStart = (byte*) fixedStruct;
        byte* targetStart = (byte*) unmanagedTarget;
        for (byte* structPtr = structStart, targetPtr = targetStart; structPtr < structStart + sizeof(SixtyFourByteStruct); ++structPtr, ++targetPtr) {
            *targetPtr = *structPtr;
        }
    }
}

private static void DoTestB() {
    Marshal.StructureToPtr(structToCopy, unmanagedTarget, false);
}

И результаты:

>>> 500000 repetitions >>> IN NANOSECONDS (1000ns = 0.001ms)
Method   Avg.         Min.         Max.         Jitter       Total
A        82ns         0ns          22,000ns     21,917ns     ! 41.017ms
B        137ns        0ns          38,700ns     38,562ns     ! 68.834ms

Как видите, ручной цикл работает быстрее (как я и подозревал). Результаты одинаковы для шестнадцатибайтовой и четырехбайтовой структуры, причем различие тем сильнее, чем меньше структура.


Итак, теперь, чтобы попробовать ручное копирование с использованием P/Invoke и memcpy:

private unsafe static void DoTestA() {
    fixed (FourByteStruct* fixedStruct = &structToCopy) {
        byte* structStart = (byte*) fixedStruct;
        byte* targetStart = (byte*) unmanagedTarget;
        for (byte* structPtr = structStart, targetPtr = targetStart; structPtr < structStart + sizeof(FourByteStruct); ++structPtr, ++targetPtr) {
            *targetPtr = *structPtr;
        }
    }
}

private unsafe static void DoTestB() {
    fixed (FourByteStruct* fixedStruct = &structToCopy) {
        memcpy(unmanagedTarget, (IntPtr) fixedStruct, new UIntPtr((uint) sizeof(FourByteStruct)));
    }
}

>>> 500000 repetitions >>> IN NANOSECONDS (1000ns = 0.001ms)
Method   Avg.         Min.         Max.         Jitter       Total
A        61ns         0ns          28,000ns     27,938ns     ! 30.736ms
B        84ns         0ns          45,900ns     45,815ns     ! 42.216ms

Итак, кажется, что ручная копия все еще лучше в моем случае. Как и раньше, результаты были примерно одинаковыми для 16/64/64 байтовых структур (хотя для 64-байтового размера разрыв был <10 нс).


Мне пришло в голову, что я тестировал только те структуры, которые помещаются в строку кэша (у меня стандартный процессор x86_64). Итак, я попробовал 128-байтовую структуру, и она изменила баланс в пользу memcpy:

>>> 500000 repetitions >>> IN NANOSECONDS (1000ns = 0.001ms)
Method   Avg.         Min.         Max.         Jitter       Total
A        104ns        0ns          48,300ns     48,195ns     ! 52.150ms
B        84ns         0ns          38,400ns     38,315ns     ! 42.284ms

Как бы то ни было, вывод ко всему этому заключается в том, что побайтовая копия кажется самой быстрой для любой структуры размером <=64 байта на процессоре x86_64 на моей машине. Примите это как хотите (и, возможно, кто-то заметит неэффективность моего кода в любом случае).

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

public static class Buffer
{
    public unsafe delegate void MemcpyDelegate(byte* dest, byte* src, int len);

    public static readonly MemcpyDelegate Memcpy;
    static Buffer()
    {
        var methods = typeof (System.Buffer).GetMethods(BindingFlags.Static | BindingFlags.NonPublic).Where(m=>m.Name == "Memcpy");
        var memcpy = methods.First(mi => mi.GetParameters().Select(p => p.ParameterType).SequenceEqual(new[] {typeof (byte*), typeof (byte*), typeof (int)}));
        Memcpy = (MemcpyDelegate) memcpy.CreateDelegate(typeof (MemcpyDelegate));
    }
}

Использование:

public static unsafe void MemcpyExample()
{
     int src = 12345;
     int dst = 0;
     Buffer.Memcpy((byte*) &dst, (byte*) &src, sizeof (int));
     System.Diagnostics.Debug.Assert(dst==12345);
}
   public void SetVariable<T>(T newValue) where T : struct

Вы не можете использовать дженерики, чтобы выполнить это быстрым способом. Компилятор не воспринимает ваши голубые глаза как гарантию того, что T на самом деле является легкомысленным, ограничение недостаточно хорошее. Вы должны использовать перегрузки:

    public unsafe void SetVariable(int newValue) {
        *(int*)varPtr = newValue;
    }
    public unsafe void SetVariable(double newValue) {
        *(double*)varPtr = newValue;
    }
    public unsafe void SetVariable(Point newValue) {
        *(Point*)varPtr = newValue;
    }
    // etc...

Что может быть неудобно, но быстро. Он компилируется в одну инструкцию MOV без затрат на вызов метода в режиме Release. Самый быстрый это может быть.

И резервный случай, профилировщик скажет вам, когда вам нужно перегрузить:

    public unsafe void SetVariable<T>(T newValue) {
        Marshal.StructureToPtr(newValue, (IntPtr)varPtr, false);
    }
Другие вопросы по тегам