Tuesday, 8 September 2015

Addendum: Letter from the Future

Fig 1. Some PHP7 logos
A couple of weeks ago, I wrote about my efforts to port pthreads to PHP7, to make it worthy of the shiny new platform.

Work continued on pthreads, and today I'm going to take the opportunity to update my previous blog post with some correction and extension.

With PHP7 in RC2, I'm sure everyone is a bit tired of blog posts about how quick PHP7 is and why it is so quick, so we're not going into that today. We will be looking at the performance of pthreads in particular.

Corrections

 

Last time I wrote about v3, it was still under development, and while the pthreads test suite passed, complex code of the kind you cannot include in an extension test suite didn't work as expected. I knew about it at the time but we were talking about untagged, unreleased code.

Note that we are still talking about untagged, unreleased code. However, this is only because the route to release is not very clear for PHP7 extensions yet, with no pear/pecl support.

Serialization Hack 

 

I previously wrote that you would be able to serialize Threaded objects like any other in PHP. I have to retract that statement; I overlooked the fact that a serialization hack is required to make Threaded objects behave as expected when they themselves are members of objects which are not Threaded (and so are serialized).

So, the hack got put back in, a bit tidier this time, and is used much less frequently than before, because no need for serialization of Threaded objects when they are members of other Threaded objects.

Note that, Threaded objects are still not serialized, only the serialize/unserialize handlers manipulated to pass around the address of an object, rather than a serial representation of the objects value.

Threaded Iteration

 

This has been improved yet again, we don't waste the memory to copy the set of keys as I previously said we would. Now we just iterate over the store copying keys and values as they are required for iteration, safely, obviously.

This couldn't be more efficient, nothing else to say about that.

Performance

 

It's going to be obvious that a Threaded object has quite some overhead associated with it, in order to provide implicit safety, store in a thread safe manner, and synchronize at the programmers will. What might not be obvious is just how many hoops pthreads has to jump through so that a programmer can manipulate one object in two contexts.

You cannot actually have an object in two contexts in a shared nothing environment, so that's the first thing to observe; There are actually O(n) objects where n is the number of contexts with a reference to the object.

But it gets worse, consider the following code:

<?php
class Test extends Thread {

 public function __construct(Threaded $threaded) {
  $this->member = $threaded;
 }

 public function run() {
  while (1) {
   $this->member->doStuff();
  }
 }
}
?>

Whenever $this->member is accessed in the thread, if you don't know that another context didn't change the reference, you have to undertake an enormous amount of work to provide the new context a reference to $this->member, at worst a new object is constructed every time.

In later versions of pthreads v2 there was a mechanism to reduce the number of constructions but it wasn't wholly effective, it leaked memory, with no chance to plug the leak. So this mechanism was removed from pthreads v3.

This means that not only are there O(n) objects where n is the number of contexts, but they must be constructed O(n) times where n is the number of accesses to the object in another context.

This is obviously extremely slow, and quite scary ...

For normal objects, objects that are not Threaded, we have little choice but to put up with the performance overhead of that. It should normally be the case that objects being used by Threads are Threaded, so this isn't much of a concern.

For Threaded objects though, this is just unacceptable.

Immutability to the Rescue


In pthreads v3, setting a member of a Threaded object (A) to another Threaded object (B) makes the reference that A holds to B immutable.

This means that during execution, once a member is set it cannot be unset or reset by any context, this means that the complexity for access is no longer O(n) but O(1).

Before you go batshit crazy about forcing immutability on the programmer, take a look at some numbers, and read what I have to say about it ...

Consider the following (quite normal, not super CPU intensive) code:

<?php
class My extends Threaded {
 public function method($i) {
  return 1 * $i;
 }
}

class Test extends Thread {
 public function __construct(Threaded $threaded) {
  $this->threaded = $threaded;
 }

 public function run() {
  while (@$i++<1000000) {
   $this->threaded[] = 
    $this->threaded->method($i);
  }
 }
}

$my = new My();
$test = new Test($my);
$test->start();
$test->join();
var_dump(count($my));
?>

Pretty standard stuff, and doesn't look that expensive, but because of the complexity of access in PHP5.6 and pthreads v2, measuring the user instructions and time taken to execute yields the following result:

  Performance counter stats for 'php time.php':

    14,547,244,407      instructions:u          

       2.133205136 seconds time elapsed

Note this is a late version of pthreads v2 with the aforementioned poor mechanism to reduce access complexity.

Same method, same machine, same extensions, PHP7 and pthreads v3 yields:

  Performance counter stats for 'php time.php':

     2,525,791,532      instructions:u          

       0.501464325 seconds time elapsed

Now that is more like it !!

A wise person once said "Never trust a benchmark you didn't fake yourself".

What we are looking at here is not really the time elapsed, for that will vary so much depending on so many things. We are interested in the number of user instructions executed because regardless of how long it takes, this gives you a good idea of how complex some user code is for the CPU.

Not only does immutability reduce the complexity of access considerably, it also allows us to avoid synchronization after the first access to $this->member in the new context. We can do this safely because we already have a reference to the object, constructed (synchronized) on the first access and stored locally to the thread and object, and we know that the reference is immutable.

Immutability breaks backward compatibility, but as mentioned, I don't care about that if I'm breaking something stupid or bad.

It pains me to say it, the amount of effort I put into making pthreads work is completely unreasonable, but it was both bad and stupid.

It may not seem like it initially, but immutability will become your best friend, very quickly.

 Volatility

 

This might be a new word to some PHP programmers, or one of those words that you heard but don't really know what it means, so here is the dictionary definition:

Fig 2. The definition of volatile, for reference.
An object that might change while being manipulated in many contexts is the epitome of volatility.

pthreads v3 introduces volatile objects to replace objects whose Threaded members are currently mutated by many contexts.

Volatile extends Threaded, so a Volatile object set as a member of a Threaded object still creates an immutable reference to the Volatile object, which is desirable because of access complexity, but the Volatile object itself is ... volatile.

Volatile objects are slow, with high access complexity, and should only be used when there is no other option.

The cost of all this ...

 

Nothing is without cost, it's arguable that multi-threaded programming in PHP just got harder to understand, but it is without question that it got so much faster that the set of power PHP programmers that use pthreads are not going to care.

No comments:

Post a Comment