【问题标题】:NSOperationQueue worse performance than single thread on computation taskNSOperationQueue 在计算任务上的性能比单线程差
【发布时间】:2017-01-27 23:09:11
【问题描述】:

我的第一个问题!

我正在对视频源进行 CPU 密集型图像处理,并且我想使用 OperationQueue。然而,结果绝对是可怕的。这是一个示例——假设我有一个 CPU 密集型操作:

var data = [Int].init(repeating: 0, count: 1_000_000)

func run() {
  let startTime = DispatchTime.now().uptimeNanoseconds
  for i in data.indices { data[i] = data[i] &+ 1 }
  NSLog("\(DispatchTime.now().uptimeNanoseconds - startTime)")
}

在我的笔记本电脑上执行大约需要 40 毫秒。我计时一百次:

(1...100).forEach { i in run(i) }

它们平均每个大约 42 毫秒,总共大约 4200 毫秒。我有 4 个物理内核,所以我尝试在 OperationQueue 上运行它:

var q = OperationQueue()
(1...100).forEach { i in
  q.addOperation {
    run(i)
  }
}
q.waitUntilAllOperationsAreFinished()

有趣的事情发生取决于 q.maxConcurrentOperationCount:

concurrency      single operation        total
     1                 45ms             4500ms
     2              100-250ms           8000ms
     3              100-300ms           7200ms
     4              250-450ms           9000ms
     5              250-650ms           9800ms
     6              600-800ms          11300ms

我使用.background的默认QoS,可以看到线程优先级是默认的(0.5)。查看 Instruments 的 CPU 利用率,我看到很多浪费的周期(第一部分在主线程上运行,第二部分在 OperationQueue 上运行):

我用 C 语言编写了一个简单的线程队列,并在 Swift 中使用了它,它随内核线性扩展,因此我的速度提高了 4 倍。但是我在 Swift 上做错了什么?

更新: 我认为我们已经得出结论,这是 DispatchQueue 中的一个合法错误。那么问题实际上是在 DispatchQueue 代码中询问问题的正确渠道是什么?

【问题讨论】:

  • 没有数据冲突吗?一个线程由于其他线程在同一索引处写入而被锁定?
  • Marek,有,但我不关心计算的结果,我只需要一个持续约 50 毫秒的计算。
  • 尝试让 run func 作为一个 NSOperation 子类

标签: swift multithreading macos performance


【解决方案1】:

您似乎在测量每个run 执行的挂钟时间。这似乎不是正确的指标。将问题并行化并不意味着每次运行都会执行得更快……它只是意味着您可以一次执行多个运行。

无论如何,让我验证一下你的结果。

您的函数run 似乎只在某些时候使用参数。为了清楚起见,让我定义一个类似的函数:

func increment(_ offset : Int) {
  for i in data.indices { data[i] = data[i] &+ offset }
}

在我的测试机器上,在发布模式下,此代码每次输入需要 0.68 ns,或者每次添加需要大约 2.3 个周期(在 3.4 GHz 下)。禁用边界检查会有所帮助(每个条目低至 0.5 ns)。

无论如何。所以接下来让我们按照你的建议并行化这个问题:

var q = OperationQueue()
for i in 1...queues {
    q.addOperation {
      increment(i)
    }
}
q.waitUntilAllOperationsAreFinished()

这似乎不是特别安全,但它很快吗?

嗯,它更快...我达到了每个条目 0.3 ns。

源码:https://github.com/lemire/Code-used-on-Daniel-Lemire-s-blog/tree/master/extra/swift/opqueue

【讨论】:

  • Daniel,我只测量经过的时间,因为奇怪的是每个项目需要更长的时间。当它们在我的线程队列上运行时,每个操作仍然需要 45 毫秒,因此产生 8 个线程意味着我可以一次执行 8 个。使用 NSOperationQueue,这 8 个中的每一个都开始花费 10 倍的时间,使队列无用。
  • @neb 你试过运行我测试过的代码吗? github.com/lemire/Code-used-on-Daniel-Lemire-s-blog/tree/master/…我对你在这个测试中的数字感兴趣。
  • 对不起,necro-post,我最近再次运行了你的代码。一定发生了一些变化,因为现在我看到处理器数量的线性加速。我从来没有感谢你花时间写下来并测试它——谢谢!
【解决方案2】:

.background 将运行优先级最低的线程。如果您正在寻找快速执行,请考虑 .userInitiated 并确保您在打开编译器优化的情况下测量性能。

还可以考虑使用 DispatchQueue 而不是 OperationQueue。它可能具有更少的开销和更好的性能。

根据您的 cmets 更新:试试这个。它从我笔记本电脑上的 38 秒到 14 秒左右。

显着变化:

  • 我使队列显式并发
  • 我在发布模式下运行这个东西
  • 用随机数代替了内循环计算,原来的优化出来了
  • QoS 设置为更高级别:QoS 现在按预期工作,.background 永远运行

var data = [Int].init(重复:0,计数:1_000_000)

func run() {
    let startTime = DispatchTime.now().uptimeNanoseconds
    for i in data.indices { data[i] = Int(arc4random_uniform(1000)) }
    print("\((DispatchTime.now().uptimeNanoseconds - startTime)/1_000_000)")
}

let startTime = DispatchTime.now().uptimeNanoseconds
var g = DispatchGroup()
var q = DispatchQueue(label: "myQueue", qos: .userInitiated, attributes: [.concurrent])
(1...100).forEach { i in
    q.async(group: g) {
        run()
    }
}
g.wait()
print("\((DispatchTime.now().uptimeNanoseconds - startTime)/1_000_000)")

但还是有问题 - 串行队列的运行速度提高了 3 倍,即使它不使用所有内核。

【讨论】:

  • 感谢您的建议!我应该提到我已经尝试过更改 QoS 参数,但它对我的测试没有任何影响,我已经尝试了所有这些参数。 DispatchQueue 是一个很好的建议——它没有用于有界线程队列的原语,但我确实尝试在它之上实现有界队列,并发现性能发生了同样的事情。
猜你喜欢
  • 1970-01-01
  • 2018-12-15
  • 2011-10-12
  • 1970-01-01
  • 2016-04-12
  • 2012-01-30
  • 2020-01-10
  • 2015-05-26
  • 1970-01-01
相关资源
最近更新 更多