我有一个单元测试设置,以证明同时执行多个繁重的任务比串行执行更快。

现在...在此之前,每个人都不会为这样的事实而迷惑,因为多线程带有许多不确定性,因此上述陈述并不总是正确的,请允许我解释一下。

通过阅读Apple文档,我知道您不能保证在要求时获得多个线程。操作系统(iOS)将分配线程,但它认为合适。例如,如果设备只有一个内核,则它将分配一个内核,并且由于并发操作的初始化代码会花费一些额外的时间,而串行传输会稍微快一些,而由于设备只有一个内核,因此并不能提高性能。

但是:这种差异应该很小。但是在我的POC设置中,差异很大。在我的POC中,并发速度慢了大约1/3的时间。

如果序列在 6秒中完成,则并发将在 9秒中完成。
即使负载较重,这种趋势仍在继续。如果序列在 125秒中完成,则并发将在 215秒中竞争。这不仅会发生一次,而且每次都会发生。

我想知道在创建此POC时是否犯了一个错误,如果是这样,该如何证明并行执行多个繁重的任务确实比串行执行更快?

快速单元测试中的我的POC:

func performHeavyTask(_ completion: (() -> Void)?) {
    var counter = 0
    while counter < 50000 {
        print(counter)
        counter = counter.advanced(by: 1)
    }
    completion?()
}

// MARK: - Serial
func testSerial () {
    let start = DispatchTime.now()
    let _ = DispatchQueue.global(qos: .userInitiated)
    let mainDPG = DispatchGroup()
    mainDPG.enter()
    DispatchQueue.global(qos: .userInitiated).async {[weak self] in
        guard let self = self else { return }
        for _ in 0...10 {
            self.performHeavyTask(nil)
        }
        mainDPG.leave()
    }
    mainDPG.wait()
    let end = DispatchTime.now()
    let nanoTime = end.uptimeNanoseconds - start.uptimeNanoseconds // <<<<< Difference in nano seconds (UInt64)
    print("NanoTime: \(nanoTime / 1_000_000_000)")
}

// MARK: - Concurrent
func testConcurrent() {
    let start = DispatchTime.now()
    let _ = DispatchQueue.global(qos: .userInitiated)
    let mainDPG = DispatchGroup()
    mainDPG.enter()
    DispatchQueue.global(qos: .userInitiated).async {
        let dispatchGroup = DispatchGroup()
        let _ = DispatchQueue.global(qos: .userInitiated)
        DispatchQueue.concurrentPerform(iterations: 10) { index in
            dispatchGroup.enter()
            self.performHeavyTask({
                dispatchGroup.leave()
            })
        }
        dispatchGroup.wait()
        mainDPG.leave()
    }
    mainDPG.wait()
    let end = DispatchTime.now()
    let nanoTime = end.uptimeNanoseconds - start.uptimeNanoseconds // <<<<< Difference in nano seconds (UInt64)
    print("NanoTime: \(nanoTime / 1_000_000_000)")
}

细节:

操作系统:macOS High Sierra
型号名称:MacBook Pro
型号标识符:MacBookPro11,4
处理器名称:Intel Core i7
处理器速度:2.2 GHz
处理器数量:1
核心总数:4

两项测试均在iPhone XS Max模拟器上完成。整个Mac重新启动后,两项测试都立即完成(为避免Mac忙于运行该单元测试以外的应用程序,导致结果模糊)

同样,两个单元测试都包装在一个异步DispatcherWorkItem中,因为该测试用例不阻塞主(UI)队列,从而防止了串行测试用例在该部分上具有优势,因为它消耗了主队列而不是后台队列,因为并发测试用例可以。

我还将接受一个答案,该答案表明POC对此进行了可靠的测试。它不必始终显示并发速度比串行速度快(请阅读上面关于不这样做的解释)。但是至少有一段时间

最佳答案

有两个问题:

  • 我会避免在循环内执行print。这是同步的,您可能会在并发实现中遇到更大的性能下降。这不是这里的全部故事,但没有帮助。
  • 即使从循环中删除了print之后,计数器的50,000增量也根本不足以看到concurrentPerform的好处。正如Improving on Loop Code所说:



    在调试版本上,我需要将迭代次数增加到接近5,000,000的值,然后才能克服此开销。而在发行版本上,这还不够。旋转循环和增加计数器的速度太快,无法提供有意义的并发行为分析。

    因此,在下面的示例中,我将此旋转循环替换为计算量更大的计算(使用历史性但效率不高的算法来计算π)。

  • 作为旁白:
  • 如果您在XCTestCase单元测试中执行此操作,而不是自己衡量性能,则可以使用 measure 来对性能进行基准测试。这会多次重复进行基准测试,捕获经过的时间,对结果进行平均等。只需确保对方案进行编辑,以便测试操作使用优化的“发布”版本而不是“调试”版本。
  • 如果您要使用调度组来使调用线程等待其完成,则没有必要将其调度到全局队列中。
  • 您也不需要使用调度组来等待concurrentPerform完成。它同步运行。

    虽然 concurrentPerform documentation是“薄”,但documentation for dispatch_apply (即 concurrentPerform uses)表示:

  • 并不是很重要,但是值得注意的是,您的for _ in 0...10 { ... }进行了11次迭代,而不是10次迭代。您显然打算使用..<

  • 因此,这里有一个示例,将其放在单元测试中,但用计算量更大的内容代替了“繁重的”计算:
    class MyAppTests: XCTestCase {
    
        // calculate pi using Gregory-Leibniz series
    
        func calculatePi(iterations: Int) -> Double {
            var result = 0.0
            var sign = 1.0
            for i in 0 ..< iterations {
                result += sign / Double(i * 2 + 1)
                sign *= -1
            }
            return result * 4
        }
    
        func performHeavyTask(iteration: Int) {
            let pi = calculatePi(iterations: 100_000_000)
    
            print(iteration, .pi - pi)
        }
    
        func testSerial () {
            measure {
                for i in 0..<10 {
                    self.performHeavyTask(iteration: i)
                }
            }
        }
    
        func testConcurrent() {
            measure {
                DispatchQueue.concurrentPerform(iterations: 10) { i in
                    self.performHeavyTask(iteration: i)
                }
            }
        }
    
    }
    

    在配备2.9 GHz Intel Core i9的MacBook Pro 2018上,发布版本的并行测试平均需要0.247秒,而串行测试所需的时间大约是1.030秒的四倍。

    10-06 03:55