Public property vs Accessor vs ArrayAccess vs Magic __set/__get/__isset

作了一个小测试, 主要是想看看不同的设计方式对于实际执行的影响.
PHP没有property,姑且把public field作为property好了.
使用Accessor替代public property有什么好处? 似乎很难有特别直接的解释,
按照我的理解, accessor最明显的用途是隔离和清洁, 避免脏数据的污染(但是实践中,很少用到).

对于PHP这种动态语言,对于纯粹的VO, 我认为一个native Array可能更好,没有必要弄一个纯粹的VO class.
所以,accessor应该至少是在DAO/Service或ActiveRecord来实现.

我倾向于Active Record模式, 基于AR的service层可以更为简洁.
缺点也很明显, 如何将AR对象剥离为纯粹的数据集是个比较麻烦的:数据的序列化不好处理,
做什么事都是有代价的,关键在于平衡.

跑题了,回到正题, 我们说的效率,有2种,一个是运行效率,一个是开发效率.
前者对于硬件,代码, 后者对于开发者,人.
我的观点是, 如果一定要取舍,应该优先开发效率,其次才是运行效率,毕竟,
硬件设备是更快更好更便宜的, 而人是最为昂贵的,从TCO来看, 减少人的投入才是关键.

不过,上述观点的前提是,运行效率是要具有一定优化的基础上进行的,
当不影响开发效率的时候,我们需要考虑运行效率, 以便选择合适的方案,
不仅代码的在本机的运行效率,还有考虑是否会影响扩展性,比如是否有利于scale out,而不是
只能scale up来进行扩展.

继续正文 :

SPL中的ArrayAccess都用过,可以以数组形式透明使用对象,看上去很美.
而Magick __set系列方法,则也可以很优雅的存取对象的attribute.
Accessor则是比较正统的方法.
Public属性是仅次于数组的值对象的构建方式.

这几个实现形式,对于实际运行的效率有和区别?

我想,多数人心里有答案,但是,是否完全正确?

测试代码:

class T1 {
public $t;
}

/**
* Test ArrayAccess
*/
class T2 implements ArrayAccess {
private $_vars = array();
//~~ArrayAccess
public function offsetExists($offset){
return isset($this->_vars[$offset]);
}
public function offsetGet($offset){
return $this->_vars[$offset];
}
public function offsetSet($offset,$value){
$this->_vars[$offset] = $value;
}
public function offsetUnset($offset){
unset($this->_vars[$offset]);
}
}

class T3 {
private $_vars=array();

public function __set($name,$value) {
$this->_vars[$name] = $value;
}

public function __unset($name) {
unset($this->_vars[$name]);
return;
}

public function __isset($name) {
return isset($this->_vars[$name]);
}
public function __get($name) {
return $this->_vars[$name];
}
}

class T4 {
private $_vars = array();
public function set($name,$value) {
$this->_vars[$name] = $value;
}
public function get($name) {
return $this->_vars[$name];
}
}

