Ускорьте добавление матрицы в С#

Я бы хотел оптимизировать этот фрагмент кода:

public void PopulatePixelValueMatrices(GenericImage image,int Width, int Height)
{            
        for (int x = 0; x < Width; x++)
        {
            for (int y = 0; y < Height; y++)
            {
                Byte  pixelValue = image.GetPixel(x, y).B;
                this.sumOfPixelValues[x, y] += pixelValue;
                this.sumOfPixelValuesSquared[x, y] += pixelValue * pixelValue;
            }
        }
}

Это будет использоваться для обработки изображений, и в настоящее время мы запускаем это для примерно 200 изображений. Мы оптимизировали значение GetPixel для использования небезопасного кода, и мы не используем image.Width или image.Height, поскольку эти свойства добавляли к нашим затратам времени выполнения.

Однако мы все еще застряли на низкой скорости. Проблема в том, что наши изображения 640x480, поэтому середина цикла называется около 640x480x200 раз. Я хотел бы спросить, есть ли способ ускорить это, или убедить меня, что это достаточно быстро, как есть. Возможно, способ заключается в быстром Матричном добавлении или Матрицевом добавлении, по сути, операции n ^ 2 без возможности ускорить его?

Возможно, использование доступа к массиву с помощью небезопасного кода ускорит его, но я не уверен, как это сделать, и стоит ли времени. Возможно нет. Спасибо.

EDIT: Спасибо за все ваши ответы.

Это метод GetPixel, который мы используем:

 public Color GetPixel(int x, int y)
    {
        int offsetFromOrigin = (y * this.stride) + (x * 3);
        unsafe
        {
            return Color.FromArgb(this.imagePtr[offsetFromOrigin + 2], this.imagePtr[offsetFromOrigin + 1], this.imagePtr[offsetFromOrigin]);
        }
    }

Ответ 1

Несмотря на использование небезопасного кода, GetPixel вполне может быть узким местом здесь. Вы рассматривали способы получения всех пикселей в изображении за один вызов, а не за один пиксель? Например, Bitmap.LockBits может быть вашим другом...

На моем нетбуке очень простая петля, повторяющая 640 * 480 * 200 раз, занимает всего около 100 миллисекунд, поэтому, если вы все пойдете медленно, вы должны еще раз взглянуть на бит внутри цикла.

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

В частности, вы можете иметь одномерный массив размером Width * Height и просто хранить индекс:

int index = 0;
for (int x = 0; x < Width; x++)
{
    for (int y = 0; y < Height; y++)
    {
        Byte pixelValue = image.GetPixel(x, y).B;
        this.sumOfPixelValues[index] += pixelValue;
        this.sumOfPixelValuesSquared[index] += pixelValue * pixelValue;
        index++;
    }
}

Используя тот же простой тестовый жгут, добавив запись в двумерный прямоугольный массив, общее время цикла составляло 200 * 640 * 480 до около 850 мс; используя одномерный прямоугольный массив, он уменьшил его до примерно 340 мс, поэтому он несколько значителен, и в настоящее время у вас есть две из этих циклов на итерации цикла.

Ответ 2

Прочитайте эту статью, в которой также есть код и упоминается о медленности GetPixel.

текст ссылки

Из статьи это код, который просто инвертирует биты. Это также показывает использование LockBits.

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

public static bool Invert(Bitmap b)
{

BitmapData bmData = b.LockBits(new Rectangle(0, 0, b.Width, b.Height), 
                               ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb); 

int stride = bmData.Stride; 
System.IntPtr Scan0 = bmData.Scan0; 
unsafe 
{ 
    byte * p = (byte *)(void *)Scan0;
    int nOffset = stride - b.Width*3; 
    int nWidth = b.Width * 3;
    for(int y=0;y < b.Height;++y)
    {
        for(int x=0; x < nWidth; ++x )
        {
            p[0] = (byte)(255-p[0]);
            ++p;
        }
        p += nOffset;
    }
}

b.UnlockBits(bmData);

return true;

}

Ответ 3

Я рекомендую вам профилировать этот код и узнать, какое максимальное время занимает.

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

long sumOfPixelValues[n,m];
long sumOfPixelValuesSquared[n,m];

