优化 Swift 中的嵌套 for 循环

Posted

技术标签:

【中文标题】优化 Swift 中的嵌套 for 循环【英文标题】:Optimizing nested for loops in Swift 【发布时间】:2019-10-22 20:07:45 【问题描述】:

我得到了这种计算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 scaleInt(image.scale)

pointer来自:

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

【问题讨论】:

提供更多关于性能的细节。您如何对此进行基准测试?您在哪些优化标志下进行基准测试?您的测试套件中有哪些图像? 但原则上,这只能在 CPU 上运行时变得如此之快。如果您希望此代码具有高性能,您可以尝试将其提升到 GPU,但要做到这一点,您需要提供 很多 更多关于您正在尝试做什么的信息计算白色像素。 请参阅***.com/a/31661519/1271826,了解如何将图像渲染到像素缓冲区。这是改编自 Apple 的 Technical Q&A 1509,这是处理像素缓冲区的旧 Objective-C 示例。代码 sn-p 已过时,但一般原则仍然适用。 或者,当您说“白色像素”时,您的意思是“任何颜色的不透明像素”? 我想做的是知道黑白图像中白色像素的确切数量,对于我收到的每张图像,我会连续收到很多。现在使用这段代码,处理器工作在 130-140%,我想减少 CPU 的使用。 【参考方案1】:

几个观察:

    确保您使用的是优化/发布版本,而不是未优化的调试版本。在我的设备上,调试构建需要大约 4 秒来处理 12 兆像素的图像,而发布构建需要 0.3 秒。

    当您有一个for 循环时,您可以将其并行化以利用 CPU 上的所有内核。通过使用跨步算法,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)

你可以并行化它

翻转xy 循环,因为我们希望外部循环是图像中的一行。这个想法是为了确保每个线程不仅应该使用连续的内存块,而且我们希望最大限度地减少重叠量以避免“缓存晃动”。因此考虑:

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

我们实际上不会使用上面的,但我们会在下一步中使用它作为模型;

concurrentPerform 替换外部for 循环(现在是y 坐标):

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)

所以,这个想法是:

concurrentPerform替换外部for循环; 与其尝试为x 的每次迭代更新total,不如为每个线程设置一个subTotal 变量,并且只在最后更新total(最大限度地减少来自多个线程对这个共享资源的争用);和 使用一些同步机制(我在这里使用了串行队列,但任何同步机制都可以)来更新total,以确保线程安全。

我试图使示例尽可能简单,但甚至可以进行其他优化:

不同的同步技术提供不同的性能。例如。您可以通过在协议扩展中定义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,请确保以线程安全的方式进行。如果要检查线程安全,请暂时打开“Thread Sanitizer”。

如果每个线程上没有足够的工作(例如,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 

【讨论】:

你可以在这里看到我的代码:github.com/robertmryan/CountWhitePixels 哇看起来超级棒!非常感谢你,我今晚会试试 :) 是的,我每秒分析 60 张图像,所以,我可以保存的任何毫秒都对性能有好处。再次感谢!【参考方案2】:

一般来说 big(o) 性能可以通过用 while 循环替换你的 for 循环来提高,while x

另一种方法是将图像拆分为单独的组件并将它们发送到不同的线程并重新组合生成的数组。您将需要使用 gcd * workitems async 来执行此操作。

【讨论】:

以上是关于优化 Swift 中的嵌套 for 循环的主要内容,如果未能解决你的问题,请参考以下文章

优化四重嵌套“for”循环

如何优化不同长度的嵌套for循环?

c语言中的循环的嵌套是怎么运行的

在 Swift 的嵌套循环中从特定索引枚举字符串

如何打破 Objective-C 中的两个嵌套 for 循环?

从嵌套for循环中的指针集中删除项目