3 votes

Optimisation des boucles for imbriquées en Swift

J'ai obtenu cette méthode qui compte les pixels blancs dans un UIImage Je dois parcourir tous les pixels pour augmenter le compteur à chaque pixel blanc que je trouve. J'essaie d'améliorer les performances de ce système mais je ne trouve pas de meilleure approche. Avez-vous des idées ?

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 est égal à 3
  • scale es Int(image.scale)
  • pointer vient de :

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

3voto

Rob Points 70987

Quelques observations :

  1. 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.

  2. 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 }
    }

-1voto

Revanth Matha Points 21

En général, les performances de big(o) peuvent être améliorées en remplaçant vos boucles for par une boucle while qui dit while x < array.count && y < array2.count { insérer le code à faire ici }.

Une autre approche consiste à diviser votre image en composants distincts, à les envoyer à différents threads et à recombiner les tableaux résultants. Vous voudrez utiliser gcd * workitems async pour faire cela.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X