浅析Memoization

Memoization,简单的说就是优化计算机的性能,缓存那些重复性的函数操作和计算,使得第一次之后的调用可以直接从缓存中得到结果,而无需重新计算和运行复杂、费时的函数。这个跟Lazy Definition的原理是比较相似的,Lazy Definition主要是对函数进行重复定义,避免浏览器检测等恶心的事情。在javascript里实现Memoization的技术,Keith Gaughan做了相关的叙述:Memoization in JavaScript。特别是它对Fibonacci的优化让我特别的玩味。

对于Fibonacci普通的实现方式是:

//这个性能不咋的,函数调用太频繁了
function Fib(n) {
    if (n < 2) { return n;}
    return Fib(n - 1) + Fib(n - 2);
}

Keith Gaughan对它的实现方式用Memoization进行了优化:

var IterMemoFib = function() {
    var cache = [1, 1];
    var fib = function(n) {
        if (n >= cache.length) {
            for (var i = cache.length; i <= n; i++) {
                //这句代码很耐人寻味,还有循环的方式
                cache[i] = cache[i - 2] + cache[i - 1];
            }
        }
        return cache[n];
    }
    return fib;
}();

上面关于Fibonacci的优化,就是使用了Memoization技巧,将上一步的加法操作缓存起来,用户循环中下一轮的递增。缓存计算结果是Memorize应用最多的需求,避免了对计算结果的重复性计算,但是如果计算结果是动态的呢,或者说你想获取动态修改过后的计算结果呢? 比如DOM的length属性,这个是易变的,这个时候就不能去获取缓存的结果了,需要重复计算。

下面先来看一段Flagrant Badassery编写的一个Timed Memoization函数,一方面用于缓存结果,另一方面则是在时间参数指定的时间间隔里清除掉缓存的结果:

var memorize=function(fn,timer){
  var obj = {};
  return function(){
    var key = [].join.call(arguments,"$");
	if(timer){
	     setTimeout(function(){delete obj[key];},timer);
	}
        //这个返回方式本人进行了一点修改
	return (key in obj) ? obj[key] : obj[key] = fn.apply(this,arguments);
  }
}

上面的代码思路不错,在指定的时间之后清楚缓存结果,但是我觉得这个思路还可以用于更多场合,比如清除对象,及时断开作用域链,使得无用的对象可以及时的回收占用的内存。基于这个思路,本人做了些修修补补,编写了下面的几个方法:

//将memorize函数生成的函数也缓存起来
var memorize2 = function(fn,timer){
  var key = [].join.call(arguments,"$"),obj={};
  return memorize2[key] ? memorize2[key] : (memorize2[key] = function(){
    var k = [].join.call(arguments,"$");
	if(timer){
	  setTimeout(function(){delete obj[k];delete memorize2[key];},timer);
	}
	return obj[k] ? obj[k] : (obj[k] = fn.apply(this,arguments);
  }
}

对于上面说到的,如果有些计算结果是会动态改变的,这时候就该在需要的时候进行重复计算,为此,编写了一个Function扩展,借鉴上面Timed Momeization的思路:

Function.prototype.memorize = function(timer){
  var obj = {}, that = this, key = "$";
  return function(flag){
    var args = [].slice.call(arguments,1) || [];
	if(flag){
	  return obj[key] = that.apply(this,args);
	}
	key = [].join.call(arguments,"$");
	if(timer){
	  setTimeout(function(){delete obj[key];},timer);
	}
	return obj[key] ? obj[key] : obj[key] = that.apply(this,args);
  }
}

如上面代码所示,使用memorize返回的函数中带有一个flag参数,如果不存在这个参数或者设置为false,则从缓存中去获取上一步缓存的结果,如果设置为true,则重复计算、并重新设置缓存的值。通过这样的实现方式,既可以通过参数的形式来判断获取动态改变的结果,也可以在不需要动态改变参数的时候获取缓存的结果,《测试用例》。使用方式如下:

var length = function(){
  return div.getElementsByTagName("p").length;
}.memorize();

为此,Memoization对于一些静态的计算结果进行缓存,从而起到优化代码性能的作用;但是对于会动态改变的计算结果,则需要另当别论了。

更多的参考文章:《ONE-LINE JAVASCRIPT MEMOIZATION》,《Memoization in JavaScript》,《Timed Memoization》,《Javascript的Lazy Definition Pattern

大家的手印:

  1. Kevin2010年07月14日于2:47 下午

    博主,你的空间是哪里的呀,速度很快呀

  2. 淘宝2010年07月17日于7:48 下午

    很喜欢你写的文章,我可以转载吗?

  3. 淘宝2010年07月18日于11:45 下午

    好文章,喜欢!希望继续有这种深度的文章,支持一下

你的手印呢?