Критический код производительности LockBits - PullRequest
3 голосов
/ 11 апреля 2009

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

    /// <summary>
    /// Copies bitmapdata from one bitmap to another at a specified point on the output bitmapdata
    /// </summary>
    /// <param name="sourcebtmpdata">The sourcebitmap must be smaller that the destbitmap</param>
    /// <param name="destbtmpdata"></param>
    /// <param name="point">The point on the destination bitmap to draw at</param>
    private static unsafe void CopyBitmapToDest(BitmapData sourcebtmpdata, BitmapData destbtmpdata, Point point)
    {
        // calculate total number of rows to draw.
        var totalRow = Math.Min(
            destbtmpdata.Height - point.Y,
            sourcebtmpdata.Height);


        //loop through each row on the source bitmap and get mem pointers
        //to the source bitmap and dest bitmap
        for (int i = 0; i < totalRow; i++)
        {
            int destRow = point.Y + i;

            //get the pointer to the start of the current pixel "row" on the output image
            byte* destRowPtr = (byte*)destbtmpdata.Scan0 + (destRow * destbtmpdata.Stride);
            //get the pointer to the start of the FIRST pixel row on the source image
            byte* srcRowPtr = (byte*)sourcebtmpdata.Scan0 + (i * sourcebtmpdata.Stride);

            int pointX = point.X;
            //the rowSize is pre-computed before the loop to improve performance
            int rowSize = Math.Min(destbtmpdata.Width - pointX, sourcebtmpdata.Width);
            //for each row each set each pixel
            for (int j = 0; j < rowSize; j++)
            {
                int firstBlueByte = ((pointX + j)*3);

                int srcByte = j *3;
                destRowPtr[(firstBlueByte)] = srcRowPtr[srcByte];
                destRowPtr[(firstBlueByte) + 1] = srcRowPtr[srcByte + 1];
                destRowPtr[(firstBlueByte) + 2] = srcRowPtr[srcByte + 2];
            }


        }
    }

Так есть ли что-нибудь, что можно сделать, чтобы сделать это быстрее? Пока проигнорируйте задачу, исправлю это позже, когда у меня будут какие-то базовые измерения производительности.

ОБНОВЛЕНИЕ: Извините, я должен был упомянуть, что причина, по которой я использую это вместо Graphics.DrawImage, заключается в том, что я реализую многопоточность и из-за этого я не могу использовать DrawImage.

ОБНОВЛЕНИЕ 2: Я все еще не удовлетворен производительностью, и я уверен, что есть еще несколько мс, которые можно иметь.

Ответы [ 10 ]

4 голосов
/ 13 мая 2009

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

byte* destRowPtr = (byte*)destbtmpdata.Scan0 + (destRow * destbtmpdata.Stride);

Получает указатель на строку назначения, но не получает столбец, в который он копирует, что в старом коде выполняется внутри цикла rowSize. Теперь это выглядит так:

byte* destRowPtr = (byte*)destbtmpdata.Scan0 + (destRow * destbtmpdata.Stride) + pointX * 3;

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

        private static unsafe void CopyBitmapToDestSuperFast(BitmapData sourcebtmpdata, BitmapData destbtmpdata, Point point)
    {
        //calculate total number of rows to copy.
        //using ternary operator instead of Math.Min, few ms faster
        int totalRows = (destbtmpdata.Height - point.Y < sourcebtmpdata.Height) ? destbtmpdata.Height - point.Y : sourcebtmpdata.Height;
        //calculate the width of the image to draw, this cuts off the image
        //if it goes past the width of the destination image
        int rowWidth = (destbtmpdata.Width - point.X < sourcebtmpdata.Width) ? destbtmpdata.Width - point.X : sourcebtmpdata.Width;

        //loop through each row on the source bitmap and get mem pointers
        //to the source bitmap and dest bitmap
        for (int i = 0; i < totalRows; i++)
        {
            int destRow = point.Y + i;

            //get the pointer to the start of the current pixel "row" and column on the output image
            byte* destRowPtr = (byte*)destbtmpdata.Scan0 + (destRow * destbtmpdata.Stride) + point.X * 3;

            //get the pointer to the start of the FIRST pixel row on the source image
            byte* srcRowPtr = (byte*)sourcebtmpdata.Scan0 + (i * sourcebtmpdata.Stride);

            //RtlMoveMemory function
            CopyMemory(new IntPtr(destRowPtr), new IntPtr(srcRowPtr), (uint)rowWidth * 3);

        }
    }

