Пара наблюдений:
Убедитесь, что вы используете оптимизированную / выпускную сборку, а не неоптимизированную отладочную сборку. На моем устройстве отладочная сборка занимает около 4 секунд для обработки 12-мегапиксельного изображения, тогда как сборка выпуска занимает 0,3 секунды.
Если у вас есть цикл 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 }
}