Static Method Abuse
When I began taking over the web development project at work, I noticed a developer using a lot of static members and methods in his class definitions. His explanation was that it was an optimization he used to improve performance. Unfortunately, he had no metrics to back the statement up. So I set out to do some of my own.
The developer was using static methods and members in abstract classes. Here is a very simple example:
abstract class Model_Abstract
{
static $db;
public static function setDb($db)
{
self::$db = $db;
}
}
This was complimented by a call in the MVC bootstrap:
$db = new Db;
Model_Abstract::setDb($db);
The developer explained to me that this forces each concrete model class to use the same database class. He said his reason for doing this was that PHP's copy-on-write functionality made a copy of the database object each time the internal results of that class changed.
I ran some tests (results below) to see if this was true and it, partially, was. If any part of a class changes, PHP performs a shallow copy. That is, if a database class contains a result class, only the result class will be copied and changed if a select query is performed. The internal reference counter of the database class will simply be incremented.
I also tested whether static members and methods circumvented the shallow copy and they do. However, this is not without confusion. The results from the database class must be stored somewhere else before another query is made. The next query will overwrite the previous results and cause some real confusion if someone forgets to do this.
This whole mess started as a code optimization technique. It looked to like pre-optimization on code that was never that slow to begin with. The developer thought that a deep copy of the class was being performed, but never actually bothered to check it out. The amount of time and resources saved by using the static approach is infinitesimal that it is definitely not worth it.
I have a small framework that I intentionally wrote this into. In a future post, I will be using this to outline the steps I used to refactor the code.
Tests
Non-static test:class Result
{
private $_result;
public function setResult($result)
{
$this->_result = $result;
}
}
class Db
{
private $_sql;
private $_result;
public function __construct()
{
$this->_result = new Result;
}
public function query($sql)
{
$this->_sql = $sql;
$this->_result->setResult(rand());
}
}
class Model
{
private $_db;
public function __construct($db)
{
$this->_db = $db;
}
public function test()
{
$res = $this->_db->query('SELECT bar FROM foo');
debug_zval_dump($this->_db);
}
}
$db = new Db;
debug_zval_dump($db);
$model = array();
$before = memory_get_usage();
for($i = 0; $i < 10; $i++) { $model[$i] = new Model($db); $model[$i]->test();
}
$after = memory_get_usage();
echo "Change in memory usage: ", $after - $before;
object(Db)#1 (2) refcount(2){ ["_sql":"Db":private]=> NULL refcount(2) ["_result":"Db":private]=> object(Result)#2 (1) refcount(1){ ["_result":"Result":private]=> NULL refcount(2) } } object(Db)#1 (2) refcount(3){ ["_sql":"Db":private]=> string(19) "SELECT bar FROM foo" refcount(1) ["_result":"Db":private]=> object(Result)#2 (1) refcount(1){ ["_result":"Result":private]=> long(1866838064) refcount(1) } } object(Db)#1 (2) refcount(4){ ["_sql":"Db":private]=> string(19) "SELECT bar FROM foo" refcount(1) ["_result":"Db":private]=> object(Result)#2 (1) refcount(1){ ["_result":"Result":private]=> long(282674262) refcount(1) } } object(Db)#1 (2) refcount(5){ ["_sql":"Db":private]=> string(19) "SELECT bar FROM foo" refcount(1) ["_result":"Db":private]=> object(Result)#2 (1) refcount(1){ ["_result":"Result":private]=> long(415846557) refcount(1) } } object(Db)#1 (2) refcount(6){ ["_sql":"Db":private]=> string(19) "SELECT bar FROM foo" refcount(1) ["_result":"Db":private]=> object(Result)#2 (1) refcount(1){ ["_result":"Result":private]=> long(459928359) refcount(1) } } object(Db)#1 (2) refcount(7){ ["_sql":"Db":private]=> string(19) "SELECT bar FROM foo" refcount(1) ["_result":"Db":private]=> object(Result)#2 (1) refcount(1){ ["_result":"Result":private]=> long(46535217) refcount(1) } } object(Db)#1 (2) refcount(8){ ["_sql":"Db":private]=> string(19) "SELECT bar FROM foo" refcount(1) ["_result":"Db":private]=> object(Result)#2 (1) refcount(1){ ["_result":"Result":private]=> long(1038889971) refcount(1) } } object(Db)#1 (2) refcount(9){ ["_sql":"Db":private]=> string(19) "SELECT bar FROM foo" refcount(1) ["_result":"Db":private]=> object(Result)#2 (1) refcount(1){ ["_result":"Result":private]=> long(1206550183) refcount(1) } } object(Db)#1 (2) refcount(10){ ["_sql":"Db":private]=> string(19) "SELECT bar FROM foo" refcount(1) ["_result":"Db":private]=> object(Result)#2 (1) refcount(1){ ["_result":"Result":private]=> long(144575963) refcount(1) } } object(Db)#1 (2) refcount(11){ ["_sql":"Db":private]=> string(19) "SELECT bar FROM foo" refcount(1) ["_result":"Db":private]=> object(Result)#2 (1) refcount(1){ ["_result":"Result":private]=> long(1310083917) refcount(1) } } object(Db)#1 (2) refcount(12){ ["_sql":"Db":private]=> string(19) "SELECT bar FROM foo" refcount(1) ["_result":"Db":private]=> object(Result)#2 (1) refcount(1){ ["_result":"Result":private]=> long(1850865612) refcount(1) } } Change in memory usage: 4996
Static test:
class Result
{
private $_result;
public function setResult($result)
{
$this->_result = $result;
}
}
class Db
{
private $_sql;
private $_result;
public function __construct()
{
$this->_result = new Result;
}
public function query($sql)
{
$this->_sql = $sql;
$this->_result->setResult(rand());
}
}
class Model
{
private static $_db;
static public function setDb($db)
{
self::$_db = $db;
}
public function test()
{
$res = self::$_db->query('SELECT bar FROM foo');
debug_zval_dump(self::$_db);
}
}
$db = new Db;
debug_zval_dump($db);
Model::setDb($db);
debug_zval_dump($db);
$model = array();
$before = memory_get_usage();
for($i = 0; $i < 10; $i++) { $model[$i] = new Model(); $model[$i]->test();
}
$after = memory_get_usage();
echo "Change in memory usage: ", $after - $before;
object(Db)#1 (2) refcount(2){ ["_sql":"Db":private]=> NULL refcount(2) ["_result":"Db":private]=> object(Result)#2 (1) refcount(1){ ["_result":"Result":private]=> NULL refcount(2) } } object(Db)#1 (2) refcount(3){ ["_sql":"Db":private]=> NULL refcount(2) ["_result":"Db":private]=> object(Result)#2 (1) refcount(1){ ["_result":"Result":private]=> NULL refcount(2) } } object(Db)#1 (2) refcount(3){ ["_sql":"Db":private]=> string(19) "SELECT bar FROM foo" refcount(1) ["_result":"Db":private]=> object(Result)#2 (1) refcount(1){ ["_result":"Result":private]=> long(1722572263) refcount(1) } } object(Db)#1 (2) refcount(3){ ["_sql":"Db":private]=> string(19) "SELECT bar FROM foo" refcount(1) ["_result":"Db":private]=> object(Result)#2 (1) refcount(1){ ["_result":"Result":private]=> long(2057122892) refcount(1) } } object(Db)#1 (2) refcount(3){ ["_sql":"Db":private]=> string(19) "SELECT bar FROM foo" refcount(1) ["_result":"Db":private]=> object(Result)#2 (1) refcount(1){ ["_result":"Result":private]=> long(332001950) refcount(1) } } object(Db)#1 (2) refcount(3){ ["_sql":"Db":private]=> string(19) "SELECT bar FROM foo" refcount(1) ["_result":"Db":private]=> object(Result)#2 (1) refcount(1){ ["_result":"Result":private]=> long(508834141) refcount(1) } } object(Db)#1 (2) refcount(3){ ["_sql":"Db":private]=> string(19) "SELECT bar FROM foo" refcount(1) ["_result":"Db":private]=> object(Result)#2 (1) refcount(1){ ["_result":"Result":private]=> long(519443361) refcount(1) } } object(Db)#1 (2) refcount(3){ ["_sql":"Db":private]=> string(19) "SELECT bar FROM foo" refcount(1) ["_result":"Db":private]=> object(Result)#2 (1) refcount(1){ ["_result":"Result":private]=> long(1746125577) refcount(1) } } object(Db)#1 (2) refcount(3){ ["_sql":"Db":private]=> string(19) "SELECT bar FROM foo" refcount(1) ["_result":"Db":private]=> object(Result)#2 (1) refcount(1){ ["_result":"Result":private]=> long(690789379) refcount(1) } } object(Db)#1 (2) refcount(3){ ["_sql":"Db":private]=> string(19) "SELECT bar FROM foo" refcount(1) ["_result":"Db":private]=> object(Result)#2 (1) refcount(1){ ["_result":"Result":private]=> long(462332752) refcount(1) } } object(Db)#1 (2) refcount(3){ ["_sql":"Db":private]=> string(19) "SELECT bar FROM foo" refcount(1) ["_result":"Db":private]=> object(Result)#2 (1) refcount(1){ ["_result":"Result":private]=> long(801369044) refcount(1) } } object(Db)#1 (2) refcount(3){ ["_sql":"Db":private]=> string(19) "SELECT bar FROM foo" refcount(1) ["_result":"Db":private]=> object(Result)#2 (1) refcount(1){ ["_result":"Result":private]=> long(557914563) refcount(1) } } Change in memory usage: 4136