我已经编写了一个模仿经典计算机的应用程序。尽管进入App Store已有两年,但我还是定期尝试通过在Instruments中使用Time Profiler进行测试来减少对CPU内核的需求。比较规格明显不同的真实设备之间的结果时,CPU%利用率显示出相反的趋势。
带注释的Xcode屏幕截图显示了不同的设备规格和CPU使用矛盾。在撰写本文时,使用了Xcode 10.2.1,并且两个设备都安装了iOS 12.2.1。即使在调试模式下运行,也会应用编译优化。在其他设备之间可以看到相同的趋势。 Time Profiler显示的百分比与Xcode相同。尽管有趣的是,当使用“文件”>“记录选项...”>“记录等待线程”时,iPad Mini 2设备降至约22%,iPhone XS Max降至约28%。
实施细节:
该应用程序具有两个并发的进程线程,用于执行两个不同的任务:
信号并将其转换为矢量图形
为避免在有任务工作时重复创建两个进程的昂贵开销,请使用分派信号量来控制进程何时进入睡眠状态。即使在调试模式下运行,也会应用编译优化。
剥离示例代码:
下面的代码演示了一些用于此帖子的原理。在我的测试设备上,CPU使用率%的差异并不明显,但仍与iPad Mini 2和iPhone XS Max设备报告的〜120%矛盾,我应该期望更现代的iPhone设备的价值明显更低。
再次记录等待线程时,该值会降低,但这次与设备的产生更加一致,iPad Mini 2 =〜48%,而iPhone XS Max =〜35%。同样,考虑到处理器的不同,这仍然符合我的期望。
每次运行此演示代码时,平均结果可能会由于无明显原因而偏离至少5%。这使我怀疑CPU使用率%的一般准确性。
final class ViewController: UIViewController {
let processorDispatchSemaphore = DispatchSemaphore(value: 0)
let videoDispatchSemaphore = DispatchSemaphore(value: 0)
fileprivate var stopEmulation = false
fileprivate var lastTime: CFTimeInterval = 0.0
fileprivate var accumulatedCycles = 0
final var pretendVideoData: [Int] = []
final var pretendDisplayData: [Int] = []
override func viewDidLoad() {
super.viewDidLoad()
let displayLink = CADisplayLink(target: self, selector: #selector(displayUpdate))
displayLink.add(to: .main, forMode: RunLoop.Mode.common)
let concurrentEmulationQueue = DispatchQueue.global(qos: .userInteractive)
// CPU simulation thread
concurrentEmulationQueue.async() {
repeat {
// pause until a display refresh
self.processorDispatchSemaphore.wait()
// calculate the number of simulated computer clock
// clock cycles that would have been executed in the
// same time
let currentTime = displayLink.timestamp
let delta: CFTimeInterval = currentTime - self.lastTime
self.lastTime = currentTime
// Z80A Microprocessor clocked at 3.25MHz = 3,250,000 per second
// 1 second / 3250000 = 0.000000307692308
var emulationCyclesRequired = Int((delta / 0.000000307692308).rounded())
// safeguard:
// Time delay every 1/60th (0.0166667) of a second
// 0.0166667 / 0.000000307692308 = 54167 cycles
// let's say that no more than 3 times that should
// be allowed = 54167 * 3 = 162501
if emulationCyclesRequired > 162501 {
// even on slow devices the thread only need
// cap cycles whilst the CADisplayLink takes
// time to kick - so after a less second the
// app need not apply this safeguard
emulationCyclesRequired = 162501
print("emulation cycles capped")
}
// do some simulated work
// **** fake process filling code ****
for cycle in 0...emulationCyclesRequired {
if cycle % 4 == 0 {
self.pretendVideoData.append(cycle &+ cycle)
}
self.accumulatedCycles = self.accumulatedCycles &+ 1
if self.accumulatedCycles > 40000 {
// unpause the CRT display simulation thread
self.videoDispatchSemaphore.signal()
self.pretendVideoData.removeAll(keepingCapacity: true)
}
}
// **** **** ****
// thread is allowed to finish when app goes to the
// background or a non-sumiulation screen.
} while !self.stopEmulation
}
let concurrentDisplayQueue = DispatchQueue.global(qos: .userInteractive)
// CRT display simulation thread
// (edit) see comment to Rob - concurrentEmulationQueue.async(flags: .barrier) {
concurrentDisplayQueue.async(flags: .barrier) {
repeat {
self.videoDispatchSemaphore.wait()
// do some simulated work
// **** fake process filling code ****
for index in 0...1000 {
self.pretendDisplayData.append(~index)
}
self.pretendDisplayData.removeAll(keepingCapacity: true)
// **** **** ****
// thread is allowed to finish when app goes to the
// background or a non-sumiulation screen.
} while !self.stopEmulation
}
}
@objc fileprivate func displayUpdate() {
// unpause the CPU simulation thread
processorDispatchSemaphore.signal()
}
}
问题:
最佳答案
我编写了一个例程,该例程执行了一致的计算(通过对Gregory-Leibniz系列求和来计算π,每60秒仅将其限制为1.2m迭代,并且具有与示例中类似的信号量/ displaylink跳动)。 iPad mini 2和iPhone Xs Max都能够维持60fps的目标(iPad mini 2勉强达到),并且看到CPU使用率值更符合人们的预期。具体来说,iPhone Xs Max(iOS 13)的CPU使用率为47%,而iPad mini 2(iOS 12.3.1)的CPU使用率为102%:
iPhone Xs Max:
iPad mini 2:
然后,我通过使用以下设置的仪器中的“时间分析器”来运行它:
对于一个具有代表性的时间示例,iPhone Xs Max报告说该线程正在运行48.2%的时间(基本上,只是等待了一半以上的时间):
在iPad mini 2上,该线程运行的时间为95.7%(几乎没有多余的带宽,几乎所有时间都在计算):
最重要的是,这表明iPhone Xs Max上的特定队列可能做的事大概是iPad mini 2的两倍。
您会看到Xcode调试器的CPU图形和Instruments的“Time Profiler”正在告诉我们相当一致的故事。它们也都符合我们的期望,即与iPhone mini 2完全相同的任务将使iPhone Xs Max的税负大大降低。
为了全面披露,当我降低工作量(例如,每60秒从120万次迭代减少到仅800k)时,CPU利用率差异就不那么明显了,iPhone上的CPU利用率为48% Xs Max,在iPad mini 2上占59%。但是,功能更强大的iPhone使用的CPU比iPad少。
你问:
一些观察:
如果您降低了帧速率或其他基于时间的差异(这些差异可能会将计算方式进行了不同划分),那么这些数字可能无法比较,因为其他因素(例如上下文切换等)可能会起作用。我会100%确保两个设备上的计算结果相同,否则比较会产生误导。
但是,一般而言,这个调试器“已用百分比”不是我可以挂在上面的数字。查看Instruments,识别被阻塞的线程,查看CPU内核的利用率等总是很有启发性。
是的,Instruments将始终为您提供更有意义,更可行的结果。
我不确定您是在说哪个“百分比”。大多数常规调用树百分比对于“当我的代码运行时,在哪里花费了多少时间”很有用,但是在没有“记录等待线程”的情况下,您会丢失很大一部分等式,即您的代码正在等待其他地方。这两个都是重要的问题,但是通过包含“记录等待线程”,您可以捕获更全面的图片(即应用程序运行缓慢的地方)。
FWIW,这是生成上面代码的代码:
class ViewController: UIViewController {
@IBOutlet weak var fpsLabel: UILabel!
@IBOutlet weak var piLabel: UILabel!
let calculationSemaphore = DispatchSemaphore(value: 0)
let displayLinkSemaphore = DispatchSemaphore(value: 0)
let queue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".pi", qos: .userInitiated)
var times: [CFAbsoluteTime] = []
override func viewDidLoad() {
super.viewDidLoad()
let displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
displayLink.add(to: .main, forMode: .common)
queue.async {
self.calculatePi()
}
}
/// Calculate pi using Gregory-Leibniz series
///
/// I wouldn’t generally hardcode the number of iterations, but this just what I empirically verified I could bump it up to without starting to see too many dropped frames on iPad implementation. I wanted to max out the iPad mini 2, while not pushing it over the edge where the numbers might no longer be comparable.
func calculatePi() {
var iterations = 0
var i = 1.0
var sign = 1.0
var value = 0.0
repeat {
iterations += 1
if iterations % 1_200_000 == 0 {
displayLinkSemaphore.signal()
DispatchQueue.main.async {
self.piLabel.text = "\(value)"
}
calculationSemaphore.wait()
}
value += 4.0 / (sign * i)
i += 2
sign *= -1
} while true
}
@objc func handleDisplayLink(_ displayLink: CADisplayLink) {
displayLinkSemaphore.wait()
calculationSemaphore.signal()
times.insert(displayLink.timestamp, at: 0)
let count = times.count
if count > 60 {
let fps = 60 / (times.first! - times.last!)
times = times.dropLast(count - 60)
fpsLabel.text = String(format: "%.1f", fps)
}
}
}
底线是,鉴于我的上述实验似乎与我们的预期相关,而您的预期与我们的预期没有关系,我想知道您的计算是否实际上每60秒执行一次完全相同的工作,而与上面的设备无关做。一旦丢了帧,对于不同的时间间隔进行了不同的计算,等等,似乎所有其他变量都会起作用,并使比较无效。
值得一提的是,以上内容涵盖了所有信号灯和显示链接逻辑。当我将其简化为尽可能快地在一个线程中求和该序列的5000万个值时,iPhone Xs Max只需0.12秒即可完成,而iPad mini 2只需0.38秒即可完成。显然,通过简单的计算而没有任何计时器或信号灯,硬件性能立即得到缓解。最重要的是,我不会倾向于依靠调试器或仪器中的任何CPU使用率计算来确定您可以实现的理论性能。
关于ios - 为什么Xcode和Time Profiler报告更快的iOS设备会占用更高的CPU使用率?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56507794/