Ускорение алгоритма с помощью List‹T›.Sort и IEnumerable

Для моего проекта я сначала загружаю изображение из файла и помещаю каждый пиксель в 2D-массив pixels[,]. Затем я хочу проверить каждый пиксель и разделить их на «ячейки» в зависимости от того, как они окрашены, а затем отсортировать каждую ячейку. Итак, у меня есть объект Bin, который инкапсулирует List<Pixel>, и List<Bin>, содержащий (относительно небольшое) количество бинов.

Моя проблема в том, что когда я пытаюсь заполнить эти ячейки очень большими изображениями (например, 1920x1200 = 2,3 миллиона пикселей), используемый мной алгоритм работает медленнее, чем хотелось бы, и я проследил проблему до некоторого C#. специфические для языка функции, которые, кажется, занимают больше времени, чем я ожидал. Я хотел бы получить несколько советов о том, как лучше использовать C# для устранения этих узких мест.

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

public void fillBinsFromSource(IEnumerable<Pixel> source)
    {
        Stopwatch s = new Stopwatch();
        foreach (Pixel p in source)
        {
            s.Start();
            // algorithm removed for brevity
            // involves a binary search of all Bins and a List.Add call
            s.Stop();
        }           
    }

Для больших изображений ожидается, что мой алгоритм займет какое-то время, но когда я помещаю Секундомер вне вызова функции, оказывается, что это занимает примерно в два раза больше времени, начисленного на s, а это означает, что выполнение перечисления с использованием foreach является занимает половину времени этой функции (около 800 мс из 1,6 секунды для изображения 1920x1200).

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

class ImageList : IEnumerable<Pixel>
{
    private List<Image> imageList;
    public IEnumerator<Pixel> GetEnumerator()
    {
        foreach (Image i in imageList)
            foreach (Pixel p in i)
                yield return p;
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    } 

class Image : IEnumerable<Pixel>
{
    private Pixel[,] pixels; // all pixels in the image        
    private List<Pixel> selectedPixels;// all pixels in the user's selection

    public IEnumerator<Pixel> GetEnumerator()
    {
        if (selectedPixels == null)
            for (int i = 0; i < image.Width; i++)
                for (int j = 0; j < image.Height; j++)
                    yield return pixels[i, j];
        else
            for (int i = 0; i < selectedPixels.Count; i++)
                yield return selectedPixels[i];
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

Затем, наконец, я называю это

ImageList list; // pretend it contains only 1 image, and it's large
fillBinsFromSource(list);

Вопрос 1) Из-за необходимости перечисления как 2D-массива пикселей, так и выбранной области, в зависимости от того, что выбрал пользователь, перечисление просто очень медленное. Как я могу ускорить это?

Затем, заполнив все эти корзины Pixel объектами, я их сортирую. Я вызываю List<Pixel>.Sort() и полагаюсь на IComparable, вот так:

ImageList list; // pretend it contains only 1 image, and it's large
fillBinsFromSource(list);
foreach(Bin b in allBins)
    b.Sort(); // calls List<Pixel>.Sort()


class Pixel : IComparable
{
    // store both HSV and RGB
    float h, s, v;
    byte r, g, b;

