Самый быстрый способ скопировать 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);
}