Eri*_*sma 2 performance multithreading grand-central-dispatch swift
我有一个单元测试设置来证明同时执行多个繁重的任务比串行更快。
现在...在此之前的每个人都对上述陈述并不总是正确的事实失去理智之前,让我解释一下。
我从阅读苹果文档中了解到,您不能保证在请求它们时获得多个线程。操作系统 (iOS) 将分配线程,但它认为合适。例如,如果设备只有一个内核,它会分配一个内核,串行会稍微快一些,因为并发操作的初始化代码需要一些额外的时间,而不会提供性能改进,因为设备只有一个内核。
但是:这种差异应该很小。但在我的 POC 设置中,差异很大。在我的 POC 中,并发速度慢了大约 1/3。
如果串行在6 秒内完成,并发将在9 秒内完成。
即使负载较重,这种趋势仍在继续。如果串行在125 秒内完成,并发将在215 秒内竞争。这也不仅发生一次,而且每次都发生。
我想知道我在创建这个 POC 时是否犯了错误,如果是,我应该如何证明并发执行多个繁重的任务确实比串行更快?
我在 swift 单元测试中的 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)")
}
Run Code Online (Sandbox Code Playgroud)
细节:
操作系统: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. 正如改进循环代码所说:
... 虽然这个 [
concurrentPerform] 可以是提高基于循环代码的性能的好方法,但您仍然必须谨慎地使用这种技术。尽管调度队列的开销非常低,但在线程上调度每个循环迭代仍然存在成本。因此,您应该确保您的循环代码做了足够的工作来保证成本。究竟需要做多少工作是必须使用性能工具来衡量的。
在调试版本中,我需要将迭代次数增加到接近 5,000,000 的值,然后才能克服这个开销。在发布版本中,即使这样还不够。旋转循环和递增计数器太快了,无法对并发行为进行有意义的分析。
因此,在我下面的示例中,我用计算量更大的计算(使用历史悠久但效率不高的算法计算?)替换了这个旋转循环。
作为旁白:
如果您在XCTestCase单元测试中执行此操作,您可以使用measure基准性能来衡量性能,而不是自己测量性能。这会多次重复基准测试,捕获经过的时间,平均结果等。只需确保编辑您的方案,以便测试操作使用优化的“发布”构建而不是“调试”构建。
如果您要使用调度组使调用线程等待它完成,则将其调度到全局队列是没有意义的。
您也不需要使用调度组来等待concurrentPerform完成。它同步运行。
虽然concurrentPerform文档是,让我们说,“薄”,文档dispatch_apply(concurrentPerform使用)说:
此函数将一个块提交到调度队列以进行多次调用,并在返回之前等待任务块的所有迭代完成。
这并不是很重要,但值得注意的是,您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)
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
在我的配备 2.9 GHz Intel Core i9 的 MacBook Pro 2018 上,同时测试平均需要 0.247 秒,而串行测试需要大约四倍的时间,即 1.030 秒。
| 归档时间: |
|
| 查看次数: |
815 次 |
| 最近记录: |