Anonymous Class Factory – The Results are in

A week or so ago, I published an article entitled “In Search of an Anonymous Class Factory” about my efforts at writing a “factory” for PHP7’s new Anonymous Classes (extending a named concrete base class, and assigning Traits to it dynamically); and about how I subsequently discovered the expensive memory demands of my original factory code, and then rewrote it using a different and (hopefully) more memory-efficient approach.

Since then, I’ve run some tests for memory usage and timings to assess just how inefficient my first attempt at the factory code was, and whether the new version of the factory really was better than the original.

Initially, I ran tests for 4 different options; but following a suggestion from axiac on my original post, I added a 5th method, and subsequently chose to add a further option using Reflection:

Option #1

Calling a standard factory that returns an instance of a normal pre-defined concrete class, using both of the Traits that I had defined. This is basically just a baseline to see the differences between using a concrete class and an anonymous class.

public function create() {
    $className = $this->className;
    return new $className(...$this->constructorArgs);
}

Option #2

Calling a standard factory that returns an Anonymous (but directly coded) Class extending a hard-coded base class, and with the two Traits hard-coded to use. Because the anonymous class is hard-coded here, the class definition is only be created once in memory, and every instance will share that definition, and only use individual memory for the instance properties. This is how all the examples in the PHP documentation work, but lacks the flexibility that I was trying to achieve with the factory.

public function create() {
    return new class(...$this->constructorArgs) {
        use SayAB;
        use SayBA;

        protected $a;
        protected $b;

        public function __construct($a, $b = null) {
            $this->a = $a;
            $this->b = $b;
        }

        public function sayWhat() {
            echo $this->a, $this->b, PHP_EOL;
        }
    };
}

Option #3

Calling my original Anonymous Class factory, building the class definition to extend the requested base class, and applying the requested Traits. To keep my tests simple (and comparable with methods 1 and 2), I call this applying both of the defined Traits for every instance created. While it doesn’t demonstrate the flexibility of the factory, I’m not testing that flexibility here: this is to test the performance rather than the functionality.

public function create() {
    $definition = "new class(...\$this->constructorArgs) extends $this->className {" . PHP_EOL;
    foreach($this->traits as $trait) {
        $definition .= "use $trait;" . PHP_EOL;
    }
    $definition .= '}';
    return eval("return $definition;");
}

Option #4

Calling my second version of the Anonymous Class factory, the approach that creates and caches the original instance of every different Anonymous Class, and then returns a clone of that instance. Again, for the purpose of keeping the test simple, I called this applying both Traits for every instance that I created.

private function buildDefinition() {
    $definition = "new class(...\$this->constructorArgs) extends $this->className {" . PHP_EOL;
    foreach($this->traits as $trait) {
        $definition .= "use $trait;" . PHP_EOL;
    }
    $definition .= "public function __reconstruct(...\$args) {" . PHP_EOL;
    $definition .= " parent::__construct(...\$args);" . PHP_EOL;
    $definition .= '}';
    $definition .= '}';
    return eval("return $definition;");
}

public function create() {
    $hash = md5($this->className . implode(',', $this->traits));
    if (!isset(self::$instances[$hash])) {
        self::$instances[$hash] = $this->buildDefinition();
    }
    $instance = clone self::$instances[$hash];
    $instance->__reconstruct(...$this->constructorArgs);
    return $instance;
}

Option #5

Following a suggestion by axiac on my original posting, suggesting the use of new with the template instance, allowing me to eliminate the __reconstruct() method, I subsequently added a further method:

public function create() {
    $hash = md5($this->className . implode(',', $this->traits));
    if (!isset(self::$instances[$hash])) {
        self::$instances[$hash] = $this->buildDefinition();
    }
    return new self::$instances[$hash](...$this->constructorArgs);
}

Option #6

After implementing axiac's suggestion, I also decided to try using Reflection to instantiate from the template class:

public function create() {
    $hash = md5($this->className . implode(',', $this->traits));
    if (!isset(self::$instances[$hash])) {
        self::$instances[$hash] = $this->buildDefinition();
    }
    return (new ReflectionClass(self::$instances[$hash]))
        ->newInstanceArgs($this->constructorArgs);
}

 


 

The code to actually run the tests, calling the factory, was identical for them all; the difference was the different versions of the factory for each option shown above.

$instanceCount = 100000;

$callStartTime = microtime(true);

$baseMemory = memory_get_usage();
$anonymous = [];
for ($i = 0; $i < $instanceCount; ++$i) {
    $anonymous[$i] = (new AnonymousClassFactory('baseClass', ...$args))
        ->withTraits('SayAB', 'SayBA')
        ->create();
}
$fullMemory = memory_get_usage();

$callEndTime = microtime(true);
$callTime = $callEndTime - $callStartTime;

echo 'Call time was ', sprintf('%.4f',$callTime), " seconds", PHP_EOL;
echo "Memory usage for $instanceCount anonymous class instances is ",
     number_format($fullMemory - $baseMemory), PHP_EOL;

I ran the tests shown above to generate 100,000 instances of my class, repeating the exercise 100 times, and averaging the results after stripping out the top and bottom 5% to give myself a trimmed mean of the timings.

Execution Speed (s) Memory Usage (MB)
Method #1 0.0961 19.20
Method #2 0.0924 19.20
Method #3 0.8444 110.20
Method #4 0.1738 16.15
Method #5 0.1562 19.20
Method #6 0.1885 19.20

 

And displaying these results graphically:

ExecutionSpeed2

Chart showing Execution Speed of each method

 

MemoryUsage2

Chart showing Memory Usage of each method

 

There were a few surprises here, not least just how inefficient my original factory had been, both in terms of memory usage and execution speed. I’d realised that it was a memory hog, which was why I’d revisited the project in the first place; but hadn’t known just how bad it was. And until I ran these tests and had something to compare with, I hadn’t realised just how slow the execution speed was either: is eval() really that expensive to call? Although I suspect a lot of that performance overhead was the memory allocation, which is always expensive.

I was slightly surprised to see that creating Anonymous Classes was (marginally) faster than instantiating concrete classes, not by a significant amount, but consistently; I reran the tests several times over, just to double check, and every time method #2 was fractionally faster than method #1. Perhaps somebody from the PHP Internals team would be able to explain why that should be so.

What really surprised me was to see that my updated Anonymous Class factory was using less memory for its 100,000 instances than either instantiating 100,000 concrete class or the same number using the more typical Anonymous Class creation of method #2. If anything, I would have expected it to be fractionally higher because it’s maintaining that array of cached master instances, so over 3MB lower (over those 100,000 instances) was quite a surprise.


 

So clearly there is something interesting happening “under the hood” with Anonymous Classes: marginally faster performance of the standard Anonymous Class compared with the concrete class; and significantly lower memory usage by my new version of the factory compared with both “standard” Anonymous Classes and with concrete classes. Certainly I shall be investigating further to see if I can discover just why I get these results.

Additional:

I’ve given (or am giving) presentations on Anonymous Classes at the following user groups and conferences

  • The PHP Surrey monthly meetup in Guildford on 6th July 2016

 

Advertisements
This entry was posted in PHP and tagged , , , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s