作者:马丁摩根(mtmorgan@fhcrc.org.),Sonali Arora(sarora@fredhutch.org
日期:2015年6月19日

性能:宗罪

本节的目标是学习如何编写正确、健壮、简单和高效的R代码。我们通过一些案例研究来做到这一点。

优先事项

  1. 正确:与手工实施例一致(相同的()All.Equal()
  2. 强大:支持现实输入,例如0长向量,NA价值观,......
  3. 简单:易于理解下个月;容易描述它对同事做了什么;易于发现逻辑错误;易于增强。
  4. 快,或者至少从现代计算机的速度来看是合理的。

策略

  1. 配置文件
  1. 矢量化 - 在向量上运行,而不是明确的循环

    x <- 1 log(x) ## NOT for (i in seq_along) x[i] <- log(x[i])
    ## [1] 0.0000000 0.6931472 1.0986123 1.3862944 1.6094379 1.7917595 1.9459101 ## [8] 2.0794415 2.1972246 2.3025851
  2. 预先分配内存,然后填写结果

    Result [i] <- runif(1): Result [i] <- runif(1) * Result [i - 1] Result
    ## [1] 0.7386850875 0.3696589691 0.3542664926 0.0510763549 0.0254611460 ## [6] 0.0070750491 0.0038783618 0.0011608073 0.0007160663 0.0006206700
  3. 在重复计算之外的升值常见子表达式,使子表达式仅计算一次
  1. 重用现有的、经过测试的代码
  1. 重新思考如何攻击问题
  1. 使用byte编译器编译脚本
  2. 使用并行评估
  3. 用舌头 - “外国”语言如C,Fortran

案例研究:从迭代到矢量化

这是一个明显低效的函数:

f0 < - 函数(n,a = 2){## stopfnot(是.integer(n)&&(长度(n)== 1)&& ##!是.na(n)&&(n> 0))结果< -  numeric()for(i在seq_len(n)中)结果[[i]] < -  a * log(i)结果}

系统时间()调查该算法如何衡量N,专注于经过的时间。

System.time(F0(10000))
##用户系统经过## 0.119 0.004 0.122
n < -  1000 * seq(1,20,2)t < -  sapply(n,function(i)system.time(f0(i))[[3]]图(t〜n,type =“b”)

记住当前的“正确”值,以及近似的时间

n < -  10000 system.time(预期< -  f0(n))
##用户系统运行## 0.121 0.000 0.120
头(预期)
## [1] 0.000000 1.386294 2.197225 2.772589 3.218876 3.583519

修改呼吸常见乘法器的功能,一种,不知情。确保“优化”结果与原始计算结果相同。使用Microbenchmark.包以比较两个版本

f1 < - 函数(n,a = 2){for(i在seq_len(n)中)的结果< -  numeric()结果[[i]] < -  log(i)a *结果}相同(预期,f1(n)))
## [1]真实
库(MicroBenchmark)Microbenchmark(F0(n),f1(n),times = 5)
##单位:毫秒为MIN LQ均值UQ MAX NEVAL ## F0(n)109.7719 109.9072 130.5134 141.9948 142.5390 148.3542 5 ## F1(n)108.563013939333.2842 13333.2842 139.59794

采用“预先分配和填补”策略

F2 < - 函数(n,a = 2){结果< -  numeric(n)for(i在seq_len(n)中)结果[[i]] < -  log(i)a *结果}相同(预期,f2(n))
## [1]真实
微基准测试(f0 (n), f2 (n),时间= 5)
##单位:毫秒## EXPR分钟LQ平均中值最大UQ ## F0(n)的121.201141 121.739866 134.761569 143.617813 143.620317 143.628707 ## F2(n)的7.684828 7.849474 8.559228 8.415322 8.888803 9.957714 ## neval ## 5 ## 5

用A.*申请()函数,以避免必须显式地预分配,并使向量化的机会更加明显。

F3 < - 函数(n,a = 2)a * sapply(seq_len(n),log)相同(预期,f3(n))
## [1]真实
微基准测试(f0(n), f2(n), f3(n), times=10)
## f0(n) 7.65573 7.698159 8.250884 8.177678 8.452503 9.566792 ## f3(n) 3.579709 3.677523 4.083759 4.048497 4.455364 4.736191 ## neval ## 10 ## 10 ## 10

既然代码以单行呈现,那么很明显它可以很容易地矢量化。抓住机会向量化它:

F4 <- function(n, a=2) a * log(seq_len(n)) identical(expected, F4 (n))
## [1]真实
Microbenchmark (f0(n), f3(n), f4(n), times=10)
##单位:微秒## EXPR分钟LQ平均中值最大UQ ## F0(n)的112256.477 145432.576 149337.9525 146027.0340 169576.336 179843.968 ## F3(n)的3583.222 3925.399 4090.4899 3987.5965 4017.998 5427.407 ## F4(N)364.700 378.439 395.9539 402.8765 407.481 422.962## Neval ## 10#10 ## 10

返回我们的显式迭代F2()在这些情况下,将代码编译为更有效的表示有助于。使用编译器包执行此操作。

f2c <- cmpfun(f2) n <- 10000相同的(f2(n), f2c(n))
## [1]真实
微基准测试(f2 (n), f2c (n),时间= 10)
## f2c(n) 2.000680 2.045304 2.157064 2.164375 2.216226 2.38820 10 ## f2c(n) 2.000680 2.045304

F4()绝对是赢家。它是如何缩放的N吗?(重复几次)

n < -  10 ^(5:8)#100x大于f0 t < -  sapply(n,function(i)system.time(f4(i))[[3]])绘图(t〜n,log =“xy“,type =”b“)

对不同响应模式的解释?

得到教训:

  1. 矢量化提供了迭代的巨大改善
  2. 在需要显式迭代时,预先分配和填充非常有用。
  3. *申请()功能有助于避免需要明确的预分配,并为矢量化的机会更加明显。这可能会出现小的性能成本,但通常是值得的
  4. 吊装常用子表达式和使用方法编译器当需要显式迭代时,Package可以帮助提高性能。

案例研究:选择算法

在抽象意义上推理算法可能非常有帮助,以了解操作如何规模的理解。这是一个有趣的问题,取自堆栈溢出假设有一个很长的分类向量

VEC < -  C(SEQ(-100,-1,length.out.out = 1e6),rep(0,20),seq(1,100,length.out = 1e6))

使用简单目标是识别小于零的值数。原始帖子和许多响应表明,扫描载体的变化对于少于零,然后求和

F0 < - 函数(v)总和(v <0)

该算法比较了每个元素vec为零,创建逻辑向量,只要原件,长度(v)。然后扫描该逻辑向量和()计算满足条件的元素数量。

问题:

  1. 有多少长度的向量V.需要分配给这个算法吗?
  2. 基于需要执行的比较数,您如何预期该算法将缩放的长度V.吗?用一个简单的数字验证这个。

    N <- seq(1,11,2) * 1e6 Time <- sapply(N, function(N) {v <- sort(rnorm(N)) system.time(f0(v))[[3]]}) plot(Time ~ N, type="b")

是否有更好的算法,即,一种到达相同答案的方法,但需要更少的时间(和/或空间)?矢量被排序,我们可以通过做一个二进制搜索。算法出奇地简单:创建最小(第一个)元素和最大(最后一个)元素的索引。检查中间的元素是否大于等于零。如果是,将最大索引移动到该点。否则,就把这个点设为新的最小值。重复此过程,直到最小索引不再小于最大索引。

F3 < - 函数(x,阈值= 0){imin < -  1l imax < - 长度(x)(imax> = imin){imid <--as.integer(imin +(imax-imin)/ 2)(imin +(imax-imin)/ 2)(X [IMID]> =阈值)IMAX < -  IMID  -  1L ELSE IMIN < -  IMID + 1L} IMAX}

每次迭代都丢弃大约一半可能的值,因此我们预计平均地将在最后到达log2(长度(v))迭代 - 算法缩放了长度的日志V.,而不是与长度V.,没有创建长向量。这些差异随着长度而越来越重要V.就长了。

问题:

  1. 使用简单的数据验证F3()f0 ()导致相同的()答案。
  2. 比较如何定时F3()向量长度的缩放。

    ## Identity Stopifnot(相同(f0( -  2):2),f3(( -  2):2)),相同(f0(2:4),f3(2:4)),相同(f0( -(4:2)),F3( - (4:2)))),相同(F0(VEC),F3(VEC)))##刻度N < -  10 ^(1:7)时间< -  sapply(n,函数(n){v < -  sort(rnorm(n))system.time(f3(v))[[3]])图(时间〜n,type =“b”)

  3. 使用Microbenchmark.包的性能比较f0 ()F3()使用原始数据,vec
  4. 可以编译R代码,并且编译有助于执行非矢量化操作的最多F3()。用编译器:: cmpfun()编译F3(),并使用Microbenchmark比较结果。

    ##相对时间库(MicroBenchmark)Microbenchmark(F0(VEC),F3(VEC))
    ##单位:微秒## expr min lq平均值UQ max ## F0(VEC)15659.97 16503.977013131386 17468.60386 17468.60386 17468.603947.157 23199.554 28.08 30.08 30.08 30.08 30.08 30.08 30.9575 47.23053 47.764 52.141 113.544 52.141 113.544 ## neval ## 100#100
    f3c <- cmpfun(f3)微基准测试(f3(vec), f3c(vec))
    ##单位:微秒针## EXPR MIN LQ平均UQ MAX NEVAL ## F3(VEC)28.470 29.8355 33.27442 34.6335 36.2500 48.503 100 ## F3C(VEC)6.578 7.0270 7.85183 7.85183 7.6945

我们很可能通过用C编写二进制搜索算法来获得额外的速度,但是我们已经很满意性能的提高,所以我们不会再那样做了!

问丢失的是有用的F3()相比f0 ()。例如,算法是否在字符向量上工作?当向量包含时呢?NA价值观?如何治疗联系?

findInterval ()可能是解决原始问题的一种更好的内置方法,并适用于其他情况。我们的想法是把我们感兴趣的问题,0.,并找到指定的间隔vec它发生的地方。

f4 <- function(v, query=0)Eps, v) identical(f0(vec), f4(vec))
## [1]真实
Microbenchmark(F0(VEC),F3(VEC),F4(VEC))
## f3(vec) 28.265 30.392 44.537 43.5655 48.658 97.324 ## f4(vec) 13645.076 13698.410 13946.125 13777.6360 14172.281 15026.055 ## neval ## 100 ## 100 ## 100

事实上,它是灵活的和良好的测试意味着它往往是首选F3(),即使它不太快速。例如,比较使用10000不同点的查询所需的时间F3和迭代,与findInterval和矢量化。

Threshold <- rnorm(10000)相同(sapply(Threshold, f3, x=vec), f4(vec, Threshold))
## [1]真实
微基准测试(sapply(x, f3), f4(vec, x))
##单位:微秒## EXPR MIN LQ平均UQ ## SAPPLED(X,F3)38.121 40.8785 76.45288855 89.028 ## F4(vec,x)13650.604 13695.7095 13811.30469 13728.5565 13830.765/13830.765 ## MAX NEVAL ## 154.216 100 ##100.

一些实现有效算法的功能是sort ()(包括Radix Sort),匹配()(哈希表查找),和表格();这些可以在您自己的代码中有用。

得到教训:

  1. 选择算法可能非常重要
  2. 实现经典算法(如二进制搜索),即使在一天结束时,也可以是奖励学习体验,它可能更好地使用现有功能。
  3. 实现高效算法的内置R功能可能是更复杂的代码的重要构建块。

并行评估

案例研究:对齐读数的GC含量

这是一个先进的运动,谨慎行事

这个扩展的示例说明了如何计算跨几个BAM文件对齐读取的GC内容的分布。我们首先顺序处理一个BAM文件,然后并行处理许多BAM文件。

查找以下示例BAM文件的路径(这些小,但足够大以说明原则。

fls <- RNAseqData.HNRNPC.bam.chr14_BAMFILES . txt

限制和迭代来管理内存

BAM文件很大,所以不能适合内存。此外,我们最终将并行处理多个BAM文件,因此我们需要进一步管理处理每个BAM文件时消耗的内存量。我们采取方法。

第一个是迭代通过BAM文件中的块,足够大,可以从'速度矢量化计算中受益,但不太大,以消耗过多的内存。我们使用使用Bamfilelist()表示我们想输入大小的大小的块中的对齐读数

库(RSAMTools)BFL < -  BamFileList(FLS,ExecionSize = 100000)

每次从BAM文件读取时,我们都会输入下一个100,000条记录。我们将采用我们的第二次管理记忆策略限制除了对准坐标之外,从BAM文件读取从BAM文件读取的数据,特别是每个读取的DNA序列。我们将通过写作函数来完成此操作屈服()使用GenomicFiles :: Readgalign()输入所需资料;请参阅帮助页面,了解我们使用但您不理解的函数,例如:?scanbamparam()

图书馆(基因组)产量< - 函数(BFL){##输入一个块的对齐库(基因组)ReadgGalignments(BFL,Param = ScanBamparam(什么=“SEQ”))}

接下来,我们将把对齐的读取转换为GC内容。我们将使用Biostrings :: Letterfrequency()计算每次读取中G或C的比例,将这些数据列成表格,并计算每次读取的累计次数。

图书馆(Biostrings)地图< - 函数(ALN){#gc内容,垃圾箱&cummulate gc < -  letterfrequency(mcols(aln)$ seq,“gc”)表格(1 + gc,73)#max。阅读长度:72}

map ()将应用于返回的每个数据的结果屈服();我们写一个函数减少()结合了两个呼叫的结果map ()变成一个简单的总结。在我们的例子中,减少只是对两次连续呼叫的返回值的追求只是map ()

减少< - `+`

基因组夫妇包裹提供了一种方法可以一起缝合这些作品,特别是reduceByYield ()功能,在以下代码块中示出

图书馆(基因组)BF < -  Bamfile(FLS [1],ExectionSize = 100000)CrameyByield(BF,产量,地图,减少)
## [1] 0 0 0 0 0 0 0 0 0 0 0 ## [12] 0 1 1 4 24 41 87 238 490 907 1397 ## [23] 2127 3208 4706 6220 8737 11052 14680 17020 19360 21804 27047 ##[34] 29212 31249 35395 39807 40259 41722 42304 44108 44073 42317 41260 ## [45] 38372 35689 32447 27815 22153 18960 14188 10768 7887 6182 4817 ## [56] 3332 2101 1652 1455 860 459 235 116 73 34 22 ## [67] 6 4 0 0 0 0 0

上面打印出的结果是与0,1,...,73g或C核苷酸的数量对齐。任何时候都没有超过100,000个BAM记录,因此内存消耗是适度的。尽管如此,我们已处理整个文件。

并行评估

现在我们可以通过单个文件迭代以在适度的内存中生成GC内容,很容易并行处理所有文件:使用bpppply()调用reduceByYield ()在每个文件上,传递其他参数收益率地图,减少

图书馆(Biocomplallel)GC < -  BPLapply(BFL,Dreambyyield,产量,地图,减少)

结果是GC-Count Vectors的列表,每个文件的一个元素。

可视化

结果可以转换为adata.frame ()

库(GGPLOT2)DF < - 堆叠(as.data.frame(lapply(gc,cumsum)))df $ gc < -  0:72

和可视化,例如

库(GGPlot2)GGPlot(DF,AES(X = GC,Y =值))+ Geom_line(AES(Color = Ind))+ XLAB(“每个读取的GC核苷酸数”)+ Ylab(“读数数”)