使用记忆化优化你的 R 代码
使用记忆化优化你的 R 代码
本文翻译自《Optimize your R Code using Memoization》(有删减)
https://www.inwt-statistics.com/read-blog/optimize-your-r-code-using-memoization.html
本文介绍如何应用名为“记忆化(Memoization)”的编程技术来加速你的 R 代码并解决性能瓶颈。维基百科:
在计算中,... 记忆化是一种优化技术,主要用于通过存储代价高昂函数调用的结果,并在再次出现相同输入时返回缓存结果来加速计算机程序。
如果你想提升速度,并且只依赖该技术,你可以在 CRAN 上找到两个包 R.cache 和 memoise。下面我将提供不同版本的实现,一来说明记忆化不是魔法而是一种相当简单的技术;二来显示 R 可以远远快于 C++!
R 中的性能优化
如果我阅读到有关高性能计算的文字,在原始计算速度方面,R 似乎声名狼藉。感谢有了 Rcpp,可以尽可能简单地将 C++ 集成到你的 R 项目中。虽然不一定容易:你仍然需要学习一些 C++。我经常觉得围绕这个主题的讨论忽略了实现的成本其实包括方方面面,因此太快提出建议意味着你必须使用不同的语言。但是,我们经常可以使用普通的旧式 R 技术并提高运行时性能,同时易于实现。怎么做?通常通过重新定义问题(作弊),将计算分解成碎片(分而治之)或找到瓶颈并优化(最大限度地利用语言)!
当然,任何这些步骤都可能迫使我们最终使用 C++ 或其他东西。但是我们可以将这一点推向更远的地方,并用一些脑力和优化策略工具箱来弥补它。在优化方面有很多东西需要学习。记忆化可以成为你的一种技巧,可以神奇地使 R 代码运行得更快。这是通过避免不必要的计算来完成的,或者说不必要地计算两次。
R 何时变慢
要开始这个练习,让我们看看 R 何时变慢,以及随后我们如何改进它。我从 Rcpp 文档中获取了这个示例,该文档显然是为了回应 StackOverflow 上的一个问题而创建的,该问题询问为什么 R 的递归函数调用如此之慢。
挑战在于使用计算效率最低的定义之一来计算斐波那契数。当然,具体的例子并非如此。还有其他更有效的方法来计算斐波那契序列。但递归定义会让你的 CPU 风扇狂转,这就是它有趣的原因。在下文中,你可以找到算法的“菜鸟”实现。这两种语言看起来或多或少都是一样的。但是,正如你所看到的,耗时已经不同了。我们可以稍微改变 N,R 中的实现将快速达到它的极限。
N <- 35 ## the position in the fibonacci sequence to compute
fibRcpp <- Rcpp::cppFunction(
'
int fibonacci(const int x)
{
if (x == 0) return(0);
if (x == 1) return(1);
return (fibonacci(x - 1)) + fibonacci(x - 2);
}
')
fibR <- function(x)
{
## Non-optimised R
if (x == 0) return(0)
if (x == 1) return(1)
Recall(x - 1) + Recall(x - 2)
}
rbenchmark::benchmark(
baseR = fibR(N),
Rcpp = fibRcpp(N),
columns = c("test", "replications", "elapsed", "user.self"),
order = "elapsed",
replications = 1)
## test replications elapsed user.self
## 2 Rcpp 1 0.07 0.06
## 1 baseR 1 29.27 27.69
R 何时变(更)快
现在让我们看看我们如何定义一个函数,仍然计算相同的数字,避免所有不必要的计算。请注意,在递归定义中,要计算第 N 个数,我们必须计算第 N-1 和 N-2 个数。这将导致计算次数的爆炸。同时,如果我们想要计算 N = 35 的斐波那契数,并且我们已经得到 N = 34 和 N = 33 的结果,我们不必重新计算它们,我们只是使用我们已经知道的东西。让我们看看如何做到这一点:
fibRMemoise <- local(
{
memory <- list()
function(x)
{
valueName <- as.character(x)
if (!is.null(memory[[valueName]])) return(memory[[valueName]])
if (x == 0) return(0)
if (x == 1) return(1)
res <- Recall(x - 1) + Recall(x - 2)
memory[[valueName]] <<- res # store results
res
}
})
我们所做的:
- 检查是否已经知道了结果
- 如果已经知道,返回结果并在此停止(不做任何计算)
- 如果不知道,进行第 2 步
- 计算必要的结果(例如斐波那契数)
- 在我们退出函数之前记住结果
- 然后,返回结果
所以这个想法非常简单。另一个复杂性是,我们需要一个闭包。在这里,我们使用 local
。local
将创建一个新的作用域(环境)并运行该环境中的代码。因此,该函数可以访问该环境,即它可以访问内存,但是我们在全局环境中看不到内存:它是函数定义的本地内容。此外,我们需要超级赋值运算符(<<-
),以便我们可以对内存进行赋值。好吧,让我们看看除了抽象之外我们获得了什么,以及代码:
rbenchmark::benchmark(
baseR = fibR(N),
Rcpp = fibRcpp(N),
memoization = fibRMemoise(N),
columns = c("test", "replications", "elapsed", "user.self"),
order = "elapsed",
replications = 1)
## test replications elapsed user.self
## 3 memoization 1 0.00 0.00
## 2 Rcpp 1 0.04 0.04
## 1 baseR 1 32.03 31.67
看到没?R 竟然比 C++ 快!如果你有时间等待 C++ 实现,我们可以看到我们可以走多远,并且 C++ 中的实现快速达到极限。
N <- 50 # not very far, but with memoization Int64 is the limit.
rbenchmark::benchmark(
# baseR = fibR(N), # not good anymore!
Rcpp = fibRcpp(N),
memoization = fibRMemoise(N),
columns = c("test", "replications", "elapsed", "user.self"),
order = "elapsed",
replications = 1)
## test replications elapsed user.self
## 2 memoization 1 0.00 0.00
## 1 Rcpp 1 87.67 87.24
很棒,非常高效的黑科技。它还说明了为什么语言之间的性能比较就像比较苹果。是的,无论如何,R 就是快:)
R 中的记忆化
上述定义存在一些问题:它不是很通用。我们对记忆化的定义仍然与斐波那契数的定义有关。但是,我们可以定义一个更高阶的函数,它将记忆化与算法分开:
memoise <- function(fun)
{
memory <- list()
function(x)
{
valueName <- as.character(x)
if (!is.null(memory[[valueName]])) return(memory[[valueName]])
res <- fun(x)
memory[[valueName]] <<- res
res
}
}
这是该技术的完美定义,并且不是很长或很复杂。原则上,你会在 R.cache
和 memoise
中找到同样的东西。显然,这两个包添加了一些功能,例如,你想如何以及在何处存储内存,也许是在磁盘上。上述函数只允许一个参数,这个问题也在上述两个包中得到解决。上述两个包还添加了其他一些有用的东西。
何时使用记忆化
何时以及为何要使用记忆化?它不像实现类似斐波纳契序列之类的日常任务。即使如此,我们也会采用不同的方式。我想到的实际用例是完全不同的。这里给你一些想法:
- 我们可以减少针对 API 的调用次数。大多数提供商(例如 Google 运营的 Google Maps)将限制你每天允许的调用次数。你可以使用记忆化快速构建内存或磁盘缓存。这将允许你快速切换回“旧”配置,而无需再次查询 API。
- 调用数据库或常规加载数据。想想一个 Shiny 应用程序,其中 UI 的更改将触发对数据库的调用。例如,当你有参数化查询时。你缓存这些查询结果后,当用户在设置之间来回切换时,你可以大大加快应用程序的速度。
Whatever we do there needs to be an important property so that memoization is useful. Wikipedia says:
无论我们做什么,有一个重要的属性是必要的,以便保证记忆化有用。维基百科:
一个函数只有在引用透明的情况下才能被记忆,也就是说,只有在调用函数与使用其返回值替换该函数调用具有完全相同的效果时。(但是,存在排除这种限制的特例。)
换句话说:我们需要留意函数的结果实际上只取决于输入参数。你是否相信你的数据库连接或 API 调用具有此属性?如果是这样,记忆化可能很有用。但要小心:记忆化导致缓存,缓存导致状态管理(何时以及如何更新缓存?),这导致难以调试的问题:确实很有趣。
使用记忆化优化你的 R 代码的更多相关文章
- POJ 1088 滑雪 记忆化优化题解
本题有人写是DP,只是和DP还是有点区别的,应该主要是记忆化 Momoization 算法. 思路就是递归,然后在递归的过程把计算的结果记录起来,以便后面使用. 非常经典的搜索题目,这样的方法非常多题 ...
- 记忆化搜索 codevs 2241 排序二叉树
codevs 2241 排序二叉树 ★ 输入文件:bstree.in 输出文件:bstree.out 简单对比时间限制:1 s 内存限制:128 MB [问题描述] 一个边长为n的正三 ...
- Codeforces Round #336 (Div. 2) D. Zuma 记忆化搜索
D. Zuma 题目连接: http://www.codeforces.com/contest/608/problem/D Description Genos recently installed t ...
- UVa 11762 Race to 1 (数学期望 + 记忆化搜索)
题意:给定一个整数 n ,然后你要把它变成 1,变换操作就是随机从小于等于 n 的素数中选一个p,如果这个数是 n 的约数,那么就可以变成 n/p,否则还是本身,问你把它变成 1 的数学期望是多少. ...
- HDU 4444 Walk (离散化建图+BFS+记忆化搜索) 绝对经典
题目地址:http://acm.hdu.edu.cn/showproblem.php?pid=4444 题意:给你一些n个矩形,给你一个起点,一个终点,要你求从起点到终点最少需要转多少个弯 题解:因为 ...
- HDU 1428 漫步校园 (BFS+优先队列+记忆化搜索)
题目地址:HDU 1428 先用BFS+优先队列求出全部点到机房的最短距离.然后用记忆化搜索去搜. 代码例如以下: #include <iostream> #include <str ...
- 【动态规划】【记忆化搜索】【搜索】CODEVS 1262 不要把球传我 2012年CCC加拿大高中生信息学奥赛
可以暴力递归求解,应该不会TLE,但是我们考虑记忆化优化. 设f(i,j)表示第i个数为j时的方案数. f(i,j)=f(1,j-1)+f(2,j-1)+……+f(i-1,j-1) (4>=j& ...
- poj 1191 棋盘切割 (压缩dp+记忆化搜索)
一,题意: 中文题 二.分析: 主要利用压缩dp与记忆化搜索思想 三,代码: #include <iostream> #include <stdio.h> #include & ...
- 非常完整的线性DP及记忆化搜索讲义
基础概念 我们之前的课程当中接触了最基础的动态规划. 动态规划最重要的就是找到一个状态和状态转移方程. 除此之外,动态规划问题分析中还有一些重要性质,如:重叠子问题.最优子结构.无后效性等. 最优子结 ...
随机推荐
- 微信OAuth2.0网页授权php示例
1.配置授权回调页面域名,如 www.aaa.com 2.模拟公众号的第三方网页,fn_system.php <?php if(empty($_SESSION['user'])){ header ...
- PHP微信授权登录信息
文件1:index.php //换成自己的接口信息 $appid = 'XXXXX'; header('location:https://open.weixin.qq.com/connect/oaut ...
- 无法访问windows安装服务。发生这种情况的可能是您在安全模式下运行windows,或是没有正确安装windows安装,。请与技术支持人员联系以获得帮助。
解决办法: 1.命令提示符下输入:msiexec/regserver 2.在“管理工具”→“服务”中启动windows Installer 程序员的基础教程:菜鸟程序员
- WebAPI如何返回json
public HttpResponseMessage PostUser(User user) { JavaScriptSerializer serializer = new JavaScriptSer ...
- 洛谷 P3478 [POI2008]STA-Station
题目描述 The first stage of train system reform (that has been described in the problem Railways of the ...
- Mysql加载本地CSV文件
Mysql加载本地CSV文件 1.系统环境 系统版本:Win10 64位 Mysql版本: 8.0.15 MySQL Community Server - GPL Mysql Workbench版本: ...
- HBase数据读写流程(1.3.1)
===数据写入流程=== 源码:https://github.com/apache/hbase/blob/master/hbase-server/src/main/java/org/apache/ha ...
- Oracle 用户验证日志
1.sysdba/sysoper 权限用户验证日志;2.非sysdba/sysoper 权限用户验证日志;3.关于sqlcode; 1.sysdba/sysoper 权限用户验证日志:在数据库设置了参 ...
- java 中toString()方法详解
1.toString()方法 Object类具有一个toString()方法,你创建的每个类都会继承该方法.它返回对象的一个String表示,并且对于调试非常有帮助.然而对于默认的toString() ...
- 实践作业3:白盒测试----学习Junit框架DAY10.
JUnit - 测试框架 首先应该了解什么是 Junit 测试框架? JUnit 是一个回归测试框架,被开发者用于实施对应用程序的单元测试,加快程序编制速度,同时提高编码的质量.JUnit 测试框架能 ...