我有一个单元测试设置,以证明同时执行多个繁重的任务比串行执行更快。
现在...在此之前,每个人都不会为这样的事实而迷惑,因为多线程带有许多不确定性,因此上述陈述并不总是正确的,请允许我解释一下。
通过阅读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秒的四倍。