Как оптимизировать копирование кусков массива в С#?
Я пишу приложение для видеоизображения в реальном времени и должен ускорить этот метод. В настоящее время он занимает около 10 мс для выполнения, и я хотел бы получить его до 2-3 мс.
Я пробовал как Array.Copy, так и Buffer.BlockCopy, и они оба берут ~ 30 мс, что на 3 раза больше, чем ручная копия.
Одна мысль заключалась в том, чтобы как-то скопировать 4 байта как целое число, а затем вставить их как целое число, тем самым уменьшив 4 строки кода до одной строки кода. Однако я не уверен, как это сделать.
Другая мысль заключалась в том, чтобы каким-то образом использовать указатели и небезопасный код для этого, но я не уверен, как это сделать.
Вся помощь очень ценится. Спасибо!
EDIT: Размеры массива: inputBuffer [327680], lookupTable [16384], outputBuffer [1310720]
public byte[] ApplyLookupTableToBuffer(byte[] lookupTable, ushort[] inputBuffer)
{
System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
sw.Start();
// Precalculate and initialize the variables
int lookupTableLength = lookupTable.Length;
int bufferLength = inputBuffer.Length;
byte[] outputBuffer = new byte[bufferLength * 4];
int outIndex = 0;
int curPixelValue = 0;
// For each pixel in the input buffer...
for (int curPixel = 0; curPixel < bufferLength; curPixel++)
{
outIndex = curPixel * 4; // Calculate the corresponding index in the output buffer
curPixelValue = inputBuffer[curPixel] * 4; // Retrieve the pixel value and multiply by 4 since the lookup table has 4 values (blue/green/red/alpha) for each pixel value
// If the multiplied pixel value falls within the lookup table...
if ((curPixelValue + 3) < lookupTableLength)
{
// Copy the lookup table value associated with the value of the current input buffer location to the output buffer
outputBuffer[outIndex + 0] = lookupTable[curPixelValue + 0];
outputBuffer[outIndex + 1] = lookupTable[curPixelValue + 1];
outputBuffer[outIndex + 2] = lookupTable[curPixelValue + 2];
outputBuffer[outIndex + 3] = lookupTable[curPixelValue + 3];
//System.Buffer.BlockCopy(lookupTable, curPixelValue, outputBuffer, outIndex, 4); // Takes 2-10x longer than just copying the values manually
//Array.Copy(lookupTable, curPixelValue, outputBuffer, outIndex, 4); // Takes 2-10x longer than just copying the values manually
}
}
Debug.WriteLine("ApplyLookupTableToBuffer(ms): " + sw.Elapsed.TotalMilliseconds.ToString("N2"));
return outputBuffer;
}
EDIT: Я обновил метод, сохраняя одни и те же имена переменных, чтобы другие могли видеть, как код будет переводиться на основе решения HABJAN ниже.
public byte[] ApplyLookupTableToBufferV2(byte[] lookupTable, ushort[] inputBuffer)
{
System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
sw.Start();
// Precalculate and initialize the variables
int lookupTableLength = lookupTable.Length;
int bufferLength = inputBuffer.Length;
byte[] outputBuffer = new byte[bufferLength * 4];
//int outIndex = 0;
int curPixelValue = 0;
unsafe
{
fixed (byte* pointerToOutputBuffer = &outputBuffer[0])
fixed (byte* pointerToLookupTable = &lookupTable[0])
{
// Cast to integer pointers since groups of 4 bytes get copied at once
uint* lookupTablePointer = (uint*)pointerToLookupTable;
uint* outputBufferPointer = (uint*)pointerToOutputBuffer;
// For each pixel in the input buffer...
for (int curPixel = 0; curPixel < bufferLength; curPixel++)
{
// No need to multiply by 4 on the following 2 lines since the pointers are for integers, not bytes
// outIndex = curPixel; // This line is commented since we can use curPixel instead of outIndex
curPixelValue = inputBuffer[curPixel]; // Retrieve the pixel value
if ((curPixelValue + 3) < lookupTableLength)
{
outputBufferPointer[curPixel] = lookupTablePointer[curPixelValue];
}
}
}
}
Debug.WriteLine("2 ApplyLookupTableToBuffer(ms): " + sw.Elapsed.TotalMilliseconds.ToString("N2"));
return outputBuffer;
}
Ответы
Ответ 1
Я провел несколько тестов, и мне удалось достичь максимальной скорости, превратив мой код в небезопасную, используя API RtlMoveMemory
. Я понял, что Buffer.BlockCopy
и Array.Copy
были намного медленнее, чем прямое использование RtlMoveMemory
.
Итак, в конце вы получите что-то вроде этого:
fixed(byte* ptrOutput= &outputBufferBuffer[0])
{
MoveMemory(ptrOutput, ptrInput, 4);
}
[DllImport("Kernel32.dll", EntryPoint = "RtlMoveMemory", SetLastError = false)]
private static unsafe extern void MoveMemory(void* dest, void* src, int size);
EDIT:
Хорошо, теперь, когда я выяснил вашу логику, и когда я провел несколько тестов, мне удалось ускорить ваш метод почти на 50%. Поскольку вам нужно скопировать небольшие блоки данных (всегда 4 байта), да, вы были правы, RtlMoveMemory не поможет здесь, и лучше скопировать данные как целое. Вот окончательное решение, которое я придумал:
public static byte[] ApplyLookupTableToBufferV2(byte[] lookupTable, ushort[] inputBuffer)
{
int lookupTableLength = lookupTable.Length;
int bufferLength = inputBuffer.Length;
byte[] outputBuffer = new byte[bufferLength * 4];
int outIndex = 0, curPixelValue = 0;
unsafe
{
fixed (byte* ptrOutput = &outputBuffer[0])
fixed (byte* ptrLookup = &lookupTable[0])
{
uint* lkp = (uint*)ptrLookup;
uint* opt = (uint*)ptrOutput;
for (int index = 0; index < bufferLength; index++)
{
outIndex = index;
curPixelValue = inputBuffer[index];
if ((curPixelValue + 3) < lookupTableLength)
{
opt[outIndex] = lkp[curPixelValue];
}
}
}
}
return outputBuffer;
}
Я переименовал ваш метод в ApplyLookupTableToBufferV1.
И вот мой результат теста:
int tc1 = Environment.TickCount;
for (int i = 0; i < 200; i++)
{
byte[] a = ApplyLookupTableToBufferV1(lt, ib);
}
tc1 = Environment.TickCount - tc1;
Console.WriteLine("V1: " + tc1.ToString() + "ms");
Результат - V1: 998 мс
int tc2 = Environment.TickCount;
for (int i = 0; i < 200; i++)
{
byte[] a = ApplyLookupTableToBufferV2(lt, ib);
}
tc2 = Environment.TickCount - tc2;
Console.WriteLine("V2: " + tc2.ToString() + "ms");
Результат - V2: 473 мс