Jan 26, 2007

You're being lied to

If you're among the crowd who have migrated an OOP based application from PHP4 to PHP5, then I'm sure you've heard the expression "Objects are copied by reference by default in PHP5". Whoever told you that, was lying.

Now, to be fair, it's an innocent lie, since objects do behave in a reference-like manner, but references are NOT what they are. Let's start with a simple illustration proving that they aren't references:

<?php
$a = new stdClass;
$b = $a;
$a->foo = 'bar';
var_dump($b);
/* Notice at this point, that $a and $b are,
* indeed sharing the same object instance.
* This is their reference-like behavior at work.
*/

$a = 'baz';
var_dump($b);

/* Notice now, that $b is still that original object.
* Had it been an actual reference with $a,
* it would have changed to a simple string as well.
*/
?>

What's going on here? Well, the answer is easiest to explain by explaining what the underlying structure of objects are. In PHP5, a variable containing an object identifies the instance by storing a simple numeric value. When an action is going to be performed on an object, that numeric value is used with a lookup table to retreive the actual instance. In PHP4, by contrast, a variable containing an array identifies that object by carrying around the actual properties table itself. What this means in practice is that when you assign (not by reference) a PHP5 object to a new variable, that integer handle is copied into the new variable, but it still points at the same instance, because it's still the same number. Assigning a PHP4 object however, means copying all the properties, effectively generating a new instance, since changes to one will not effect the other.

To put this another way, PHP4 objects are basically Arrays with functions associated with them, PHP5 objects are basicly Resources (a la MySQL result handles, or file pointers) again with functions loosely associated to them. Consider the following code in PHP4 (or any version):

<?php
$fp = fopen('foo.txt', 'r');
$otherVar = $fp;
fwrite($fp, "One\n");
fwrite($otherVar, "Two\n");
fclose($fp);

/* This fails, because the file is closed */
fwrite($otherVar, "Three\n");
?>

You'd fully expect data to be written to the same file, as though you'd used $fp everywhere, rather than interchanging the variables right? Well, PHP5 objects are the same. The instance itself isn't duplicated when you assign to a new variable, just the unique identifier.


I'm lying to you also

"Copying" a variable doesn't exactly mean copying. Take the following code block:

<?php
$a = 'foo';
$b = $a;
$a = 'bar';
?>

Now, you know PHP well enough to know that by the end of this code block, the value of $b will still be 'foo'. What you may not know, is that the original copy of 'foo' that was in $a, was never actually duplicated.

To understand what PHP is doing, you need to understand the internal structure of the variable and how it relates to userspace visible variable names ('a' and 'b' in this case). First off, the actual contents of a variable (known as a zval) consists of four parts: type (e.g. NULL, Boolean, Integer, Float, String, Array, Resource, Object), a specific value (e.g. 123, 3.1415926535, etc...), is_ref - a flag indicating if the value is a reference or not, and refcount which tells how many times this value is being shared.

What you think of as a variable (e.g. $x) is actually just a label, that label ('x' in this case) is used as a lookup to find the zval which conatins the actual value. These are just like keys in an associative array, in fact, the mechanisms are identical.

