3  数据处理效能的衡量

效能是指使用行为目的和手段方面的正确性与效果方面的有利性。衡量数据处理效能的依据是计算效率、实现效果和获得效益,是对数据科学过程的效率、效果的概括性、综合性评价。总体来说,可以通过时间和空间两个维度对数据处理效能进行评价。一般来说,在保证结果真实准确的前提下,数据处理的时间越短、使用空间越少,那么数据处理的效能就越高。

3.1 时间衡量

任意特定操作、函数或一组代码的执行都需要一定的时间,在保证代码能够正确工作前提下,应该对R代码进行优化,使其执行得更快。在R中,对代码的时间进行衡量非常重要,因为这个步骤可以获知代码中运行缓慢和低效的部分,从而识别瓶颈,然后进行代码优化,进而提高程序的性能。因此,如何在R中对代码进行计时,对开发者来说是一项非常重要的技能。

在R中,能够实现计时的工具非常多,本书不会穷举所有的方法,而是对最常见而实用的方法进行介绍。这里我们统一使用Sys.sleep来构造测试代码,Sys.sleep是系统里面用于暂停制定时间的函数,可以让脚本运行暂停一定的秒数。首先,如果想要对某一段代码运行的时间进行衡量,可以使用tidyfst包的pst函数:

library(tidyfst)
Thank you for using tidyfst!
To acknowledge our work, please cite the package:
Huang et al., (2020). tidyfst: Tidy Verbs for Fast Data Manipulation. Journal of Open Source Software, 5(52), 2388, https://doi.org/10.21105/joss.02388
pst({
  Sys.sleep(0.5) # 暂停0.5秒的时间
})
[1] "# Finished in 0.510s elapsed (0.000s cpu)"

在上面给出的结果中,首先给出的是实际系统运行时间(Wall-Clock Time),随后在括号内给出的时间是CPU运行时间(CPU Time)。CPU运行时间指的是CPU实际用于处理某个程序的时间。这个时间只计算CPU在执行这个特定程序或进程上的工作时间,不包括系统处理其他任务的时间(如输入/输出操作、磁盘操作或网络通信)的时间。实际系统运行时间是从程序开始到程序结束所经过的总时间,就像是用墙上的时钟来度量时间一样。这个时间包括了所有的等待和延迟时间,比如CPU切换到其他任务、数据从硬盘加载、网络延迟等。因此,这个时间通常会比CPU运行时间长,因为它包括了程序执行中所有可能的等待时间。

有的时候,我们认为仅仅利用一次运行的计时结果不够稳定,可以增加运行的次数。在这种情况下,使用microbenchmark包的microbenchmark函数。比如我们想要让代码重复5次,可以这样操作:

library(microbenchmark)
microbenchmark(Sys.sleep(0.1), # 暂停0.1秒的时间
               times = 5)  # 重复运行5次
Unit: milliseconds
           expr      min       lq     mean   median       uq     max neval
 Sys.sleep(0.1) 108.5984 108.8506 109.6036 109.2285 109.5447 111.796     5

得到的结果中,“Unit”部分声明了本次代码执行时间衡量的时间单位(“milliseconds”代表时间单位为微秒),expr代表执行的代码,neval代表代码执行的次数,而meanmedian分别代表多次执行中时间的平均值和中位数,minmax则给出了执行时间的最小值和最大值。 此外,microbenchmark函数还可以比较不同代码的运行时间长短,实现方法如下:

microbenchmark(Sys.sleep(0.1), # 暂停0.1秒的时间
               Sys.sleep(0.2), # 暂停0.2秒的时间
               times = 5) # 各自重复运行5次
Unit: milliseconds
           expr      min       lq     mean   median       uq      max neval
 Sys.sleep(0.1) 107.9187 108.5331 108.7826 108.5972 109.3027 109.5615     5
 Sys.sleep(0.2) 201.7077 202.2759 205.4304 204.4330 204.9903 213.7449     5

3.2 空间衡量

一般来说,R将所有对象存储在计算机的物理内存(RAM,Random-access Memory)中进行操作。如果我们的计算机的物理内存不足以处理一些工作,那么就需要使用一些新的方法来对其进行处理。因此,了解计算环境的可用内存限制对我们而言至关重要。在使用R时值得注意的第一件事情是,您的计算机实际上有多少物理内存。通常情况下,我们可以通过查看操作系统的设置来了解这一点。举例来说,如果我们有一个内存为8GB的笔记本电脑,那么R可用的RAM量将远远小于此值,即上限是无法达到8G的。如果我们计划在这台笔记本上读入一个占用16GB内存的对象,那么我们需要换一台内存更大的计算机才有可能实现。

在R中,pryr包提供了一系列函数来对R中的内存使用情况进行分析。首先,我们可以使用object_size函数来测度某一个对象占用了多少内存:

library(pryr)

Attaching package: 'pryr'
The following object is masked from 'package:tidyfst':

    object_size
a = rnorm(1e5) # 生成10万个服从正态分布的随机数,赋值给a
object_size(a) # 查看变量a占据了多少内存
800.05 kB

如果我们想要看整个R当前所占的总内存数量,那么可以使用mem_used函数:

mem_used()
53.5 MB

有的时候,我们需要知道经过某一步操作之后,我们R占用内存的变化是多少,可以使用mem_change函数来完成这一步操作:

mem_change(rm(a)) # 把a变量移除后,R所占内存数量的变化
-800 kB

注意,如果结果带有负号,说明R所占用内存减少了,否则就是增加了。

3.3 综合衡量

在上面的章节中,我们分别讲述了如何在R中对一些操作完成的时间和占用的内存进行测量。实际上,这两个步骤可以同时完成。使用bench包的mark函数可以实现这一过程,比如我们要对一段代码运行的时间和空间花销进行测度,可以这样操作:

library(bench)
mark(a = rnorm(1e5))
# A tibble: 1 × 6
  expression      min   median `itr/sec` mem_alloc `gc/sec`
  <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
1 a            2.55ms   2.81ms      349.     781KB     4.08

上面的代码会对表达式执行若干次,然后进行效能的衡量。在返回结果中,median代表若干次迭代中时间花销的中位数,mem_alloc代表在运行表达式时,R分配的内存总量;而itr/sec则告诉我们每秒可以对该表达式执行多少次。 mark函数不仅可以对一个表达式的效能进行测量,还可以对多个表达式的效能进行比较。一般来说,默认情况下要求不同表达式的返回值必须是一致的,但是我们可以通过把check参数设置为FALSE来避免这一默认设置。实现方法如下:

mark(
  a = rnorm(1e5),
  b = rnorm(1e5 + 1),
  c = rnorm(2e5),
  check = FALSE
)
# A tibble: 3 × 6
  expression      min   median `itr/sec` mem_alloc `gc/sec`
  <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
1 a            2.54ms    2.8ms      345.   781.3KB     6.24
2 b            2.55ms   2.79ms      346.   781.3KB     4.09
3 c            5.11ms   5.61ms      175.    1.53MB     6.39

以上代码利用mark函数对比了3个表达式的效能。

3.4 小结

在本章中,我们解释了什么是R语言中执行代码的效能,并给出一系列方法来对执行R代码时的时间和空间花销进行衡量。拥有这些工具,我们就可以较为精准地对R代码进行评估,看看其是否高效地完成了我们所需要的功能。