Почему Xcode и Time Profiler сообщают о более высокой загрузке процессора для более быстрых устройств iOS? - PullRequest
1 голос
/ 08 июня 2019

Я написал приложение для эмуляции классического компьютера. Несмотря на то, что я был в App Store в течение нескольких лет, я регулярно пытался снизить потребность в ядрах процессоров посредством тестирования с помощью Time Profiler в инструментах. При сравнении результатов между реальными устройствами со значительно отличающимися характеристиками загрузка ЦП показывает обратные тенденции.

enter image description here

Аннотированные скриншоты XCode показывают противоречивые характеристики устройства и противоречие использования ЦП. На момент написания статьи использовался Xcode 10.2.1, и на обоих устройствах установлена ​​iOS 12.2.1. Оптимизация компиляции применяется даже при работе в режиме отладки. Такая же тенденция прослеживается между другими устройствами. Time Profiler показывает те же проценты, что и Xcode. Интересно, что при использовании «Файл»> «Параметры записи»> «Записать ожидающие потоки» устройство iPad Mini 2 падает до ~ 22%, а iPhone XS Max падает до ~ 28%.

Детали реализации:

Приложение имеет два параллельных потока процесса для двух разных задач:

  • Поток симуляции процессора - обработка эмулируемых компьютерных инструкций
  • Поток моделирования ЭЛТ-дисплея - обработка необработанного эмулированного видео сигналы и превращение их в векторы графики

Чтобы избежать дорогостоящих накладных расходов, связанных с повторным созданием двух процессов, когда есть работа для задачи, семафоры отправки используются для управления временем ожидания процессов. Оптимизация компиляции применяется даже при работе в режиме отладки.

Пример кода без кода:

Этот код ниже демонстрирует некоторые принципы для этой статьи. На моих тестовых устройствах разница в процентах использования ЦП не столь выражена, но все еще противоречива, поскольку устройства iPad Mini 2 и iPhone XS Max показывают ~ 120%, и я ожидаю, что более современное устройство iPhone будет значительно ниже.

При повторной записи ожидающих потоков значения ниже, но в этот раз больше соответствуют поколению устройства, iPad Mini 2 = ~ 48% против iPhone XS Max = ~ 35%. Опять же, это все еще соответствует моим ожиданиям, учитывая разницу в их процессорах.

Каждый раз, когда запускается этот демонстрационный код, средние результаты могут отклоняться без видимой причины как минимум на 5%. Что заставляет меня сомневаться в общей точности использования процессора%.

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()
    }

}

Вопросы:

  1. Почему загрузка ЦП% может быть выше для устройств с более быстрыми ЦП? Есть основания полагать, что результаты не точны?
  2. Как мне лучше интерпретировать цифры или получить более точные тесты между устройствами?
  3. Почему записи ожидающих потоков приводят к более низкому проценту использования ЦП (но все же незначительно отличаются, а иногда и выше для более быстрого устройства)?

1 Ответ

1 голос
/ 09 июня 2019

Я написал подпрограмму, которая выполняла непротиворечивые вычисления (вычисление π путем суммирования рядов Грегори-Лейбница, дросселированных до 1,2 м итераций каждую 60-ую секунду, с таким же танцем семафора / displaylink, как и в вашем примере). И iPad mini 2, и iPhone Xs Max смогли выдержать целевую скорость 60 кадров в секунду (iPad mini 2 едва ли) и увидели, что значения загрузки ЦП более соответствуют ожидаемому. В частности, загрузка процессора составила 47% на iPhone Xs Max (iOS 13), но 102% на iPad mini 2 (iOS 12.3.1):

iPhone Xs Max:

enter image description here

iPad mini 2:

enter image description here

Затем я запустил это через «Time Profiler» в Instruments со следующими настройками:

  • «Высокочастотная» выборка;
  • «Запись ожидающих потоков»;
  • «Отложенный» или «оконный» захват; и
  • Изменено дерево вызовов для сортировки по «состоянию».

Для репрезентативной временной выборки 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 и инструменты «Time Profiler» рассказывают нам довольно последовательные истории. И они оба согласуются с нашими ожиданиями, что iPhone Xs Max будет значительно меньше облагаться той же задачей, что и iPhone mini 2.

