PHP Memoization
经常会遇到这种情况,比如一个读文件内容的类,有个比较耗时的方法 getContent,假设这个方法对于固定参数返回的内容是固定的,比如:
class FileReader { public function getContent($file) { $content = // open -> read -> do some decode return $content; } } $r = new FileReader(); $r->readContent('a.txt'); $r->readContent('a.txt'); $r->readContent('a.txt');
这样重复调用开销比较大的,然后就加了个cache:
class FileReader { private $cache = array(); public function getContent($file) { if($cache[$file]) { return $cache[$file]; } $cache[$file] = // open -> read -> do some decode return $cache[$file]; } }
这样好很多,问题是
1. 破坏了getContent的功能单一性
2. 可读性
3. 还加了个类成员变量$cache
需要一种方法能把cache机制抽象出来,类似js的memoize(见参考1)
function create_memoized_class($new_class, $base, $memo_funcs) { if(is_string($memo_funcs)) return create_memoized_class($new_class, $base, array($memo_funcs)); static $NON_STATIC_FUNC = 1;//01b static $STATIC_FUNC = 2;//10b $ref = new ReflectionClass($base); $type = 0; foreach($memo_funcs as $f) { if(!$ref->hasMethod($f)) throw new Exception("function '$f' not found in class '$base'"); $m = $ref->getMethod($f); $type = $type | ($m->isStatic() ? $STATIC_FUNC : $NON_STATIC_FUNC); } $code = array( "class $new_class extends $base {" ); if($type & $NON_STATIC_FUNC) $code[] = "private \$cache = array(); function __call(\$m, \$v) { \$k = md5(serialize(func_get_args())); if(!array_key_exists(\$k, \$this->cache)) { \$this->cache[\$k] = call_user_func_array(array('parent', \$m), \$v); } return \$this->cache[\$k]; }"; if($type & $STATIC_FUNC) $code[] = "private static \$s_cache = array(); public static function __callStatic(\$m, \$v) { \$k = md5(serialize(func_get_args())); if(!array_key_exists(\$k, self::\$s_cache)) { self::\$s_cache[\$k] = call_user_func_array(array('parent', \$m), \$v); } return self::\$s_cache[\$k]; }"; foreach($memo_funcs as $f) { $m = $ref->getMethod($f); $prefix = $m->isPrivate() ? 'private' : ($m->isPublic() ? 'public' : 'protected'); if($m->isStatic()) { $code[] = "$prefix static function $f() { return self::__callStatic('$f', func_get_args()); }"; } else { $code[] = "$prefix function $f() { return \$this->__call('$f', func_get_args()); }"; } } $code[] = "}"; $code = implode('', $code); // debug($code); eval($code); }
用法:
//测试 class Person { var $job = "coding"; function __construct($name) { debug("a person created: [$name]"); } function work($time) { debug("now: $time, sleep 1 hour first..."); sleep(1); debug("Hi, I'm $this->job"); return "going home"; } public static function getInstance($name) {//原Singleton模式可被简化成这样 return new Person($name); } } create_memoized_class("LazyPerson", "Person", array("work", "getInstance")); //非静态方法,没问题 $z = new LazyPerson('zz'); $z->work('9:00'); $z->work('9:00');//成功cache住 //静态方法,没问题 $x = LazyPerson::getInstance('aj'); $y = LazyPerson::getInstance('aj'); debug($x===$y); //true //把上面2个放一起问题就来了 debug(LazyPerson::getInstance('aj')->work('9:00')); debug(LazyPerson::getInstance('aj')->work('9:00'));//没有cache住,原始work被调用2次 //原因是getInstance里new出来的是原始Person,而不是LazyPerson。 //解决方法: 让Person::getInstance 返回 LazyPerson。
原理:动态eval出一个新的类,挂钩其中关键函数,加上cache机制
需注意点:
1. 被cache的方法不能是private的,至少需要改成protected,原因是本机制是通过extend实现,所以原函数至少得是protected的
2. Singleton和其他方法一起cache的话,getInstance中返回的实例必须是cache过的类,否则很容易出现陷阱,看似有cache功能实际无效而且很隐蔽不会报错,见上面代码
可改进的地方:
1. 目前的key是通过 $k = md5(serialize(func_get_args())); 生成的,如果函数参数复杂的话,可以扩展一下,自定义一个 key_gen
2. 可以把cache存放处抽象出来,比如存到memcache中,或者文件
参考:
1. JS 的 memoization 机制:http://www.cnblogs.com/aj3423/archive/2011/02/28/3150514.html
2. PHP普通函数的memoization: http://stackoverflow.com/questions/3540403/caching-function-results-in-php
3. 原讨论帖: http://topic.csdn.net/u/20110916/14/e2bb3043-44e6-45fa-a4f0-631bc168a1d2.html
4. 一个用于 memoize 的 php 插件: https://github.com/arraypad/php-memoize