Кажется, что короткий ответ - «Нет», а длинный - «Да, но вам это не понравится». Даже ответ на этот вопрос займет некоторое время (так что постарайтесь, я могу его обновить).
При работе с профилированием в R есть несколько основных моментов, которые нужно решить:
Во-первых, есть много разных способов думать о профилировании. Весьма типично думать в терминах стека вызовов. В любой данный момент это последовательность вызовов функций, которые активны, по сути, вложенные друг в друга (подпрограммы, если хотите). Это очень полезно для понимания состояния вычислений, где возвращаются функции, и многих других вещей, которые важны для того, чтобы видеть вещи так, как их видит компьютер / интерпретатор / ОС. Rprof
вызывает профилирование стека.
Во-вторых, другая точка зрения состоит в том, что у меня есть куча кода, и конкретный вызов занимает много времени: какая строка в моем коде вызвала этот вызов? Это профилирование линии. Насколько я могу судить, у R нет профилирования линий. Это в отличие от Python и Matlab, которые оба имеют линейные профилировщики.
В-третьих, отображение от строк к вызовам сюръективно, но не биективно: учитывая конкретный стек вызовов, мы не можем гарантировать, что сможем отобразить его обратно в код. Фактически, анализ стека вызовов часто суммирует вызовы полностью вне контекста всего стека (т.е. совокупное время сообщается независимо от того, где был этот вызов во всех различных стеках, в которых он произошел).
В-четвертых, даже при том, что у нас есть эти ограничения, мы можем надеть наши статистические шляпы и тщательно проанализировать данные стека вызовов и посмотреть, что мы можем с ними сделать. Информация о стеке вызовов data и нам нравятся данные, не так ли? :)
Просто краткое введение в стек вызовов. Давайте просто предположим, что наш стек вызовов выглядел так:
"C" "B" "A"
Это означает, что функция A называется B, которая затем вызывает C (порядок меняется), а стек вызовов имеет 3 уровня глубины. В моем коде стек вызовов достигает 41 уровня. Поскольку стеки могут быть настолько глубокими и представлены в обратном порядке, это более понятно для программного обеспечения, чем для человека. Естественно, мы начинаем очищать и преобразовывать эти данные. :)
Теперь наши данные действительно выглядят так:
".Call" "subCsp_cols" "[" "standardGeneric" "[" "eval" "eval" "callGeneric"
"[" "standardGeneric" "[" "myFunc2" "myFunc1" "eval" "eval" "doTryCatch"
"tryCatchOne" "tryCatchList" "tryCatch" "FUN" "lapply" "mclapply"
"<Anonymous>" "%dopar%"
Несчастный, не так ли? У него даже есть дубликаты таких вещей, как eval
, какой-то парень по имени <Anonymous>
- возможно, какой-то чертов хакер. (Аноним это легион, между прочим. :-))
Первым шагом к преобразованию этого в нечто полезное было разделение каждой строки вывода Rprof()
и обратный ввод записей (через strsplit
и rev
). Первые 12 записей (последние 12, если вы посмотрите на необработанный стек вызовов, а не на версию после rev
) были одинаковыми для каждой строки (из которых было около 12000, интервал выборки составлял 0,5 секунды, то есть около 100 минут профилирования), и они могут быть отброшены.
Помните, нам все еще интересно знать, какие строки (ей) привели к .Call
, что заняло так много времени. Прежде чем мы перейдем к этому вопросу, мы добавим статистические ограничения: отчеты по профилированию, например, из summaryRprof
, profr
, ggplot
и т. д., отражают только совокупное время, потраченное на данный вызов или на вызовы ниже данного вызова. Что эта накопительная информация не говорит нам? Бинго: был ли тот звонок сделан много или несколько раз, и было ли время, проведенное постоянно, во всех вызовах этого звонка или есть какие-то выбросы. Определенная функция может выполняться 100 или 100 тыс. Раз, но вся стоимость может быть вызвана одним вызовом (не следует, но мы не узнаем, пока не посмотрим на данные).
Это только начинает описывать веселье. Пример A-> B-> C не отражает то, как все может выглядеть на самом деле, например, A-> B-> C-> D-> B-> E. Теперь «В» можно посчитать пару раз. Более того, предположим, что на уровне C тратится много времени, но мы никогда не выполняем выборки именно на этом уровне, а только видим его дочерние вызовы в стеке. Мы можем видеть значительное время для «total.time», но не для «self.time». Если в C много разных дочерних вызовов, мы можем упускать из виду то, что нужно оптимизировать - стоит ли вообще убирать C или настраивать детей, B, D и E?
Просто чтобы учесть потраченное время, я взял последовательности и провел их через digest
, сохраняя счетчики для усвоенных значений, через hash
. Я также разделяю последовательности, сохраняя {(A), (A, B), (A, B, C) и т. Д.}. Это не кажется таким интересным, но удаление синглетонов из счетчиков очень помогает в очистке данных. Мы также можем хранить время, потраченное на каждый звонок, используя rle()
. Это полезно для анализа распределения времени, затраченного на данный вызов.
Тем не менее, мы нигде не приблизились к тому, чтобы найти фактическое время, потраченное на строку кода. Мы никогда не получим строки кода из стека вызовов. Более простой способ сделать это - сохранить список раз по всему коду, который хранит вывод proc.time()
для данного вызова. Разница во времени показывает, какие строки или разделы кода занимают много времени. (Подсказка: это то, что мы действительно ищем, а не реальные звонки.)
Однако у нас есть этот стек вызовов, и мы могли бы также сделать что-то полезное. Подниматься вверх по стеку несколько интересно, но если мы перемотаем информацию профиля немного раньше, мы можем найти, какие вызовы имеют тенденцию предшествовать более длительным вызовам. Это позволяет нам искать ориентиры в стеке вызовов - позиции, где мы можем привязать вызов к определенной строке кода. Это немного упрощает сопоставление большего количества обращений к коду, если все, что у нас есть, это стек вызовов, а не инструментальный код. (Как я продолжаю упоминать: вне контекста нет сопоставления 1: 1, но при достаточно тонкой детализации, особенно при неоднократных обращениях к вызовам, которые являются отличительными, вы можете найти ориентиры в вызовах, которые сопоставляются с кодом .)
В целом, я смог найти, какие звонки занимают много времени, будь то длительный интервал или много маленьких, каково было распределение времени, и с некоторыми усилиями я смог сопоставить наиболее важные и трудоемкие обращения к коду и выяснить, какие части кода могут извлечь наибольшую пользу от переписывания или изменения алгоритмов.
Статистический анализ стека вызовов доставляет массу удовольствия, но исследование конкретного вызова на основе совокупного потребления времени не очень хороший способ. Кумулятивное время, потребляемое вызовом, является информативным относительно, но оно не учитывает нас как то, потреблял ли один или несколько вызовов это время, ни глубину вызова в стеке, ни раздел кода, ответственный за вызовы. , Первые две вещи могут быть решены с помощью немного большего количества R-кода, в то время как последняя лучше всего реализуется с помощью инструментального кода.
Поскольку у R еще нет профилировщиков линий, таких как Python и Matlab, самый простой способ справиться с этим - просто обработать свой код.