Quelques observations :
-
Assurez-vous que vous utilisez une version optimisée/version, et non une version de débogage non optimisée. Sur mon appareil, une version de débogage prend environ 4 secondes pour traiter une image de 12 mégapixels, alors que la version de publication prend 0,3 seconde.
-
Lorsque vous avez un for
vous pouvez la paralléliser pour exploiter tous les cœurs du processeur. En procédant de la sorte avec l'algorithme striding, l'algorithme for
La boucle était presque 4 fois plus rapide.
Cela semble génial, mais malheureusement, le problème est que sur les 0,3 secondes nécessaires au traitement de l'image, la plus grande partie est consacrée à la préparation du tampon d'image. (Dans votre exemple, vous ne restituez pas l'image dans un tampon de pixels prédéfini, ce qui est un peu dangereux à mon avis, donc peut-être que vous n'avez pas cette surcharge. Mais, quoi qu'il en soit, la différence de 10+ msec n'est généralement pas observable à moins que vous ne traitiez des centaines d'images). Le temps réel for
La boucle ne représentait que 16 msec du temps écoulé. Ainsi, réduire ce temps à 4 msec est presque 4 fois plus rapide, mais du point de vue de l'utilisateur, c'est sans importance.
De toute façon, n'hésitez pas à voir l'algorithme parallèle avec striding ci-dessous, dans ma réponse originale.
Une approche très simple pour améliorer for
La performance de la boucle consiste à utiliser concurrentPerform
pour paralléliser la routine :
Par exemple, voici une routine non parallélisée :
var total = 0
for x in 0..<maxX {
for y in 0..<maxY {
if ... {
total += 1
}
}
}
print(total)
Vous pouvez le paralléliser en
-
Retourner le x
y y
car nous voulons que la boucle extérieure soit une ligne de l'image. L'idée est de s'assurer que non seulement chaque thread travaille avec des blocs de mémoire contigus, mais aussi de minimiser la quantité de chevauchement pour éviter le "sloshing" du cache. Considérez donc :
for y in 0..<maxY {
for x in 0..<maxX {
if ... {
total += 1
}
}
}
Nous n'allons pas réellement utiliser ce qui précède, mais nous l'utiliserons comme modèle à l'étape suivante ;
-
remplacement de l'extérieur for
(maintenant la y
coordonnées) avec 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)
Donc, l'idée est :
- remplacer l'extérieur
for
boucle avec concurrentPerform
;
- plutôt que d'essayer de mettre à jour
total
pour chaque itération de x
ont un subTotal
pour chaque thread et seulement mettre à jour total
à la fin (minimisant la contention de plusieurs threads pour cette ressource partagée) ; et
- utiliser un mécanisme de synchronisation (j'ai utilisé une file d'attente sérielle ici, mais n'importe quel mécanisme de synchronisation fonctionnera) pour mettre à jour
total
pour assurer la sécurité des fils.
J'ai essayé de garder l'exemple aussi simple que possible, mais il existe d'autres optimisations possibles :
-
Les différentes techniques de synchronisation offrent des performances différentes. Par exemple, vous pouvez utiliser NSLock
(qui, selon la sagesse conventionnelle, est plus lent, mais mes récents tests suggèrent que les performances peuvent être meilleures que celles de GCD dans de nombreux scénarios) en définissant un fichier sync
dans une extension de protocole (pour offrir une manière agréable et sûre d'utiliser les verrous) comme suit :
// Adapted from Apple’s `withCriticalSection` code sample
extension NSLocking {
func sync<T>(_ closure: () throws -> T) rethrows -> T {
lock()
defer { unlock() }
return try closure()
}
}
Alors vous pouvez faire quelque chose comme :
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)
N'hésitez pas à essayer les mécanismes de synchronisation que vous voulez. Mais l'idée est que si vous allez accéder à total
à partir de plusieurs threads, assurez-vous de le faire d'une manière sûre pour les threads. Activez temporairement le "Thread Sanitizer" si vous souhaitez vérifier la sécurité de vos fils.
-
S'il n'y a pas assez de travail sur chaque fil (par ex. maxX
n'est pas très grande ou, comme dans ce cas, l'algorithme est très rapide), la surcharge de la routine parallélisée peut commencer à compenser les avantages de l'implication de plusieurs cœurs dans le calcul. Ainsi, vous pouvez "parcourir" plusieurs lignes de y
à chaque itération. Par exemple :
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 }
}