深入研究R剖析信息

时间:2011-08-31 04:38:31

标签: r profiling

我正在尝试优化一些代码,并对来自summaryRprof()的信息感到困惑。特别是,它看起来像是对外部C程序进行了多次调用,但是我无法确定哪个 C程序,从哪个R函数。我打算通过一堆切片和切割代码来解决这个问题,但是想知道我是否忽略了一些更好的方法来解释分析数据。

最耗费的函数是.Call,这显然是对C代码调用的一般描述;下一个主要功能似乎是分配操作:

$by.self
                             self.time self.pct total.time total.pct
".Call"                        2281.0    54.40     2312.0     55.14
"[.data.frame"                  145.0     3.46      218.5      5.21
"initialize"                    123.5     2.95      217.5      5.19
"$<-.data.frame"                121.5     2.90      121.5      2.90
"as.vector"                     110.5     2.64      416.0      9.92

我决定专注于.Call,看看它是如何产生的。我查看了分析文件,在调用堆栈中找到.Call的条目,以下是调用堆栈中的顶部条目(按出现次数计算):

13640 "eval"
11252 "["
7044 "standardGeneric"
4691 "<Anonymous>"
4658 "tryCatch"
4654 "tryCatchList"
4652 "tryCatchOne"
4648 "doTryCatch"

这个清单就像泥巴一样清晰:我有<Anonymous>standardGeneric

我认为这是由于对Matrix包中的函数的调用,但那是因为我正在查看代码,并且该包似乎是唯一可能的C代码源。但是,在这个包中调用了Matrix的许多不同的函数,并且似乎很难确定这次函数正在消耗。

所以,我的问题非常基本:是否有某种方法可以解密和归因这些调用(例如.Call<Anonymous>等)?考虑到所涉及的函数数量,此代码的调用图的绘图相当棘手。

我看到的回退策略要么是(1)注释掉一些代码(以及为了使代码能够使用它而破解)以查看时间消耗发生的位置,或者(2)将某些操作包含在其他内部函数并查看这些函数何时出现在调用堆栈中。后者是不优雅的,但似乎这是向调用堆栈添加标记的最佳方式。前者是不愉快的,因为运行代码需要相当长的时间,迭代地取消注释代码并重新运行是令人不快的。

3 个答案:

答案 0 :(得分:10)

我建议您使用profr包。这是哈德利的另一个魔力。它是Rprof的包装器,可以看到调用堆栈和时间。

我发现profr非常易于使用和解释。例如,下面是一些ddply示例代码的配置文件以及生成的profr图:

library(profr)
p <- profr(
    ddply(baseball, .(year), "nrow"),
    0.01
)
plot(p)

enter image description here

您可以立即看到以下内容:

  • ddply如何调用ldplyllplyloop_apply
  • loop_apply内,有.Call个功能。

您可以通过阅读loop_apply

的源代码来确认这一点
> plyr:::loop_apply
function (n, f, env = parent.frame()) 
{
    .Call("loop_apply", as.integer(n), f, env)
}
<environment: namespace:plyr>

编辑。 ggplot.profr方法有些奇怪之处。我向哈德利提出了以下修正案。 (您可能希望在您的示例中尝试此操作。)

ggplot.profr <- function (data, ..., minlabel = 0.1, angle = 0){
  if (!require("ggplot2", quiet = TRUE)) 
    stop("Please install ggplot2 to use this plotting method")
  data$range <- diff(range(data$time))
  ggplot(as.data.frame(data), aes(y=level)) + 
      geom_rect(
          #aes(xmin=(level), xmax=factor(level)+1, ymin=start, ymax=end),  
          aes(ymin=level-0.5, ymax=level+0.5, xmin=start, xmax=end),  
          #position = "identity", stat = "identity", width = 1, 
          fill = "grey95", 
          colour = "black", size = 0.5) + 
      geom_text(aes(label = f, x = start + range/60), 
          data = subset(data, time > max(time) * minlabel), size = 4, angle = angle, vjust=0.5, hjust = 0) + 
      scale_x_continuous("time") + 
      scale_y_continuous("level")
}

答案 1 :(得分:3)

配置文件中的行可能看起来像

"strsplit" ".parseTabix" ".readVcf" "readVcf" "standardGeneric" "readVcf" "system.time" 

其中说,从右到左,最外面的函数是system.time,它调用readVcf,这是一个调度到readVcf方法的S4泛型,调用一个函数.readVcf,调用.parseTabix,最后调用strsplit。

在这里,我们读取配置文件,对行进行排序,计算它们(使用rle - 运行长度编码),然后在配置文件中选择六个最常见的路径

