优化 Swift 中的嵌套 for 循环

2024-01-10

我得到了这个方法来计算白色像素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 is Int(image.scale)
  • pointer来自:

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

一些观察结果:

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

  2. 当你有一个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)

您可以通过以下方式并行化它

  • 翻转x and 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从多个线程,请确保以线程安全的方式执行此操作。如果您想检查线程安全性,请暂时打开“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 }
    }
    
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

优化 Swift 中的嵌套 for 循环 的相关文章

随机推荐