Копирование изображения 500x500 в изображение 5000x5000 в сетке 50 раз заняло: 00: 00: 07.9948993 с. Теперь с изменениями выше это занимает 00: 00: 01.8714263 сек. Намного лучше.

2 голосов
/ 11 апреля 2009

Ну ... Я не уверен, что форматы растровых данных .NET полностью совместимы с функциями Windows GDI32 ...

Но одним из первых немногих Win32 API, который я узнал, был BitBlt:

BOOL BitBlt(
  HDC hdcDest, 
  int nXDest, 
  int nYDest, 
  int nWidth, 
  int nHeight, 
  HDC hdcSrc, 
  int nXSrc, 
  int nYSrc, 
  DWORD dwRop
);

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

Вот подпись BitBlt PInvoke для использования в C # и связанная информация об использовании, отличное чтение для любого, кто работает с высокопроизводительной графикой в ​​C #:

Определенно стоит посмотреть.

1 голос
/ 13 мая 2009

К сожалению, у меня нет времени, чтобы написать полное решение, но я хотел бы изучить использование платформы RtlMoveMemory () для перемещения строк в целом, а не побайтово. Это должно быть намного быстрее.

1 голос
/ 10 мая 2009

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

        private static void CopyBitmapToDest(BitmapData sourcebtmpdata, BitmapData destbtmpdata, Point point)
    {
        byte[] src = new byte[sourcebtmpdata.Height * sourcebtmpdata.Width * 3];
        int maximum = src.Length;
        byte[] dest = new byte[maximum];
        Marshal.Copy(sourcebtmpdata.Scan0, src, 0, src.Length);
        int pointX = point.X * 3;
        int copyLength = destbtmpdata.Width*3 - pointX;
        int k = pointX + point.Y * sourcebtmpdata.Stride;
        int rowWidth = sourcebtmpdata.Stride;
        while (k<maximum)
        {
            Array.Copy(src,k,dest,k,copyLength);
            k += rowWidth;

        }
        Marshal.Copy(dest, 0, destbtmpdata.Scan0, dest.Length);
    }
1 голос
/ 09 мая 2009

Возможно, вы захотите посмотреть на Eigen .

Это библиотека шаблонов C ++, которая использует SSE (2 и более поздние версии) и наборы команд AltiVec с постепенным отступлением от не векторизованного кода .

Быстро. (См. Эталонный тест).
Шаблоны выражений позволяют разумно удалять временные значения и включать ленивую оценку, когда это уместно - Eigen позаботится об этом автоматически и в большинстве случаев также обрабатывает псевдонимы.
Явная векторизация выполняется для наборов команд SSE (2 и более поздних) и AltiVec с постепенным отступлением от не векторизованного кода. Шаблоны выражений позволяют выполнять эти оптимизации глобально для целых выражений.
В случае объектов фиксированного размера динамическое выделение памяти исключается, и циклы развертываются, когда это имеет смысл.
Для больших матриц особое внимание уделяется кешированию.

Вы можете реализовать свою функцию в C ++ и затем вызывать ее из C #

1 голос
/ 12 апреля 2009

Внутренний цикл - это то место, где вы хотите сконцентрировать много времени (но, чтобы убедиться, делайте измерения)

for  (int j = 0; j < sourcebtmpdata.Width; j++)
{
    destRowPtr[(point.X + j) * 3] = srcRowPtr[j * 3];
    destRowPtr[((point.X + j) * 3) + 1] = srcRowPtr[(j * 3) + 1];
    destRowPtr[((point.X + j) * 3) + 2] = srcRowPtr[(j * 3) + 2];
}
  1. Избавьтесь от умножений и индексации массива (которая является умножением под капотами) и замените указатель, который вы увеличиваете.

  2. То же самое с +1, +2, увеличить указатель.

  3. Возможно, ваш компилятор не будет сохранять точку вычисления .X (проверка), но на всякий случай создаст локальную переменную. Он не будет делать это за одну итерацию, но может делать каждую итерацию.

0 голосов
/ 13 мая 2009

Хорошо, это будет довольно близко к линии того, сколько мс вы можете получить из алгоритма, но избавьтесь от вызова Math.Min и замените его триным оператором. вместо этого.

Как правило, выполнение библиотечного вызова займет больше времени, чем выполнение чего-либо самостоятельно, и я сделал простой тестовый драйвер для подтверждения этого для Math.Min.

