在众多编程语言中,R语言是典型的运行慢和耗内存。当数据表比较庞大(比如一个数据集达100G),而内存有限时(比如一台普通电脑内存16G),使用R语言一次读入和处理,常规做法完全不可行。即使调大虚拟内存(swap空间),使用memory.limit(Windows系统)或 ulimit -s -v(Linux系统)等操作(虚拟内存其实很慢),即使再辅之以rm()和gc()及时清理内存(个人感觉效果甚微),也会让电脑运行陷入无止尽的卡顿中。当硬件设备有限时,我们可以使用并行运算和分段技巧应对这种情形。

R语言中一般使用parallel包完成并行运算,具体写法此处不赘述,请自行查阅相关资料。建议使用Linux或Mac执行并行运算,因为可以使用FORK模式共享主程序的变量从而节约内存。并行运算不仅可以解决运行速度慢的问题,还可以用于及时回收内存,效果明显优于rm和gc(R语言的内存管理特点请参考相关资料,值得注意的是,同名变量有可能出现多份拷贝)。当我们估计某一段代码会产生较多的临时变量时,可以将这段代码放入并行运算中(新开一个集群cluster),运行完后关闭cluster即可回收所有内存。如果某段代码完整运行完需要大量内存,担心内存不够用时,可以再将代码分段(每运行完一段就回收一次cluster),比如按col将数据表分成100份,原本需要60G内存的操作,可优化到仅需6G。简言之,并行结合分段,可以将CPU和内存的使用优化到非常舒服的状态。

另外,将中间结果保存到硬盘,也是一种节约时间和内存的技巧。一方面,当需要重新调用程序,则可以直接“读档”(中间结果)来节约时间;另一方面,由于产生中间结果时,已经消耗了较多内存而又无法很好得回收内存时,重启R再读档中间结果,则可回收大量内存。注意,当中间结果较大时,应该分段保存(比如将8G的数据表分成16份)。读取时,若内存够用,则可使用多个CPU同时读取再合并;若担心读取时内存不够用,再使用分段的技巧读取。由于读取再合并结果后,实际上总共会有2份数据,若担心rm和gc回收效果不佳,则可将读取和合并的操作放入新的cluster中,合并后关掉cluster即可。

简单的“存档”和“读档”代码示例如下

library(dplyr)
library(parallel)

# Save data_Methy
cl <- makeCluster(1, type="FORK")
out<-parLapply(cl,1,function(j){
  temp<-list()
  for (i in 1:8){
    start<-(i-1)*36+2
    end<-i*36+1
    batch_col<-c(1,c(start:end))
    temp[[i]]<-data_methy_m %>% select(c(1,batch_col))
  }
  return(temp)
})[[1]]
stopCluster(cl)

x<-1:8
cl <- makeCluster(8, type="FORK")
parLapply(cl,x,function(i){
  write.table(out[[i]],file=paste("Methylation/Methy_beta_",as.character(i),".tsv",sep=""),row.names = F,sep="\t")
  return(0)
})[[1]]
stopCluster(cl)

# Load data_Methy
cl <- makeCluster(1, type="FORK")
data_methy_m<-parLapply(cl,1,function(j){
  for (k in 1:8){
    cl_2 <- makeCluster(1, type="FORK")
    results<-parLapply(cl_2,k,function(i){
      temp<-read.table(paste("Methylation/Methy_beta_",as.character(i),".tsv",sep=""),sep="\t",header=T) %>% as_tibble()
      if (i>1){
        temp<-inner_join(results,temp,by="Composite.Element.REF")
      }
      return(temp)
    })[[1]]
    stopCluster(cl_2)
  }
  return(results)
})[[1]]
stopCluster(cl)