    // we sort by HSV's value property
    public int CompareTo(object obj)
    {
        // this is much faster than calling CompareTo on a float
        Pixel rhs = obj as Pixel;
        if (v < rhs.v)
            return -1;
        else if (v > rhs.v)
            return 1;
        return 0;
    }

Вопрос 2) Предположим, что allBins имеет 7 элементов; сортировка 7 отдельных списков, содержащих в общей сложности 2,3 миллиона Pixels, занимает около 2 секунд. Сортировка одного списка из 2,3 миллиона случайных ints занимает менее 200 миллисекунд. Я могу оценить, что есть некоторый уровень ускорения при использовании примитивных типов, но действительно ли использование IComparable более чем в 10 раз медленнее? Есть ли здесь ускорение?

Прошу прощения за длинный вопрос, если у кого-то есть какие-либо советы для меня, я был бы признателен!


person XenoScholar    schedule 30.11.2012    source источник
comment
являются ли выбранные пиксели прямоугольной областью или произвольным несмежным набором?   -  person dthorpe    schedule 01.12.2012
comment
Произвольный и несмежный. Они используют инструменты выделения, которые я запрограммировал (рисование произвольной формы, прямоугольник и т. д.), чтобы выбрать 1 или несколько областей.   -  person XenoScholar    schedule 01.12.2012
comment
Одной из моих мыслей было определить делегат или какой-нибудь метод обратного вызова, а затем вместо перебора пикселей в fillBinsFromSource я бы просто выполнил базовый цикл в классе Image и использовал обратный вызов. Это кажется особенно (и излишне) грязным с точки зрения дизайна, но я бы вообще избегал использования каких-либо итераторов. Я хотел бы не запутать свой дизайн, если это возможно.   -  person XenoScholar    schedule 01.12.2012
comment
Имейте в виду, что при повторении перечисления, если перечисление начинается с выражения запроса (например, запроса Linq), выражение запроса будет выполняться для каждого элемента в каждой итерации. Если выражение запроса дорогое, это повлияет на итерацию. Вы можете избежать затрат на выражение, используя .ToList(), но тогда вы удвоите использование памяти. Для небольших списков это выгодно, но для 2,3 млн элементов использование памяти может быть непомерно высоким.   -  person dthorpe    schedule 01.12.2012
comment
Точно так же, если перечисление пикселей исходит из запроса, который извлекает пиксели из растрового изображения с помощью GetPixel(x,y), производительность итератора будет ограничиваться ужасно низкой производительностью GetPixel(x,y). Не используйте GetPixel. Всегда.   -  person dthorpe    schedule 01.12.2012
comment
Если сам итератор потребляет большую часть времени выполнения (это невозможно узнать, кроме как путем профилирования), рассмотрите возможность изменения перечисления с перечисления пикселей на перечисление строк развертки — групп смежных пикселей. Это должно уменьшить количество элементов в перечислении и количество итераций по крайней мере на порядок в большинстве ситуаций. Ваш основной цикл будет брать каждую строку сканирования из перечисления и запускать узкий цикл кода по смежным пикселям.   -  person dthorpe    schedule 01.12.2012
comment
Да, я никогда не использовал класс Bitmap до этого проекта и ошибочно предположил, что GetPixel реализован прилично. До недавнего внедрения исправления, включающего LockBits, простая загрузка изображения в исходный массив пикселей занимала около 5 секунд. К счастью, это было исправлено, и загрузка изображения не связана с приведенным выше кодом.   -  person XenoScholar    schedule 01.12.2012
comment
Я бы использовал for вместо foreach. В тестах производительности, которые я видел, foreach медленнее, чем во всех случаях, кроме одного, где он был почти таким же. Поскольку вы хотите отсортировать данные, используйте SortedDictionary (или, если вы часто обращаетесь к содержимому после заполнения, используйте SortedDictionary и преобразуйте его в SortedList после заполнения).   -  person Trisped    schedule 01.12.2012


Ответы (3)


Вам действительно нужно профилировать свой код и посмотреть, что работает медленно.

Самое очевидное:

