Оптимизация вложенных циклов в Swift - PullRequest
2 голосов
/ 22 октября 2019

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

func whitePixelCount() -> Int {
    let width = Int(image.size.width)
    let height = Int(image.size.height)
    var counter = 0
    for x in 0..<(width*scale) {
        for y in 0..<(height*scale) {
            // We multiply per 4 because of the 4 channels, RGBA, but later we just use the Alpha
            let pixelIndex = (width * y + x) * 4

            if pointer[pixelIndex + Component.alpha.rawValue] == 255 {
                counter += 1
            }
        }
    }
    return counter
}
  • Component.alpha.rawValue равно 3
  • scale равно Int(image.scale)
  • pointer приходитот:

    guard let cfdata = self.image.cgImage?.dataProvider?.data,
        let pointer = CFDataGetBytePtr(cfdata) else {
            return nil
    }
    

Ответы [ 2 ]

1 голос
/ 23 октября 2019

Пара наблюдений:

  1. Убедитесь, что вы используете оптимизированную / выпускную сборку, а не неоптимизированную отладочную сборку. На моем устройстве отладочная сборка занимает около 4 секунд для обработки 12-мегапиксельного изображения, тогда как сборка выпуска занимает 0,3 секунды.

  2. Если у вас есть цикл for, вы можете распараллелитьэто использовать все ядра на процессоре. Делая это с помощью алгоритма шага, цикл for был почти в 4 раза быстрее.

    Звучит замечательно, но, к сожалению, проблема в том, что для обработки изображения требовалось 0,3 секунды, большая часть этого былаподготовка буфера изображения. (Теперь, в вашем примере, вы не перерисовываете его в предопределенный пиксельный буфер, что немного опасно для ИМХО, поэтому, возможно, у вас нет этих накладных расходов. Но, несмотря на это, разница в 10+ мс обычно не наблюдаетсяесли вы не обрабатываете сотни изображений.) Фактический цикл for составлял только 16 мсек прошедшего времени. Таким образом, сокращение до 4 мс почти в 4 раза быстрее, но с точки зрения пользователя, это несущественно.

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


Один очень простой подход к улучшению производительности цикла for заключается в использовании concurrentPerform для распараллеливания процедуры:

Например, здесь не распараллеленорутина:

var total = 0

for x in 0..<maxX {
    for y in 0..<maxY {
        if ... {
            total += 1
        }
    }
}

print(total)

Вы можете распараллелить ее с помощью

  • Отражая циклы x и y, потому что мы хотим, чтобы внешний цикл был строкой визображение. Идея состоит в том, чтобы гарантировать, что каждый поток должен работать не только с непрерывными блоками памяти, но мы хотим минимизировать количество перекрытий, чтобы избежать «кэш-памяти». Итак, рассмотрим:

    for y in 0..<maxY {
        for x in 0..<maxX {
            if ... {
                total += 1
            }
        }
    }
    

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

  • заменавнешний цикл for (теперь y координата) с concurrentPerform:

    var total = 0
    
    let syncQueue = DispatchQueue(label: "...")
    
    DispatchQueue.concurrentPerform(iterations: maxY) { y in
        var subTotal = 0
        for x in 0..<maxX {
            if ... {
                subTotal += 1
            }
        }
        syncQueue.sync {
            total += subTotal
        }
    }
    
    print(total)
    

Итак, идея такова:

  • заменитьвнешний for цикл с concurrentPerform;
  • вместо попытки обновления total для каждой итерации x, с переменной subTotal для каждого потока и только обновлением total в конце (минимизация конкуренции между несколькими потоками за этот общий ресурс);и
  • использовать некоторый механизм синхронизации (я использовал здесь последовательную очередь, но любой механизм синхронизации будет работать), чтобы обновить total для обеспечения безопасности потока.

Я былпытаясь сделать пример как можно более простым, но есть и другие способы оптимизации, которые можно сделать:

  • Различные методы синхронизации обеспечивают разную производительность. Например, вы можете использовать NSLock (что по общему мнению медленнее, но мои недавние тесты показывают, что производительность может быть лучше, чем GCD во многих сценариях) путем определения метода sync в расширении протокола (чтобы получить хороший, безопасныйспособ использования блокировок), например, так:

    // Adapted from Apple’s `withCriticalSection` code sample
    
    extension NSLocking {
        func sync<T>(_ closure: () throws -> T) rethrows -> T {
            lock()
            defer { unlock() }
            return try closure()
        }
    }
    

    Затем вы можете сделать что-то вроде:

    let lock = NSLock()
    
    DispatchQueue.concurrentPerform(iterations: maxY) { y in
        var subTotal = 0
        for x in 0..<maxX {
            if ... {
                subTotal += 1
            }
        }
        lock.sync {
            total += subTotal
        }
    }
    
    print(total)
    

    Не стесняйтесь попробовать любые механизмы синхронизации, которые вы хотите. Но идея заключается в том, что если вы собираетесь обращаться к total из нескольких потоков, убедитесь, что вы делаете это в поточно-ориентированном режиме. Временно включите «Средство очистки нитей», если хотите проверить безопасность потоков.

  • Если недостаточно работы с каждым потоком (например, maxX не очень большой или,поскольку в этом случае алгоритм работает очень быстро), издержки распараллеленной подпрограммы могут начать компенсировать преимущества привлечения нескольких ядер к расчету. Таким образом, вы можете «шагать» по нескольким строкам y в каждой итерации. Например:

    let lock = NSLock()
    
    let stride = maxY / 20
    let iterations = Int((Double(height) / Double(stride)).rounded(.up))
    
    DispatchQueue.concurrentPerform(iterations: iterations) { i in
        var subTotal = 0
        let range = i * stride ..< min(maxY, (i + 1) * stride)
        for y in range {
            for x in 0 ..< maxX {
                if ... {
                    subTotal += 1
                }
            }
        }
    
        lock.sync { count += subTotal }
    }
    
0 голосов
/ 22 октября 2019

Как правило, производительность big (o) можно увеличить, заменив циклы for на цикл while, который говорит, что при x

другойподход состоит в том, чтобы разделить ваше изображение на отдельные компоненты и отправить их в разные потоки и объединить полученные массивы. Для этого вы захотите использовать gcd * workitems async.

...