ios - 为什么Xcode和Time Profiler报告更快的iOS设备会占用更高的CPU使用率?

标签 ios swift xcode semaphore cpu-usage

我已经编写了一个模仿经典计算机的应用程序。尽管进入App Store已有两年,但我还是定期尝试通过在Instruments中使用Time Profiler进行测试来减少对CPU内核的需求。比较规格明显不同的真实设备之间的结果时,CPU%利用率显示出相反的趋势。

enter image description here

带注释的Xcode屏幕截图显示了不同的设备规格和CPU使用矛盾。在撰写本文时,使用了Xcode 10.2.1,并且两个设备都安装了iOS 12.2.1。即使在调试模式下运行,也会应用编译优化。在其他设备之间可以看到相同的趋势。 Time Profiler显示的百分比与Xcode相同。尽管有趣的是,当使用“文件”>“记录选项...”>“记录等待线程”时,iPad Mini 2设备降至约22%,iPhone XS Max降至约28%。

实施细节:

该应用程序具有两个并发的进程线程,用于执行两个不同的任务:

  • CPU仿真线程-处理仿真的计算机指令
  • CRT显示模拟线程-处理原始模拟视频
    信号并将其转换为矢量图形

  • 为避免在有任务工作时重复创建两个进程的昂贵开销,请使用分派信号量来控制进程何时进入睡眠状态。即使在调试模式下运行,也会应用编译优化。

    剥离示例代码:

    下面的代码演示了一些用于此帖子的原理。在我的测试设备上,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()
        }
    
    }
    

    问题:
  • 为什么CPU速度更快的设备的CPU使用率百分比会更高?有什么理由认为结果不正确?
  • 如何更好地解释数字或获得设备之间的更好基准?
  • 为什么“记录等待线程”会导致较低的CPU使用率(但仍然没有显着差异,对于更快的设备,有时会更高)?
  • 最佳答案

    我编写了一个例程,该例程执行了一致的计算(通过对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:

    enter image description here

    iPad mini 2:

    enter image description here

    然后,我通过使用以下设置的仪器中的“时间分析器”来运行它:

  • “高频”采样;
  • “记录等待线程”;
  • “延迟”或“窗口化”捕获;和
  • 将调用树更改为按“状态”排序。

  • 对于一个具有代表性的时间示例,iPhone Xs Max报告说该线程正在运行48.2%的时间(基本上,只是等待了一半以上的时间):

    enter image description here

    在iPad mini 2上,该线程运行的时间为95.7%(几乎没有多余的带宽,几乎所有时间都在计算):

    enter image description here

    最重要的是,这表明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少。

    你问:


  • 为什么CPU速度更快的设备的CPU使用率百分比会更高?有什么理由认为结果不正确?


  • 一些观察:
  • 我不确定您是在这里比较苹果还是苹果。如果要进行这种比较,请绝对确保在每个设备上的每个线程上所做的工作都是完全相同的。 (我喜欢几年前在WWDC演讲中听到的那句话;换句话说,“理论上,理论与实践之间没有区别;在实践中,世界之间存在差异”。)

    如果您降低了帧速率或其他基于时间的差异(这些差异可能会将计算方式进行了不同划分),那么这些数字可能无法比较,因为其他因素(例如上下文切换等)可能会起作用。我会100%确保两个设备上的计算结果相同,否则比较会产生误导。
  • ,恕我直言,调试器的CPU“已用百分比”只是一个有趣的晴雨表。也就是说,您要确保在没有任何事情发生时仪表保持良好状态且电量低,以确保那里没有任何流氓任务。相反,在进行大规模并行化和计算密集型的操作时,您可以使用它来确保不会出现会导致设备无法充分利用的错误。

    但是,一般而言,这个调试器“已用百分比”不是我可以挂在上面的数字。查看Instruments,识别被阻塞的线程,查看CPU内核的利用率等总是很有启发性。
  • 在您的示例中,您将重点放在调试器的报告上:iPad mini 2的CPU“已用百分比”为47%,而iPhone Xs Max为85%。您显然会忽略iPad mini,它仅占整体容量的1/4,而对于iPhone Xs Max仅约为1/3。最重要的是,与这些简单的百分比相比,总体仪表没有那么令人担忧。


  • 如何更好地解释数字或获得设备之间的更好基准?


  • 是的,Instruments将始终为您提供更有意义,更可行的结果。


  • 为什么“记录等待线程”会导致较低的CPU使用率(但仍然没有显着差异,对于更快的设备,有时会更高)?


  • 我不确定您是在说哪个“百分比”。大多数常规调用树百分比对于“当我的代码运行时,在哪里花费了多少时间”很有用,但是在没有“记录等待线程”的情况下,您会丢失很大一部分等式,即您的代码正在等待其他地方。这两个都是重要的问题,但是通过包含“记录等待线程”,您可以捕获更全面的图片(即应用程序运行缓慢的地方)。

    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/

    相关文章:

    ios - 尝试获取文档文件夹 URL 时发生 BAD_ACCESS 异常

    ios - 是否可以动态填充应用内购买的产品列表?

    ios - UITableviewCell 中的渐变添加导致重新加载时出现动画问题

    ios - 如何在 swift xcode 6.3.1 中导入和使用 SDWebImage

    ios - 保存最喜欢的按钮状态

    iphone - iOS 开发 : Strange problem with authenticating Game Center user

    Swift:无法以编程方式推送查看 Controller

    ios - 我如何创建一个 UIPageViewController,其中所有屏幕都不同并且有自己的 ViewControllers?

    ios - 在项目中手动导入时,从 swagger-codegen 生成的 Objective c 客户端无法正常工作

    ios - 无法为 UIMainStoryboardFile 'Main' 实例化默认 View Controller