With me so far? Good. Now, when you first create a variable (e.g. $x = 123;, PHP allocates a new zval for it, stores the specific value, and associates the label with the value:

  'x' => zval ( type       => IS_LONG,
value.lval => 123,
is_ref => 0,
refcount => 1 )

So far, refcount is 1 since the zval value is only being referenced by one label. If we now put this value into a full-reference set using $y =& $x;, the same zval is reused. It's simply associated with a new label and it's reference counters are adjusted properly.

  'x' => zval ( type       => IS_LONG,
| value.lval => 123,
| is_ref => 1,
| refcount => 2 )
'y' /

This way, when you later change the value of $x, $y appears to change as well because it's looking at the same internal value. But what if we hadn't done a reference assignment, what if we'd done a normal assignment: $y = $x;, surprisingly, the result would be almost the same.

  'x' => zval ( type       => IS_LONG,
| value.lval => 123,
| is_ref => 0,
| refcount => 2 )
'y' /

Again, the original zval associated with $x is reused, the only difference this time is that is_ref is not set to 1. This is known as a copy-on-write reference set (as opposed to the full-reference set described above). This 0 flag tells the engine that if anyone tries to change this value (regardless of which label they use to reach it), any other references to it should be left alone. Here's what happens if we take that current state and do $x = 456;

  'y' => zval ( type       => IS_LONG,
value.lval => 123,
is_ref => 0,
refcount => 1 )
'x' => zval ( type => IS_LONG,
value.lval => 456,
is_ref => 0,
refcount => 1 )

$x has been disassociated from the original zval (thus dropping its refcount back to 1), and new zval has been created for it.


Why referencing when you don't have to is a bad idea.

Let's consider one more situation, take a look at this code block:

<?php
$a = 'foo';
$b = $a;
$c = &$a;
?>

At the first instruction, a single zval is created, associated to a single label:

  'a' => zval ( type => IS_STRING, value.str.val = 'foo', is_ref = 0, refcount = 1 )

At the second intstruction, that zval is associated to a second label, so far so good:

  'a' => zval ( type          => IS_STRING,
| value.str.val => 'foo',
| is_ref => 0,
| refcount => 2 )
'b' /

At the third intstruction, however, we run into problems. Since this zval is already tied up in a copy-on-write reference set which include $b, that zval can't be simply promoted to is_ref==1. Doing so would drag $b into $a and $c's full-reference set, and that would be wrong. In order to resolve this, the engine is forced to duplicate that zval into two identical copies, from which it can begin to shuffle around reference flags and counts:

  'b' => zval ( type          => IS_STRING,
value.str.val =>'foo',
is_ref => 0,
refcount => 1 )
'a' => zval ( type => IS_STRING,
| value.str.val => 'foo',
| is_ref => 1,
| refcount => 2 )
'c' /

Now you've got two copies of the same literal value, so you're wasting memory for the storage, and processing time required to actually make the duplication. Since a LOT of events lead to copy-on-write uses (including simply passing an argument to a function), this sort of forced duplication actually happens very commonly when you start involving actual references.


The moral of the story

Assigning values by references when you don't need to (in order to later modify the original value through a different label) is NOT a case of you outsmarting the silly engine and gaining speed and performance. It's the opposite, it's you TRYING to outsmart the engine and failing, because the engine is already doing a better job than you think.

How does this reflect on objects? They're not special. They're not different from other variables. They are not pretty snowflakes. In this code block:

<?php
$a = new stdClass;
$b = $a;
?>

The labels are still placed into copy-on-write reference sets. What's important, is that even when a duplication does occur, (A) only that unique integer is copied (which is cheap), and (B) the duplicated integer still points to the same place. Hence you get reference-like behavior, but not an actual reference by default.

Hungry for more? Check out my coverage of the zval.

Dec 28, 2006

PHP-2006: A look back

Well, Davey Shafik started us off with his year-end wrapup so I'll follow suit with mine. The thoughts below are mine and mildly influenced by alcohol. They represent a foggy review of how I experienced the year through the imperfect recollection of mailing list archives.

January began with releases of 4.4.2 and 5.1.2. Version 5.1.2 was especially close to my heart since it was the first version to ship an extension of mine not only bundled, but enabled by default. I've had my hands in most of the PHP runtime, but this was the first time I could point at a standard extension and say that it was basicly my work (Note: Mike Wallner did a fair bit of work adding to the number of hash algorithms supported, don't let me discount his efforts). Tim Starling wrote to ask why PHP4 refcounts are 16bit, and what he could do about getting that counter increased in future versions of PHP4. Since such a change would break binary compatability and since the PHP4 branch is already quite dead, the request was ultimately left alone with an admonishment to "not do that".

A few other requests were broached or continued, such as support for Friend Classes, Named Arguments, and Naming Arguments (The last of which would eventually be implemented). It was also this month in which Rasmus suggested adding JSON to the standard distribution, this quietly morphed into votes for including filter; Both were eventually linked in. James Crane had the idea that Array Literals in PHP could use sprucing up, meanwhile Sean Coates planted the seed for what would become the PHP6 Unicode Progress tracker.

February came in fairly quiet, mostly wrapping up the topics from January. Appearantly Steph didn't like the quiet and decided to stir up the hornet's nest with some four letter words cleverly disguised under the heading of True Labeled Breaks. For those who have blocked out 2005, this was easily one of the longest threads of that year and this resumption promised to be just as bad. By March, this thread would finnally be brought to a halt as the functionality was slipped quietly into the engine. I did not see that coming...