В интересах полного раскрытия, когда я снизил рабочую нагрузку (например, взяв ее с 1,2 м итераций каждую 60-ую секунды, до всего лишь 800 КБ), разница в загрузке ЦП была менее резкой, где загрузка ЦП составляла 48 % на iPhone Xs Max и 59% на iPad mini 2. Но все же, более мощный iPhone использовал меньше процессоров, чем iPad.

Вы спросили:

  1. Почему загрузка ЦП% может быть выше для устройств с более быстрыми ЦП? Есть основания полагать, что результаты не точны?

Пара наблюдений:

  • Я не уверен, что вы сравниваете яблоки с яблоками здесь. Если вы собираетесь проводить такого рода сравнения, убедитесь, что работа, проделанная в каждом потоке на каждом устройстве, абсолютно идентична. (Мне нравится эта цитата, которую я слышал во время презентации WWDC несколько лет назад; перефразируя: «в теории нет разницы между теорией и практикой; на практике существует мир различий».)

    Если бы вы упали частоты кадров или другие временные различия, которые могли бы разделить вычисления по-разному, числа могут быть несопоставимыми, потому что другие факторы, такие как переключение контекста и тому подобное, могут вступить в игру. Я на 100% уверен, что расчеты на двух устройствах идентичны, иначе сравнения будут вводить в заблуждение.

  • ЦП отладчика «Процент использования», имхо, просто интересный барометр. То есть, вы хотите убедиться, что счетчик работает нормально и низко, когда у вас ничего не происходит, чтобы убедиться, что там нет какой-то жуликской задачи. И наоборот, когда вы выполняете что-то массово распараллеленное и требующее большого объема вычислений, вы можете использовать это, чтобы убедиться, что у вас нет какой-либо ошибки, которая мешает полному использованию устройства.

    Но этот отладчик «Процент использования» - это вообще не число, на которое я вешаю шляпу. Всегда интереснее смотреть на инструменты, определять потоки, которые заблокированы, смотреть на использование ядром процессора и т. Д.

  • В вашем примере вы уделяете большое вниманиеОтчет отладчика об использовании CPU в процентах от 47% на iPad mini 2 против 85% на iPhone Xs Max. Очевидно, вы игнорируете, что на iPad mini он составляет примерно одну десятую от общей емкости, но только рядом с пятым для iPhone Xs Max. Итог, общий счетчик менее беспокоит, чем эти простые проценты.

  1. Как мне лучше интерпретировать цифры или получить более точные тесты между устройствами?

Да, инструменты всегда дадут вам более значимые, более действенные результаты.

  1. Почему запись ожидающих потоков приводит к более низкому проценту использования ЦП (но все же незначительно отличается, а иногда и выше для более быстрого устройства)?

Я не уверен, о каких «процентах» вы говорите. Большинство общих процентов дерева вызовов полезно для «когда мой код работает, какой процент времени тратится на что», но в отсутствие «записи ожидающих потоков» вы пропускаете большую часть уравнения, т.е. где ваш код ждет чего-то другого. Обе эти проблемы важны, но, включив «Запись ожидающих потоков», вы получаете более целостную картину (т. Е. Когда приложение работает медленно).


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-ую секунду, независимо от устройства, как и выше. Если у вас есть пропущенные кадры, другие вычисления для разных временных интервалов и т. Д., Кажется, что в игру вступают все другие переменные, и сравнение становится недействительным.


Для чего бы это ни стоило, вышеизложенное относится ко всей логике семафора и отображаемой ссылки. Когда я упростил его, чтобы просто как можно быстрее суммировать 50 миллионов значений последовательности в одном потоке, iPhone Xs Max сделал это за 0,12 секунды, тогда как iPad mini 2 сделал это за 0,38 секунды. Очевидно, что с простыми вычислениями без каких-либо таймеров или семафоров производительность оборудования резко снижается. В итоге я не склонен полагаться на какие-либо расчеты использования ЦП в отладчике или инструментах для определения теоретической производительности, которую вы можете достичь.

...