using System;
using System.Diagnostics;

namespace TestDriver
{
    class Program
    {
        static void Main(string[] args)
        {
            // Start the stopwatch
            if (Stopwatch.IsHighResolution)
            { Console.WriteLine("Using high resolution timer"); }
            else
            { Console.WriteLine("High resolution timer unavailable"); }
            // Test Math.Min for 10000 iterations
            Stopwatch sw = Stopwatch.StartNew();
            for (int ndx = 0; ndx < 10000; ndx++)
            {
                int result = Math.Min(ndx, 5000);
            }
            Console.WriteLine(sw.Elapsed.TotalMilliseconds.ToString("0.0000"));
            // Test trinary operator for 10000 iterations
            sw = Stopwatch.StartNew();
            for (int ndx = 0; ndx < 10000; ndx++)
            {
                int result = (ndx < 5000) ? ndx : 5000;
            }
            Console.WriteLine(sw.Elapsed.TotalMilliseconds.ToString("0.0000"));
            Console.ReadKey();
        }
    }
}

Результаты при запуске вышеупомянутого на моем компьютере Intel T2400 с частотой 1,83 ГГц. Кроме того, обратите внимание, что результаты немного отличаются друг от друга, но, как правило, оператор trinay работает быстрее примерно на 0,01 мс. Это не так много, но на достаточно большом наборе данных он будет складываться.

Использование таймера высокого разрешения
0,0539
0,0402

0 голосов
/ 11 мая 2009

Я не уверен, что это даст дополнительную производительность, но я часто вижу шаблон в Reflector.

Итак:

int srcByte = j *3;
destRowPtr[(firstBlueByte)] = srcRowPtr[srcByte];
destRowPtr[(firstBlueByte) + 1] = srcRowPtr[srcByte + 1];
destRowPtr[(firstBlueByte) + 2] = srcRowPtr[srcByte + 2];

становится:

*destRowPtr++ = *srcRowPtr++;
*destRowPtr++ = *srcRowPtr++;
*destRowPtr++ = *srcRowPtr++;

Вероятно, нужно больше скобок.

Если ширина фиксирована, вы можете развернуть всю строку на несколько сотен строк. :)

Обновление

Вы также можете попробовать использовать больший тип, например Int32 или Int64, для лучшей производительности.

0 голосов
/ 10 мая 2009

Я смотрю на ваш код C # и не могу распознать ничего знакомого. Все это выглядит как тонна C ++. Кстати, похоже, что DirectX / XNA должен стать вашим новым другом. Просто мои 2 цента. Не убивай посланника.

Если вы должны полагаться на процессор для этого: я сам провел некоторые 24-битные оптимизации компоновки и могу сказать, что скорость доступа к памяти должна быть вашим узким местом. Используйте инструкции SSE3 для максимально быстрого побайтного доступа. Это означает C ++ и встроенный ассемблер. В чистом C вы будете на 30% медленнее на большинстве машин.

Имейте в виду, что современные графические процессоры НАМНОГО быстрее, чем ЦП, в таких операциях.

0 голосов
/ 11 апреля 2009

Я думаю, что размер шага и ограничения на количество строк можно рассчитать заранее.

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

private static unsafe void CopyBitmapToDest(BitmapData sourcebtmpdata, BitmapData destbtmpdata, Point point)
{
    //TODO: It is expected that the bitmap PixelFormat is Format24bppRgb but this could change in the future
    const int pixelSize = 3;

    // calculate total number of rows to draw.
    var totalRow = Math.Min(
        destbtmpdata.Height - point.Y,
        sourcebtmpdata.Height);

    var rowSize = Math.Min(
        (destbtmpdata.Width - point.X) * pixelSize,
        sourcebtmpdata.Width * pixelSize);

    // starting point of copy operation
    byte* srcPtr = (byte*)sourcebtmpdata.Scan0;
    byte* destPtr = (byte*)destbtmpdata.Scan0 + point.Y * destbtmpdata.Stride;

    // loop through each row
    for (int i = 0; i < totalRow; i++) {

        // draw the entire row
        for (int j = 0; j < rowSize; j++)
            destPtr[point.X + j] = srcPtr[j];

        // advance each pointer by 1 row
        destPtr += destbtmpdata.Stride;
        srcPtr += sourcebtmpdata.Stride;
    }

}

Тщательно не проверил, но у вас должно получиться, что это сработает.

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

Дайте мне знать, если это поможет: -)

...