Amidst this unexpected addition, others were busily pulling things out, with Andi slashing away at safe_mode in HEAD, and Marcus integrating support for function deprecation into the 5.2 branch.

March came in like a lion with Marcus poking the list about Late Static Binding with less argument about Why, and a healthy focus on How. Johannes Schlueter made a very pragmatic and uncontroversial suggestion to change the internal symbol prefix applied to methods in order to distinguish them from functions. Pierre ran with some PDM recommendations shouting Adieu register_globals and Adieu a la magie. Sebastian Bergmann was frustrated with trying to apply streams filters to include and require, until I pointed him at the php://filter wrapper. Though this met his need, he made a strong case for being able to set an automatic filter to be applied to all streams. I promised to do this after streams in HEAD had been cleaned up, and while I've gotten streams where I want them, I still havn't fullfilled his feature request... Shame on me.

All this time, the GOTO debate was still raging and similar types of language altering features were being put on the table. Finally, Zeev decided he'd had enough calling for people to "Give the language a rest". This managed to have a decent effect which Rasmus soon channeled into his call for performance geeks to narrow the performance gap between 4.4 and 5.1. Towards the end of the month, I suggested adding an open_basedir_for_include directive. While there was some interrest for this, Ilia quickly convinced me that my approach to the problem was flawed and wouldn't give any real benefit.

April showered the internals list with Round 2 of the Late Static Binding discussion, and Thomas Boutell's anouncement that he'd be turning over primary development of the GD library to the PHP project, spearheaded by Pierre. I made some more noise complaining of RETURN_RT_STRING() (and family)'s leakage of memory under certain not-uncommon conditions, while Nuno brought the infamous Coverity Report to attention.