function bench($cnt) {
$obj1 = new T1();
$obj2 = new T2();
$obj3 = new T3();
$obj4 = new T4();

$start1 = microtime(true);
for ($i=0; $i < $cnt; $i++) {
$obj1->t = $i;
$obj1->t;
}
$end1 = microtime(true);

$start2 = microtime(true);
for ($i=0; $i < $cnt; $i++) {
$obj2['t'] = $i;
$obj2['t'];
}
$end2 = microtime(true);

$start3 = microtime(true);
for ($i=0; $i < $cnt; $i++) {
$obj3->t = $i;
$obj3->t;
}
$end3 = microtime(true);

$start4 = microtime(true);
for ($i=0; $i < $cnt; $i++) {
$obj4->set(’t',$i);
$obj4->get(’t');
}
$end4 = microtime(true);

$time1 = $end1-$start1;
$time2 = $end2-$start2;
$time3 = $end3-$start3;
$time4 = $end4-$start4;
echo “————— Bench result ( $cnt ) ————— \n”;
echo “Public property(Base) => $time1 \n”;
echo “ArrayAccess => $time2 \n”;
echo “Magic __set => $time3 \n”;
echo “Accessor => $time4 \n”;
echo “ArrayAccess vs Base => “.sprintf(”%+.2f”,($time2-$time1)/$time1*100).”%\n”;
echo “Magic __set vs Base => “.sprintf(”%+.2f”,($time3-$time1)/$time1*100).”%\n”;
echo “Accessor vs Base => “.sprintf(”%+.2f”,($time4-$time1)/$time1*100).”%\n”;
echo “ArrayAccess vs Accessor => “.sprintf(”%+.2f”,($time2-$time4)/$time4*100).”%\n”;
echo “Magic __set vs Accessor => “.sprintf(”%+.2f”,($time3-$time4)/$time4*100).”%\n”;
echo “Magic __set vs ArrayAccess => “.sprintf(”%+.2f”,($time3-$time2)/$time2*100).”%\n”;
}

bench(10000);
bench(50000);
bench(100000);
bench(500000);
bench(1000000);
?>

输出的结果:

Macintosh-2:doggy night$ php t.php
--------------- Bench result ( 10000 ) ---------------
Public property(Base) => 0.015399932861328
ArrayAccess => 0.082892894744873
Magic __set => 0.076795101165771
Accessor => 0.074687957763672
ArrayAccess vs Base => +438.27%
Magic __set vs Base => +398.67%
Accessor vs Base => +384.99%
ArrayAccess vs Accessor => +10.99%
Magic __set vs Accessor => +2.82%
Magic __set vs ArrayAccess => -7.36%
--------------- Bench result ( 50000 ) ---------------
Public property(Base) => 0.054196119308472
ArrayAccess => 0.3666570186615
Magic __set => 0.36188292503357
Accessor => 0.34752082824707
ArrayAccess vs Base => +576.54%
Magic __set vs Base => +567.73%
Accessor vs Base => +541.23%
ArrayAccess vs Accessor => +5.51%
Magic __set vs Accessor => +4.13%
Magic __set vs ArrayAccess => -1.30%
--------------- Bench result ( 100000 ) ---------------
Public property(Base) => 0.10039806365967
ArrayAccess => 0.729327917099
Magic __set => 0.71878385543823
Accessor => 0.68846893310547
ArrayAccess vs Base => +626.44%
Magic __set vs Base => +615.93%
Accessor vs Base => +585.74%
ArrayAccess vs Accessor => +5.93%
Magic __set vs Accessor => +4.40%
Magic __set vs ArrayAccess => -1.45%
--------------- Bench result ( 500000 ) ---------------
Public property(Base) => 0.4905378818512
ArrayAccess => 3.6530811786652
Magic __set => 3.6181490421295
Accessor => 3.3984169960022
ArrayAccess vs Base => +644.71%
Magic __set vs Base => +637.59%
Accessor vs Base => +592.79%
ArrayAccess vs Accessor => +7.49%
Magic __set vs Accessor => +6.47%
Magic __set vs ArrayAccess => -0.96%
--------------- Bench result ( 1000000 ) ---------------
Public property(Base) => 0.96968817710876
ArrayAccess => 7.2781820297241
Magic __set => 7.235552072525
Accessor => 6.9458839893341
ArrayAccess vs Base => +650.57%
Magic __set vs Base => +646.17%
Accessor vs Base => +616.30%
ArrayAccess vs Accessor => +4.78%
Magic __set vs Accessor => +4.17%
Magic __set vs ArrayAccess => -0.59%

说明:
数字是运行时间,数字越大表明运行越慢.
对比数据中, 正数是前者比后者运行速度要慢的百分比, 负数则表示前者比后者快.
分别测试了正常和一些极端情况下的比较.

和你的直觉是一样吗?

我稍微有点意外:
1. SPL的ArrayAccess比magic __set要慢
2. Accessor 和 __set 的差距没有那么想象中的那么明显

结论:
1. profiling你的代码, 如果可能,将瓶颈的accessor替换为public field,会有很大的提升. ( 我想没有必要担心不遵守约定的赋值行为,
当有人给一个object属性赋值为一个字符串, 抛出一个PHP致命错误并不是什么坏事,呵呵.)

2. 小心使用ArrayAccess, 看上去很美的伪数组和真正的数组差距还是很大的.

3. __set 可以用来充当通用的Accessor,毕竟二者差距在可忍受的范围之内. 或者, 你可以用代码生成来实现accessor.

忙着doggy 1.3开发

最近有点忙, 重写Doggy, 力争月底前完成1.3的第一个版本.
重构doggy的过程中,catalyst的源码也读了不少,有些许收获.
重构是可以说是痛并快乐的,很多事情都是在反复中前进, 所谓螺旋式上升.
一直强调TDD, 没有单元测试,重构是无法进行的.
不知道那些受我鼓吹快1年TDD的同事们能否做到? 严重怀疑…

在重构doggy中,写了不少perl代码, 算是享受. 彻底扔掉Phing了, 嗯,
当初选择Phing是由于需要植入很多java代码, 用多了Ant.
现在这些都被一个个perl小工具取代了.
上个版本用ZDE开发, 由于过多的代码提示, 忽略了最基本的简洁,
现在用TextMate, 虽然没有那么智能的代码提示, 但是更潜心注重代码的质量,开发效率反而提升了…
这是一种风格的转变, 从Java到Perl, 重量,大而全到小巧简单.

轻松一些, coding何必那么累?

因此,我将doggy 1.3的code name称之为:”New Soul”