DispatchQueue:为什么串行完成比并行完成快?

时间:2019-02-11 16:36:10

标签: swift multithreading performance grand-central-dispatch

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

现在...在此之前,由于多线程具有很多不确定性,上述陈述并不总是正确的,在这里让每个人都失去主意之前,请允许我解释一下。

通过阅读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)")
}

详细信息:

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

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

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

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

1 个答案:

答案 0 :(得分:4)

有两个问题:

  1. 我会避免在循环中执行print。这是同步的,您可能会在并行实施中遇到更大的性能下降。这不是全部,但这无济于事。

  2. 即使从循环中删除print之后,计数器的50,000增量也根本不足以看到concurrentPerform的好处。正如Improving on Loop Code所说:

      

    ...尽管此[concurrentPerform]是提高基于循环的代码的性能的好方法,但您仍必须谨慎地使用此技术。尽管分派队列的开销非常低,但是在线程上安排每次循环迭代仍然有成本。因此,您应该确保您的循环代码能够完成足够的工作以保证成本。您到底需要做多少工作,这是您必须使用性能工具来衡量的。

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

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

顺便说一句:

  1. 如果您在XCTestCase单元测试中执行此操作,则可以使用measure来对性能进行基准测试,而不是自己测量性能。这样会多次重复进行基准测试,捕获经过的时间,对结果进行平均等。只需确保对方案进行编辑,以便测试操作使用优化的“发布”版本而不是“调试”版本。

  2. 如果您要使用调度组来使调用线程等待其完成,则没有必要将其调度到全局队列中。

  3. 您也不需要使用调度组来等待concurrentPerform完成。它同步运行。

    concurrentPerform documentation说“薄”的同时,documentation for dispatch_applyconcurrentPerform uses)说:

      

    此函数将一个块提交给调度队列以进行多次调用,并在返回之前等待任务块的所有迭代完成。

  4. 这并不是很重要,但是值得注意的是,您的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秒的四倍。