Richard Lynch shared his WTF concerning the oft misunderstood tsrm_ls parameter in PHP's sources. Sadly, I hadn't written my summary of the topic yet, but although noone gave him a detailed description (It's a bit long to go into in an email), several helpful links were supplied. Round about the middle of the month, Rasmus announced PHP's eminent participation in the Google Summer of Code project (wait, don't you work for Yahoo!?). After signing up as a mentor (though I never actually mented -- is that a word?), I gathered up my materials and flew off to php|tek 2006 in Orlando.

May opened to the initial planning rounds for PHP 5.2.0, and the resumption of the ifsetor() request, now dressed up as coalesce(). By the way, why havn't we embraced this feature request yet? About a week into the month, Ilia branched the PHP_5 tree leaving room for the earnest development of 5.2.

William Candillon wrote in to request a PHP version of C's #line macro, but was drowned out by the roar of Derick's plea to "Stop breaking our apps for the sake of OO". Once the din of that had quieted down a little, Jason Garber decided to ask about making it possible to mark properties as read only. In case anyone was curious, none of these proposals gained footing.

June now, and my book is finally released bringing with it an invitation to appear on php|architect's Pro::PHP podcast. Marcus Boerger suggested that array indices could benefit from implicit __toString() calls, but he was eventually shot down. Dimitry's suggestion however, which provided for automatic module global registration via the module entry, did receive a warm welcome and you'll see it if you look inside PHP 5.2.0.

Clearly this was the month of bright ideas, because Nuno Lopes tossed in gcc branch prediction. It got a warm initial reception from the engine folk including Zeev stating "I actually like how it makes the code more readable hinting which branches are rare.". Andi agreed that it was a nice idea, but brought in the sobering reality that it didn't really do much for performance and could potentially bring unexpected results if applied heavily.

July was a busy month for me as I changed jobs for the first time in over six years. I'm loving the new job by the way. Fantastic coworkers and....interresting challenges.... Laupretre François thought it'd be a good idea to extend include_path to support stream wrappers. Those of us who know what trouble include_path already causes for performance and security were quick to nix that particular idea; Short version: No.

Marcus popped in mid-month with an implementation of the #line directive suggested back in May. Still no love from the internals community at large though; Short version: no #line for you. Dmitry committed a large patch to the Zend Engine to change how non-persistent memory is allocated and freed within a request. The good news is that emallocs are now faster, the bad news is that a hack I'd made in PHP5.1 for manipulating the non-persistent memory pool no longer worked...Grrrr.....

On July 27th, Jani said Good-Bye.

August picked up Mike Wallner's July post decrying the state of OO strictness building up in PHP. This maelstrom took up most of the first week with no real conclusion (at least, not a satisfactory one). A different Mike made some more noise later in the month asking why accessing non-existant functions/methods has to be so darned fatal. The good news is that they aren't so much anymore. Yay for E_RECOVERABLE_ERROR.

September brought a landslide of movement in the PHP6 function migration/review process. There was some degree of question as to whether unicode.semantics should be SYSTEM, PERDIR, or USER. Noone really considered the latter as a possibility as it wrecks way too many assumptions, but the SYSTEM/PERDIR debate raged for awhile with the idealists wanting PERDIR support to aid migration, and the realists clinging to the maintainable simplicity of SYSTEM. In the end, SYSTEM won.

October was a relatively slow month, leading up to conference season such as it was. Midmonth sometime I tossed out the idea of allowing open_basedir to be tightened (but not loosened) during runtime and it was green-lighted surprisingly quietly. Ilia got a little frustrated with the mounting delays holding back the release of PHP 5.2.0 which was already way behind schedule. Tragicly, this plea made the delay last even longer. Finally at the end of the month, he rolled final.

November was a big conference month for me. First I crashed the nearby ZendCon, then turned right around and flew off to Germany for the International PHP Conference. Sean poked the namespaces topic with a sharp stick and managed to generate a week's worth of noise resulting in no actual commitments by any capable/interrested parties. No sooner had that comotion died down than did Antony Dovgal bemoan a regression in fgets()'s behavior which I introduced in HEAD. I still think it's a silly behavior for a userspace function, but BCs are BCs, so the regression's been reverted.

December's most exciting event was a debaucle over the backward/forward compatability of serialize given the additional escaping concerns that processing unicode strings imposes. Ilia brought forward a proposal to finally remove the unnecessary COM/Sockets/MHash extensions from the distribution bundle. After some light debate it was decided to keep COM around and only nix Sockets and MHash as of PHP6 (possibly 5.3 if such a version comes about). Wietse Venema brought back the concept of introducing a taint mode for PHP. The topic is still being discussed, but things don't look good for Wietse...

And now here we are, at year's end. PHP6's unicode function migration process has passed the 50% mark and a preview release is sure to come once we handle a couple more extensions. PHP 5.2 is grabbing hold amongst the serious PHP shops, and even web hosters are trickling away from PHP4. There's still a huge, bright future ahead for this language, together we can make it happen.

¡Feliz Año Nuevo!

Nov 29, 2006

When good encodings go bad

In the past year, I've been doing some work with Unicode as part of the PHP6 upgrade. I've learned more than I wanted to know about all sorts of encodings from UTF-7 to koi8-r to good old iso-8859-1. I've picked apart the picayune differences between UCS-2 and UTF-16, and played the game of surrogate pairing and orphaning. Despite all that exposure however, I wasn't prepared when a question crossed my inbox about a lesser known encoding called AL32UTF8.

I'd never heard of this one before, so I went to my favorite search engine for some answers. Turns out it's something Oracle came up with and later got adopted as a proper standard with the name CESU-8. At first glance, CESU-8 looks identical to UTF-8 in the same way that UCS-2 looks a lot like UTF-16. In fact every codepoint from U+0000 to U+FFFF is encoded identically under both sets of rules: 16 bits, split up over one, two, or three bytes, with leftover bits framing the encoding protocol.

When you jump up above U+FFFF however, into the realm of CJK codepoints and the like (such as my personal nom du pointe: 𣚺) something funny starts to happen. In the UTF8 world, these codepoints are accomodated by adding one extra byte to the mix which allows for up to 22bits of data (All of unicode only requires 21). In the CESU-8 world however, the code point is split according to UTF-16 surrogacy rules making two separate unicode points (each in the range U+D800 - U+DFFF). These two unicode points are then encoded individually into UTF-8 sequences. This means that we've now promoted our variable length (4 max) multibyte encoding to a variable length (6 max) multibyte-multibyte encoding. Thank you Oracle. Thank you for adding complexity to encoding rules while increasing data storage requirements. What would the world do without you?

P.S. - Java is at fault too... its 'Modified UTF-8' uses nearly identical rules.

Nov 5, 2006

Don't worry, I slept last Thursday

On this, my first foray to Europe, indeed my first real trip outside the US (Those couple hours in Tijuana don't count), I'm faced with one undeniable, inexcapable fact. Jet lag sucks.

It doesn't help that all last week I was staying up past my normal bedtime partying with the attendees of ZendCon06, but I think my real mistake was trying to outsmart my own circadian rhythm. See, I figured "I've got this long flight across the atlantic, it'll go faster if I can fall asleep at some point." Seems reasonable so far. How to ensure sleep? Why, stay up all night before the flight. Brilliant! But wait, what if I can't fall asleep during the flight?

Sometime sunday morning, my plane lands in Frankfurt and I wander, zombie-like, through passport control, baggage claim, and customs somehow managing to board the right shuttle to reach the conference hotel. Based on advices from battle-hardened globetrotters, I was planning to put in a one hour power nap, then go for a walk to "reset" my internal clock. Unfortunately the sixty-plus hour run of consciousness had other plans and by the time I awoke, the sun had set.

Finally this morning (Monday), I managed to put in that walk, touring a nearby suburb which reminded me somewhat spookily of the setting from "Shaun of the Dead" (more zombie tie-ins). Five euros and three liter bottles of diet coke later and I'm almost coherent enough to....what the hell was I gonna say? Sorry, my brain has been cutting out a lot the past few days...

Guten Morgen!

Jul 5, 2006

Moving right along

After well over half a decade at the University of California at Berkeley, I'm moving on to greener pastures. Well, maybe not greener (Berkeley is full of evergreens and...other verdant plant substances), but pastures at the very least. Next monday, my life's journy will bring me to that farmland come technopolis known the world over as Silicon Valley. I'll be cubefarming private sector style for one of the few dotcom survivors, a little mom & pop outfit called Yahoo!. I've worked in education and the public sector since 1997 and while I'm optimistic everything will go smoothly, well....I think Dan Aykroyd put it best as Dr. Raymond Stantz in Ghostbusters: "You don't know what it's like out there, you've never worked in the private sector, They expect results! (shudder)". If nothing else it'll be nice to get my commute back down to the 20minute zone. At any rate, I've got 2 days and as many going-away parties to get through, then it's on to the enemy camp (Yahoo's birth {not as a company, but as the index site it started out as} was at UCBerkeley's long-time rival, Stanford University).

Jun 18, 2006

How long is a piece of string

Sunday morning I was asked by an IRC regular: "Where does the engine parse quoted strings?". Being a sunday morning, I began to launch into a sermon on the distinction between CONSTANT_ENCAPSED_STRING and the problems which befall a single-pass compiler when you start to introduce interpolation. Not what he asked precisely, but an important component in answering his question. Unfortunately, at the time I was busy watching the Brasil-Australia game so I didn't go into the kind of detail I would have. Now, some 12 hours later, since Angela is off buying toe-socks in Santa Cruz, I'll bore anyone with little enough life to read my blog by explaining the pitfalls of using PHP's string interpolation without using an optimizer.

To start things off, let's take a page from my earlier discourse on Compiled Variables and look at the opcodes generated by a few simple PHP scripts:

<?php
echo "This is a constant string";
?>

Yields the nice, simple opcode:

ECHO            'This is a constant string'

No problem... Exactly what you'd expect... Now let's complicate the expressions a little:

<?php
echo "This is an interpolated $string";
?>

Yields the surprisingly messy instruction set:

INIT STRING  ~0
ADD_STRING ~0 ~0 'This'
ADD_STRING ~0 ~0 ' '
ADD_STRING ~0 ~0 'is'
ADD_STRING ~0 ~0 ' '
ADD_STRING ~0 ~0 'an'
ADD_STRING ~0 ~0 ' '
ADD_STRING ~0 ~0 'interpolated'
ADD_STRING ~0 ~0 ' '
ADD_VAR ~0 ~0 !0
ECHO ~0

Where !0 represents the compiled variable named $string. Looking at these opcodes: INIT_STRING allocates an IS_STRING variable of one byte (to hold the terminating NULL). Then it's realloc'd to five bytes by the first ADD_STRING ('This' plus the terminating NULL). Next it's realloc'd to six bytes in order to add a space, then again to eight bytes for 'is', then nine to add a space, and so on until the temporary string has the contents of the interpolated variable copied into its contents before being used by the echo statement and finally discarded. Now let's rewrite that line to avoid interpolation and use concatenation instead:

<?php
echo "This is a concatenated " . $string;
?>

Which yields the significantly shorter and simpler set of ops:

CONCAT       ~0 'This is a concatenated ' !0
ECHO ~0

A vast improvement already, but this version still creates a temporary IS_STRING variable to hold the combined string contents meaning that data is duplicated when it's being used in a const context anyway. Now let's try out this oft-overlooked use of the echo statement:

<?php
echo "This is a stacked echo " , $string;
?>

Look close, there is a meaningful difference from the last one. This time we're using a comma rather than a dot between the operands. If you don't know what the comma is doing there, ask the manual then check back here. Here's the resulting opcodes:

ECHO            'This is a stacked echo '
ECHO !0

Same number of opcodes, but this time no temporary variables are being created so there's no duplication and no pointless copying (unless of course $string wasn't of type IS_STRING, in which case it does have to be converted for output, but don't get picky now). Think this is bad? Consider the average heredoc string which spans several lines of prepared output embedding perhaps a handful of variables along the way. Here's one of several such blocks found in run-tests.php within the PHP distribution source tree:


<?php
echo <<NO_PCRE_ERROR

+-----------------------------------------------------------+
| ! ERROR ! |
| The test-suite requires that you have pcre extension |
| enabled. To enable this extension either compile your PHP |
| with --with-pcre-regex or if you've compiled pcre as a |
| shared module load it via php.ini. |
+-----------------------------------------------------------+

NO_PCRE_ERROR;
?>

Notice that we're not even embedding variables to be interpolated here, yet does this come out to a simple, single opcode? Nope, because the rules necessary to catch a heredoc's end token demand the same careful examination as double-quoted variable substitution and you wind up (in this case) with SEVENTY-EIGHT opcodes! One INIT_STRING, 76 ADD_STRINGs. and a final ECHO. That means a malloc, 76 reallocs, and a free which will be executed every time that code snippet comes along. Even the original contents take up more memory because they're stored in 76 distinct zval/IS_STRING structures.

Why does this happen? Because there are about a dozen ways that a variable can be hidden inside an interpolated string. Similarly, when looking for a heredoc end-token, the token can be an arbitrary length, containing any of the label characters, and may or may not sit on a line by itself. Put simply, it's too difficult to encompass in one regular expression.

The engine could perform a second-pass during compilation, however the time saved reassembling these strings will typically be about the same amount of time spent actually processing them during runtime (if one assumes that each instance will execute exactly once). Rather than complicate the build process (potentially slowing down overall run-times in the process), the compiler leaves this optimization step to opcode caches which can achieve exponentially greater advantage cleaning up this mess then caching the results and reusing the faster, leaner versions on all subsequent runs.

If you're using APC, you'll find just such an optimizer built in, but not enabled by default. To turn it on, you'll need to set apc.optimization=on in your php.ini. In addition to stitching these run-on opcodes back together, it'll also add run-time speed-ups like pre-resolving persistent constants to their actual values, folding static scalar expressions (like 1 + 1) to their fixed results (e.g. 2), and simpler stuff like avoiding the use of JMP when the target is the next opcode, or boolean casts when the original expression is known to be a boolean value. (It should be noted that these speed-ups also break some of the runtime-manipulation features of runkit, but that was stuff you....probably should have been doing anyway)

Can't use an optimizer because your webhost doesn't know how to set php.ini options? You can still avoid 90% of the INIT_STRING/ADD_STRING dilema by simply using single quotes and concatenation (or commas when dealing with echo statements). It's a simple trick and one which shouldn't harm maintainability too much, but on a large, complicated script, you just might see an extra request or two per second.


Jun 7, 2006

Extending and Embedding PHP


It's official!!!! After a year in development Extending and Embedding PHP is now shipping from fine book stores everywhere.


I've gotten good reviews from the half dozen people I know who've gotten their hands on it, and I am really satisfied with most of it. Do I think it could be better? Of course I do, but I don't think I was ever going to be completely satisfied.


I've learned a lot through this process and while I don't see any more titles in my immediate future (there are things coming down the pipe which are likely to change my availability), I do expect that my next book, should it materialize, will be even better.


If you pre-ordered it, you should see it soon, if you've already got your copy, let me know what you think! Did I skim over some topic too quickly? Did I belabour something else? If this book eventually finds it's way into a 2nd edition (no promises mind you), are there topics you want to see added? Tossed out? Expanded/Compressed?