к

struct Sums
{
    long sumOfPixelValues;
    long sumOfPixelValuesSquared;
}

Sums sums[n,m];

Это будет зависеть от того, что вы найдете, когда вы просматриваете код.

Ответ 4

Профилирование кода - лучшее место для начала.

Матричное добавление является высокопараллельной операцией и может ускоряться путем параллелизации операции с несколькими потоками.

Я бы рекомендовал использовать библиотеку Intels IPP, которая содержит многопоточно оптимизированный API для такого рода операций. Возможно, на удивление это всего лишь около 100 долларов, но добавит значительную сложность вашему проекту.

Если вы не хотите беспокоиться о смешанном программировании на языке и IPP, вы можете попробовать библиотеки centerpace С# math. API NMath содержит простые в использовании, прямое масштабирование, операции с матрицами.

Пол

Ответ 5

System.Drawing.Color - это структура, которая в текущих версиях .NET убивает большинство оптимизаций. Так как вы все равно интересуетесь голубым компонентом, используйте метод, который только получает нужные вам данные.

public byte GetPixelBlue(int x, int y)
{
    int offsetFromOrigin = (y * this.stride) + (x * 3);
    unsafe
    {
        return this.imagePtr[offsetFromOrigin];
    }
}

Теперь замените порядок итераций x и y:

public void PopulatePixelValueMatrices(GenericImage image,int Width, int Height)
{            
    for (int y = 0; y < Height; y++)
    {
        for (int x = 0; x < Width; x++)
        {
            Byte  pixelValue = image.GetPixelBlue(x, y);
            this.sumOfPixelValues[y, x] += pixelValue;
            this.sumOfPixelValuesSquared[y, x] += pixelValue * pixelValue;
        }
    }
}

