#c#
Вопрос:
Разница в скорости кода не так важна, как я удивляюсь, почему. Один метод короткий, другой, на мой взгляд, оптимизирован намного лучше, но в тесте короче, но хуже, быстрее, так что, если кто-нибудь знает, почему.
Оптимизированный метод немного похож на то, что находится в библиотеке NET Framework 4.8 в классе Buffer в методе Memmove.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Versioning;
using System.Runtime.ConstrainedExecution;
using System.Diagnostics;
namespace test
{
public static class TestClass
{
const int MAX_CHARS = 101;
static int _cyclesLength = 10000000;
static char[] _source;
static TestClass()
{
_source = ("A computer is a machine that can be programmed to carry out sequences of arithmetic or logical operations automatically. Modern computers can perform generic "
"sets of operations known as programs. These programs enable computers to perform a wide range of tasks. A computer system is a complete computer that includes the "
"hardware, operating system (main software), and peripheral equipment needed and used for full operation. This term may also refer to a group of computers that are "
"linked and function together, such as a computer network or computer cluster. A broad range of industrial and consumer products use computers as control systems.Simple "
"special-purpose devices like microwave ovens and remote controls are included, as are factory devices like industrial robots and computer - aided design, as well as general "
"- purpose devices like personal computers and mobile devices like smartphones.Computers power the Internet, which links hundreds of millions of other computers and users.").ToArray();
}
[System.Security.SecurityCritical]
[ResourceExposure(ResourceScope.None)]
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
public static unsafe void Copy(char* source, char* target, int length)
{
while (length >= 8)
{
*(int*)source = *(int*)target;
*(int*)(source 2) = *(int*)(target 2);
*(int*)(source 4) = *(int*)(target 4);
*(int*)(source 6) = *(int*)(target 6);
target = 8;
source = 8;
length -= 8;
}
while (length >= 2)
{
*(int*)source = *(int*)target;
target = 2;
source = 2;
length -= 2;
}
if (length > 0)
*source = *target;
}
[System.Security.SecurityCritical]
[ResourceExposure(ResourceScope.None)]
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
public static unsafe void OptimizedCopy(char* source, char* target, int length)
{
if (length < 8)
{
switch (length)
{
case 0:
return;
case 1:
*target = *source;
return;
case 2:
*(int*)target = *(int*)source;
return;
case 3:
*(int*)target = *(int*)source;
*(target 2) = *(source 2);
return;
case 4:
*(int*)target = *(int*)source;
*(int*)(target 2) = *(int*)(source 2);
return;
case 5:
*(int*)target = *(int*)source;
*(int*)(target 2) = *(int*)(source 2);
*(target 4) = *(source 4);
return;
case 6:
*(int*)target = *(int*)source;
*(int*)(target 2) = *(int*)(source 2);
*(int*)(target 4) = *(int*)(source 4);
return;
case 7:
*(int*)target = *(int*)source;
*(int*)(target 2) = *(int*)(source 2);
*(int*)(target 4) = *(int*)(source 4);
*(target 6) = *(source 6);
return;
}
}
switch (length amp; 7)
{
case 0:
break;
case 1:
*(int*)(target length - 2) = *(int*)(source length - 2);
break;
case 2:
*(int*)(target length - 2) = *(int*)(source length - 2);
break;
case 3:
*(int*)(target length - 4) = *(int*)(source length - 4);
*(int*)(target length - 2) = *(int*)(source length - 2);
break;
case 4:
*(int*)(target length - 4) = *(int*)(source length - 4);
*(int*)(target length - 2) = *(int*)(source length - 2);
break;
case 5:
*(int*)(target length - 6) = *(int*)(source length - 6);
*(int*)(target length - 4) = *(int*)(source length - 4);
*(int*)(target length - 2) = *(int*)(source length - 2);
break;
case 6:
*(int*)(target length - 6) = *(int*)(source length - 6);
*(int*)(target length - 4) = *(int*)(source length - 4);
*(int*)(target length - 2) = *(int*)(source length - 2);
break;
case 7:
*(int*)(target length - 8) = *(int*)(source length - 8);
*(int*)(target length - 6) = *(int*)(source length - 6);
*(int*)(target length - 4) = *(int*)(source length - 4);
*(int*)(target length - 2) = *(int*)(source length - 2);
break;
}
while (true)
{
*(int*)target = *(int*)source;
*(int*)(target 2) = *(int*)(source 2);
*(int*)(target 4) = *(int*)(source 4);
*(int*)(target 6) = *(int*)(source 6);
if (length < 16) return;
*(int*)(target 8) = *(int*)(source 8);
*(int*)(target 10) = *(int*)(source 10);
*(int*)(target 12) = *(int*)(source 12);
*(int*)(target 14) = *(int*)(source 14);
if (length < 24) return;
*(int*)(target 16) = *(int*)(source 16);
*(int*)(target 18) = *(int*)(source 18);
*(int*)(target 20) = *(int*)(source 20);
*(int*)(target 22) = *(int*)(source 22);
if (length < 32) return;
*(int*)(target 24) = *(int*)(source 24);
*(int*)(target 26) = *(int*)(source 26);
*(int*)(target 28) = *(int*)(source 28);
*(int*)(target 30) = *(int*)(source 30);
if (length < 40) return;
*(int*)(target 32) = *(int*)(source 32);
*(int*)(target 34) = *(int*)(source 34);
*(int*)(target 36) = *(int*)(source 36);
*(int*)(target 38) = *(int*)(source 38);
if (length < 48) return;
*(int*)(target 40) = *(int*)(source 40);
*(int*)(target 42) = *(int*)(source 42);
*(int*)(target 44) = *(int*)(source 44);
*(int*)(target 46) = *(int*)(source 46);
if (length < 56) return;
source = 48;
target = 48;
length -= 48;
}
}
private static unsafe long TestCopy()
{
long cyclesLength = _cyclesLength;
char[] sourceArr = _source;
char[] targetArr = new char[MAX_CHARS];
fixed (char* source = sourceArr, target = targetArr)
{
for (long i = 0; i < cyclesLength; i )
{
for (int j = 1; j <= MAX_CHARS; j )
Copy(source, target, j);
}
}
return 1;
}
private static unsafe long TestOptimizedCopy()
{
long cyclesLength = _cyclesLength;
char[] sourceArr = _source;
char[] targetArr = new char[MAX_CHARS];
fixed (char* source = sourceArr, target = targetArr)
{
for (long i = 0; i < cyclesLength; i )
{
for (int j = 1; j <= MAX_CHARS; j )
OptimizedCopy(source, target, j);
}
}
return 1;
}
public static unsafe void TestMethod(long pocetCyklu = 0)
{
Stopwatch stopwatch = new Stopwatch();
//TestCopy
System.GC.Collect();
System.Threading.Thread.Sleep(1000);
stopwatch.Start();
TestCopy();
stopwatch.Stop();
Console.WriteLine( stopwatch.Elapsed.TotalSeconds.ToString() " TestCopy");
//TestOptimizedCopy
System.GC.Collect();
System.Threading.Thread.Sleep(1000);
stopwatch.Restart();
TestOptimizedCopy();
stopwatch.Stop();
Console.WriteLine(stopwatch.Elapsed.TotalSeconds.ToString() " TestOptimizedCopy");
}
}
}
Мне кажется, что худший, но более короткий алгоритм по какой-то причине работает быстрее, но я не знаю почему. Может быть, это потому, что CLR требуется некоторое время для перевода CIL в машинный код?
Изменить: Очень упрощенный метод NET Framework 4.8 из класса Buffer Buffer.cs. Я удалил код для HAS_CUSTOM_BLOCKS, BIT64, перекрытия буферов… и я изменил тип данных с байта* на символ*. Недавно добавленный код теперь очень близок к оригиналу, но результат еще хуже. Первый простой метод, описанный выше, когда-то был написан мной, но теперь, когда я наткнулся на профессиональный код из библиотеки, я хотел заменить его. Но сначала я сравнил их, и с тех пор мне стало интересно, почему новый не быстрее. Когда мое решение записывает гораздо больше значений источника, назначения и длины. В любом случае, код библиотеки использует несколько трюков, таких как копирование с конца. Многие люди стараются избегать ключевого слова goto.. 🙂
[System.Security.SecurityCritical]
[ResourceExposure(ResourceScope.None)]
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
internal unsafe static void Memmove(char* src, char* dest, int len)
{
//const int CopyThreshold = 1024; //PLATFORM_WINDOWS (2048 bytes)
char* srcEnd = src len;
char* destEnd = dest len;
if (len <= 8) goto MCPY02;
if (len > 32) goto MCPY05;
MCPY00:
*(int*)dest = *(int*)src;
*(int*)(dest 2) = *(int*)(src 2);
*(int*)(dest 4) = *(int*)(src 4);
*(int*)(dest 6) = *(int*)(src 6); // [0,16]
if (len <= 16) goto MCPY01;
*(int*)(dest 8) = *(int*)(src 8);
*(int*)(dest 10) = *(int*)(src 10);
*(int*)(dest 12) = *(int*)(src 12);
*(int*)(dest 14) = *(int*)(src 14); // [0,32]
if (len <= 24) goto MCPY01;
*(int*)(dest 16) = *(int*)(src 16);
*(int*)(dest 18) = *(int*)(src 18);
*(int*)(dest 20) = *(int*)(src 20);
*(int*)(dest 22) = *(int*)(src 22); // [0,48]
MCPY01:
*(int*)(destEnd - 8) = *(int*)(srcEnd - 8);
*(int*)(destEnd - 6) = *(int*)(srcEnd - 6);
*(int*)(destEnd - 4) = *(int*)(srcEnd - 4);
*(int*)(destEnd - 2) = *(int*)(srcEnd - 2);
return;
MCPY02:
if ((len amp; 12) == 0) goto MCPY03;
*(int*)dest = *(int*)src;
*(int*)(dest 2) = *(int*)(src 2);
*(int*)(destEnd - 4) = *(int*)(srcEnd - 4);
*(int*)(destEnd - 2) = *(int*)(srcEnd - 2);
return;
MCPY03:
if ((len amp; 2) == 0) goto MCPY04;
*(int*)dest = *(int*)src;
*(int*)(destEnd - 2) = *(int*)(srcEnd - 2);
return;
MCPY04:
if (len == 0) return;
*dest = *src;
return;
MCPY05:
//if (len > CopyThreshold) goto PInvoke; //I don't use so big range
int n = len >> 5;
MCPY06:
*(int*)dest = *(int*)src;
*(int*)(dest 2) = *(int*)(src 2);
*(int*)(dest 4) = *(int*)(src 4);
*(int*)(dest 6) = *(int*)(src 6);
*(int*)(dest 8) = *(int*)(src 8);
*(int*)(dest 10) = *(int*)(src 10);
*(int*)(dest 12) = *(int*)(src 12);
*(int*)(dest 14) = *(int*)(src 14);
*(int*)(dest 16) = *(int*)(src 16);
*(int*)(dest 18) = *(int*)(src 19);
*(int*)(dest 20) = *(int*)(src 20);
*(int*)(dest 22) = *(int*)(src 22);
*(int*)(dest 24) = *(int*)(src 24);
*(int*)(dest 26) = *(int*)(src 26);
*(int*)(dest 28) = *(int*)(src 28);
*(int*)(dest 30) = *(int*)(src 30);
dest = 32;
src = 32;
n--;
if (n != 0) goto MCPY06;
len %= 32;
if (len > 8) goto MCPY00;
*(int*)(destEnd - 8) = *(int*)(srcEnd - 8);
*(int*)(destEnd - 6) = *(int*)(srcEnd - 6);
*(int*)(destEnd - 4) = *(int*)(srcEnd - 4);
*(int*)(destEnd - 2) = *(int*)(srcEnd - 2);
return;
}
Для копирования большого количества байтов класс имеет очень быстрый (по крайней мере, по сравнению с управляемым кодом) метод __Memmove(byte* dest, byte* src, nuint len). Управляемая память к управляемой.
Я точно не знаю, как работает бенчмарк, но его результаты очень нестабильны. Один раз все методы почти одинаковы (разница 20%), а в другой раз разница большая (около 100%). В данном случае очень медленный массив.Метод копирования, который многие люди пишут о том, насколько он медленный и, согласно секундомеру, в 10 раз медленнее, является самым быстрым.
Комментарии:
1. Я думаю, что интересным экспериментом было бы попробовать это в C (вам, очевидно, удобно с указателями!) И посмотрите, получите ли вы те же результаты. Я бы не подумал, что время компиляции JIT CLR является фактором здесь, но то, как CLR компилирует IL в сборку x86/x64, в значительной степени является черным ящиком. Я думаю, что большинство людей скажут вам не пытаться чрезмерно оптимизировать . ЧИСТЫЙ код таким образом, потому что вы не знаете, что CLR собирается с ним делать. Но это увлекательный вопрос!
2. Ваш «оптимизированный» метод содержит множество операций сравнения/перехода для каждого прохода.
3. Обратите внимание, что тестирование с секундомером-не лучший подход к бенчмаркингу в .NET, потому что эффекты кода JITting во время выполнения, сбора мусора и прочего могут оказать большое влияние, если вы не будете очень осторожны в написании тестов. Обычно рекомендуемым решением является использование Benchmark.NET который будет выполнять такие вещи, как «разминочные» тесты для решения подобных проблем.
Ответ №1:
В вашем методе много операций сравнения OptimizedCopy
. Взгляните на перегрузки Marshal.Copy, в этом случае они могут быть намного быстрее.
Комментарии:
1.
Buffer.BlockCopy
это тоже очень быстро.2. На мой взгляд, оба метода содержат почти одинаковое количество сравнений, это просто скрыто в цикле. Но определенно более короткий метод записывает гораздо больше в память. Я использую только управляемую память, поэтому выбрал класс Buffer и метод BlockCopy.