  • Pixel не реализует общий IComparable<Pixel>, и поэтому Compare пришлось проделать гораздо больше работы.
  • Попробуйте сделать тип значения Pixel. Скорее всего, вы увидите падение производительности, а может и нет.
  • Рассмотрите возможность передачи 2d-диапазонов (прямоугольник) и доступа к Pixel объектам напрямую по индексу вместо итераторов, если вы обнаружите, что производительность ниже того, что вы считаете приемлемым. Итераторы хороши, но не бесплатны.
person Alexei Levenkov    schedule 30.11.2012
comment
Будучи новичком в C#, я на самом деле понятия не имел, что существует IComparable<T>. Это имеет смысл теперь, когда я посмотрел это, хотя. Изменение Pixel для реализации IComparable<T> ускорило сортировку примерно с 2 до 1,3 секунды. Это все еще примерно в 7 раз медленнее, чем сортировка int (мне все еще трудно поверить, что это цена использования IComparable), но это, безусловно, улучшение, так что спасибо! Я рассмотрю ваше предложение по типу значения. - person XenoScholar; 01.12.2012
comment
Опять же, я понятия не имел, что существует такая фундаментальная разница между struct и class в C#. Изменение Pixel на структуру (тип значения) вызывает у меня несколько проблем с некоторым другим кодом (который, по совпадению, будет исправлен с комментарием делегата к моему основному сообщению), но простое изменение Pixel на struct еще больше увеличило скорость, примерно до 1 секунды. Вместе с IComparable<Pixel> я вдвое сократил время сортировки! Есть ли у кого-нибудь дополнительные предложения по еще большему сокращению стоимости использования IComparable? - person XenoScholar; 01.12.2012
comment
@XenoScholar, примечание: обязательно найдите и прочитайте о различиях между структурами С# и классами (т.е. ссылки из stackoverflow .com/questions/9923256/class-vs-ref-struct) — типы значений часто демонстрируют неожиданное поведение, если вы не готовы. И избегайте создания изменяемого типа значения, чтобы сэкономить время на отладку :). - person Alexei Levenkov; 01.12.2012

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

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

Вам действительно нужно правильно понять самый внутренний цикл. Не выделяйте там, не вызывайте виртуальные функции (или методы интерфейса или делегаты).

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

Напротив, не имеет значения, как вы предоставляете поток изображений. Используйте LINQ там, где хотите. Это малообъемная операция, потому что на изображение приходится миллионы пикселей.

person usr    schedule 30.11.2012
comment
Или для непрямоугольных областей рассмотрите возможность группировки последовательностей смежных пикселей в строки сканирования вместо перечисления отдельных пикселей, разбросанных повсюду. - person dthorpe; 01.12.2012
comment
Я думаю, что собираюсь немного реструктурировать свои переменные. Причина, по которой я везде использую IEnumerable, заключается в том, что я храню пиксели в виде 2D-массива, а selectedPixels — в виде List‹T›. Вероятно, мне следует подумать о создании функций, которые преобразуют их в одномерные массивы, когда я хочу выполнить массивный цикл, а затем выполнить for(int i=0; i < image.IterableList.Length; i++) или что-то подобное. Вероятно, мне также следует пересмотреть, как я храню эти значения... возможно, можно хранить пиксели в виде массива 1D и предоставлять перегруженный индексатор 2D, если мне когда-либо понадобится доступ к 2D в неитерируемом фрагменте кода. - person XenoScholar; 01.12.2012
comment
Да, 2D-массивы медленнее, потому что они всегда выполняют проверку границ. Вы также можете переключиться на небезопасный код ядра алгоритма. Может и стоит. - person usr; 01.12.2012

Вместо использования итератора или даже создания массива/списка пикселей для начала вы можете использовать шаблон посетителя. Изображения, списки изображений и другие объекты, представляющие произвольные выборки, могут принимать класс посетителей с одним методом VisitPixel и вызывать этот метод для каждого пикселя, который представляет объект. Затем класс посетителя будет отвечать за объединение всех пикселей по мере их посещения.

Это может устранить необходимость извлекать все ваши пиксели в отдельный массив. Это также может исключить создание итераторов в пользу простых циклов for.

У Алексея Левенкова есть несколько хороших замечаний по вашему второму вопросу. Возможно, будет еще быстрее использовать Sort перегрузку, которая принимает экземпляр IComparer<T>.

person Michael Petito    schedule 30.11.2012