r = rle(sort(readLines("readVcf.Rprof"))
o = order(r$lengths, decreasing=TRUE)
r$values[head(o)]

r$lengths[head(o)]

告诉我们每个调用堆栈的采样次数。

有一些常见的模式可以帮助解释这一点。这是一个S4泛型被分派到它的方法

"readVcf" "standardGeneric" "readVcf"

lapply迭代其功能

"FUN" "lapply"

围绕tryCatch

.Call
".Call" "doTryCatch" "tryCatchOne" "tryCatchList" "tryCatch"

通常会尝试分析相对较小的代码块而不是整个脚本,其中小块通过例如交互式地逐步执行代码或者对哪些部分可能很慢进行一些有根据的猜测来识别。事实上.Call是最常见的采样功能并不令人鼓舞 - 它表明大部分时间已经花在C上。可能你最好的选择是提出更好的整体算法,而不是说蛮力方法

答案 2 :(得分:3)

似乎简短的回答是&#34; No&#34;答案很长,是的,但是你不会喜欢这个。&#34;即使回答这个问题也需要一些时间(所以我可能会更新它)。

在R中进行性能分析时,有几个基本的事情可以解决:

首先,有许多不同的方法可以考虑分析。从调用堆栈的角度来考虑是很典型的。在任何给定的瞬间,这是活动的函数调用序列,基本上彼此嵌套(子程序,如果你愿意)。这对于理解评估的状态非常有用,其中函数将返回,以及许多其他对于看到计算机/解释器/ OS可能看到的东西很重要的事情。 Rprof会调用堆栈分析。

其次,一个不同的观点是我有一堆代码并且特定的调用需要很长时间:我的代码中的哪一行导致了该调用?这是线性分析。据我所知,R没有线性分析。这与Python和Matlab形成对比,后者都有行剖析器。

第三,从线到调用的映射是完全的,但它不是双射的:给定一个特定的调用堆栈,我们不能保证我们可以将它映射回代码。实际上,调用堆栈分析通常会完全脱离整个堆栈的上下文来总结调用(即,无论调用发生在所有不同堆栈的哪个位置,都会报告累积时间。)

第四,即使我们有这些限制,我们也可以戴上我们的统计帽子并仔细分析调用堆栈数据,看看我们能做些什么。调用堆栈信息是数据,我们喜欢数据,不是吗? :)

快速介绍一下调​​用堆栈。让我们假设我们的调用堆栈看起来像这样:

"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()输出的每一行并反转条目(通过strsplitrev)。前12个条目(如果查看原始调用堆栈,则为最后12个条目,而不是post rev版本)对于每一行都是相同的(其中大约有12000个,采样间隔为0.5秒 - 所以大约100分钟的分析),这些可以丢弃。

请记住,我们仍然有兴趣知道哪些导致了.Call,这需要花费很多时间。在我们讨论这个问题之前,我们提出了统计上限:分析报告,例如:来自summaryRprofprofrggplot等,仅反映给定呼叫或给定呼叫下的呼叫所花费的累计时间。这些累积信息没有告诉我们什么?宾果游戏:无论是多次调用,还是少数调用,以及花费的时间是否都是针对该调用的所有调用的持续时间,还是存在一些异常值。一个特定的功能可能会执行100次或100K次,但所有的费用可能来自一次调用(它不应该,但我们不知道,直到我们查看数据)。

这只是开始描述的乐趣。 A-> B-> C实例并不反映事物可能真实出现的方式,例如A-> B-> C-> D-> B-> E。现在,&#34; B&#34;可能会被计算几次。更重要的是,假设在C级别花费了大量时间,但我们从未在该级别上进行采样,只看到其子级调用在堆栈中。对于&#34; total.time&#34;我们可能会看到相当长的时间,但对于&#34; self.time&#34;则没有。如果在C下有很多不同的儿童电话,我们可能会忽略优化的内容 - 我们应该完全取出C还是调整孩子,B,D和E?

为了说明花费的时间,我接受了序列并通过digest运行它们,通过hash存储消化值的计数。我还分割了序列,存储{(A),(A,B),(A,B,C)等。}。这看起来并不那么有趣,但从计数中删除单例有助于清理数据。我们还可以使用rle()存储每次通话所花费的时间。这对于分析给定呼叫所花费的时间分布非常有用。

我们仍然无法找到每行代码所花费的实际时间。我们永远不会从调用堆栈中获取代码行。一种更简单的方法是在整个代码中存储一个时间列表,它存储proc.time()的输出,用于给定的调用。利用这些时间的差异可以发现代码的哪些行或部分需要很长时间。 (提示:这是我们真正想要的,而不是实际的呼叫。)

但是,我们有这个调用堆栈,我们也可以做一些有用的事情。上升堆栈有点令人感兴趣,但如果我们将配置文件信息提前一点,我们可以找到哪些调用往往在较长时间运行的调用之前。这允许我们在调用堆栈中查找地标 - 我们可以将调用绑定到特定代码行的位置。这使得将更多的调用映射回代码变得容易一些,如果我们只有调用堆栈而不是检测代码。 (正如我一直提到的那样:在上下文之外,没有1:1的映射,但是在足够精细的粒度下,特别是在重复命中的独特呼叫中,你可能能够在映射到的呼叫中找到地标代码。)

总而言之,我能够找到哪些电话占用了大量时间,无论是基于1个长间隔还是许多小电话,花费的时间分配是什么样的,并且通过一些努力,我能够映射最重要的&amp;耗费时间回调代码并发现代码的哪些部分可以从重写或算法更改中获益最多。

调用堆栈的统计分析很有趣,但根据累计时间消耗调查特定呼叫并不是一个很好的方法。呼叫消耗的累积时间在相对的基础上提供信息,但它并不能告诉我们这次是否消耗了一个或多个呼叫,也没有告诉我们堆栈中的呼叫深度,也没有负责代码的部分。调用。前两个方面可以通过更多的R代码来解决,而后者最好通过检测代码来实现。

由于R还没有像Python和Matlab这样的行剖析器,处理这个问题的最简单方法就是设置代码。

相关问题