Теперь вы получаете доступ ко всем значениям в строке сканирования последовательно, что значительно улучшит использование кэша ЦП для всех трех задействованных матриц (image.imagePtr, sumOfPixelValues ​​и sumOfPixelValuesSquared. [Спасибо Джону, заметив это, когда я исправил доступ к image.imagePtr, я сломал два других. Теперь индексирование выходного массива заменяется, чтобы поддерживать его оптимальным.]

Далее, избавьтесь от ссылок на элементы. Другой поток теоретически может устанавливать sumOfPixelValues ​​в другой массив на полпути, что делает ужасные ужасные вещи для оптимизации.

public void PopulatePixelValueMatrices(GenericImage image,int Width, int Height)
{          
    uint [,] sums = this.sumOfPixelValues;
    ulong [,] squares = this.sumOfPixelValuesSquared;
    for (int y = 0; y < Height; y++)
    {
        for (int x = 0; x < Width; x++)
        {
            Byte  pixelValue = image.GetPixelBlue(x, y);
            sums[y, x] += pixelValue;
            squares[y, x] += pixelValue * pixelValue;
        }
    }
}

Теперь компилятор может генерировать оптимальный код для перемещения по двум выходным массивам, а после встраивания и оптимизации внутренний цикл может проходить через массив image.imagePtr с шагом 3 вместо пересчета смещения все время. Теперь небезопасная версия для хорошей меры, делая оптимизации, которые, как мне кажется, должны быть достаточно умными, но, вероятно, это не так:

unsafe public void PopulatePixelValueMatrices(GenericImage image,int Width, int Height)
{          
    byte* scanline = image.imagePtr;
    fixed (uint* sums = &this.sumOfPixelValues[0,0])
    fixed (uint* squared = &this.sumOfPixelValuesSquared[0,0])
    for (int y = 0; y < Height; y++)
    {
        byte* blue = scanline;
        for (int x = 0; x < Width; x++)
        {
            byte pixelValue = *blue;
            *sums += pixelValue;
            *squares += pixelValue * pixelValue;
            blue += 3;
            sums++;
            squares++;
        }
        scanline += image.stride;
    }
}

Ответ 6

Где хранятся изображения? Если каждый из них находится на диске, тогда может потребоваться некоторая часть времени обработки, связанного с извлечением их с диска. Вы можете проверить это, чтобы увидеть, если это проблема, и если да, то перепишите для предварительной выборки данных изображения, чтобы код обработки массива не дождался данных...

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

Ответ 7

Единственный возможный способ, с помощью которого я могу ускорить его, - попытаться выполнить некоторые из дополнений параллельно, что с вашим размером может быть полезным для накладных расходов.

Ответ 8

Добавление матрицы - это, конечно, операция n ^ 2, но вы можете ускорить ее, используя небезопасный код или, по крайней мере, используя неровные массивы вместо многомерных.

Ответ 9

О единственном способе эффективного ускорения умножения на матрицу следует использовать правильный алгоритм. Существуют более эффективные способы ускорения матричного умножения. Посмотрите на Stressen и Coopersmith Winograd. Также отмечается [с предыдущими ответами], что вы можете парализовать код, что очень помогает.

Ответ 10

Я не уверен, если это быстрее, но вы можете написать что-то вроде:

public void PopulatePixelValueMatrices(GenericImage image,int Width, int Height)
{            
        Byte pixelValue;
        for (int x = 0; x < Width; x++)
        {
            for (int y = 0; y < Height; y++)
            {
                pixelValue = image.GetPixel(x, y).B;
                this.sumOfPixelValues[x, y] += pixelValue;
                this.sumOfPixelValuesSquared[x, y] += pixelValue * pixelValue;
            }
        }
}

Ответ 11

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

  • Можете ли вы асинхронно предварительно загружать изображение [n + 1] во время обработки изображения [n]?
  • Вы можете загружать только канал B из изображения? Это уменьшит пропускную способность памяти?
  • Можете ли вы загрузить значение B и сразу обновить массивы sumOfPixelValues ​​(Squared), т.е. прочитать файл и обновить вместо чтения файла, сохранения, чтения, обновления? Опять же, это уменьшает пропускную способность памяти.
  • Можете ли вы использовать одномерные массивы вместо двухмерных? Возможно, создайте свой собственный класс массивов, который работает в любом случае.
  • Возможно, вы могли бы изучить использование Mono и SIMD-расширений?
  • Можете ли вы обработать изображение в кусках и назначить их на незанятые процессоры в среде с несколькими процессорами?

EDIT:

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

public Color GetBPixel (int x, int y)
{
    int offsetFromOrigin = (y * this.stride) + (x * 3);
    unsafe
    {
        return this.imagePtr [offsetFromOrigin + 1];
    }
}

или, еще лучше:

public Color GetBPixel (int offset)
{
    unsafe
    {
        return this.imagePtr [offset + 1];
    }
}

и используйте приведенное выше в цикле, например:

for (int start_offset = 0, y = 0 ; y < Height ; start_offset += stride, ++y)
{
   for (int x = 0, offset = start_offset ; x < Width ; offset += 3, ++x)
   {
      pixel = GetBPixel (offset);
      // do stuff
   }
}

Ответ 12

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

Если вы хотите выполнять более сложные операции, вам нужно использовать высоко оптимизированную математическую библиотеку, например NMath.Net, которая использует собственный код, а не .net.

Ответ 13

Иногда делать вещи на родном С#, даже небезопасные вызовы, происходит только медленнее, чем с использованием уже оптимизированных методов.

Никаких результатов не гарантировано, но вы можете исследовать пространство имен System.Windows.Media.Imaging и посмотреть на всю вашу проблему по-другому.

Ответ 14

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

Byte  pixelValue = image.GetPixel(x, y).B;

Ясно, что если pixelValue = 0, то нет причин делать так, чтобы ваша программа могла стать

public void PopulatePixelValueMatrices(GenericImage image,int Width, int Height)
  {
  for (int x = 0; x < Width; x++)
    {
    for (int y = 0; y < Height; y++)
      {
       Byte  pixelValue = image.GetPixel(x, y).B;

       if(pixelValue != 0)
         {
         this.sumOfPixelValues[x, y] += pixelValue;
         this.sumOfPixelValuesSquared[x, y] += pixelValue * pixelValue;
         }}}}

Однако вопрос заключается в том, как часто вы будете видеть pixelValue = 0 и будет ли сохранение в вычислении и хранении компенсировать стоимость теста.

Ответ 15

сложность сложения матрицы O(n^2), в количестве дополнений.

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

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