Press "Enter" to skip to content

Yaf 3.2 released

After more than two weeks of refactoring, I finally bit the bullet and released Yaf 3.2.0 beta today — otherwise I'd keep thinking of various possible optimization points and writing nonstop, never finishing. 🙂

The starting point of this refactoring was changing the original Yaf objects from native PHP objects to custom objects:

Structural refactor

In previous Yaf versions — take Yaf_Request as an example — it used to be a PHP object, with properties, and used much like an object a user would define in PHP. For instance, in earlier versions of Yaf, if you wanted to read the uri property of a Request object from the extension, you'd do:

zend_read_property(yaf_request_ce, obj, “uri”, sizeof(“uri”)-1, 1, NULL);

to "read" the uri property of this request object. To a degree this has historical reasons — after all, Yaf was written ten years ago. 🙂

From 3.2.0 onward, the Yaf_Request object became a pure C struct:

typedef struct {
    zend_uchar  flags;
    zend_string *method;
    zend_string *module;
    zend_string *controller;
    zend_string *action;
    zend_string *base_uri;
    zend_string *uri;
    zend_string *language;
    zend_array  *params;
    zend_array  *properties;
    zend_object std;
} yaf_request_object;

If you've read my earlier article Deep dive into the PHP7 kernel: Object, this definition shouldn't be unfamiliar. When a user obtains a Yaf_Request object on the PHP script side, it corresponds to the yaf_request_object.std member.

This way, to get uri, you just need to compute the offset, get the address of the yaf_request_object from std, and then (yaf_request_object)->uri — done. Performance improves noticeably.

All the classes Yaf provides have been rewritten into this form. But this means we need to emulate PHP object behavior, so that when a user on the PHP script side also wants to get the uri property of a Yaf_Request object, they can access it normally.

In addition, there's an improvement in cache-friendliness. Besides minimizing memory usage, here's an example: previously the use of global variables was heavier — mainly referring to YAF_G(). For some very hot functions, using too many global variables is not cache-friendly. So we converged many flag-type fields onto the object itself. For example, Yaf_Loader needs to track several flags such as whether use_spl_autoload is enabled, name_suffix, name_separtor, and lowercase_path — previously all of these required accessing Yaf_G(). Now they're all converged into yaf_loader_object->flags. (In practice, it reuses the std.properties_table[0].u2.var_flags field as flags, with no new memory added.)

#define YAF_LOADER_FLAGS(loader)  YAF_VAR_FLAGS(loader->std.properties_table[0])
typedef struct {
    zend_object std;
    zend_string *library;
    zend_string *glibrary;
    zend_array  *properties;
} yaf_loader_object

PSR-4 Autoloading

This time we also added partial PSR-4 support to the autoloader. Users can now declare a namespace's loading path:

Yaf_Loader::getInstance()->registerNamespace(“FooBar”, “/var/lib/foo”);

Then all classes in the FooBar namespace will be looked up under the /var/lib/foo directory:

FooBarDummy  -> /var/lib/foo/Dummy.php

Of course, you can also register a namespace path in the config file:

application.library.namespace./Foo/Bar=“/var/lib/foo”
However, for compatibility with the old loading protocol, the underscore is still treated as a directory separator.

FooBarVer_Don -> /var/lib/foo/Ver/Don.php

Performance improvement

This refactoring is essentially still about performance. So what are the actual results?

To reflect the test as realistically as possible, I used the demo generator tool/cg/yaf_cg under the Yaf codebase to generate a complete Yaf application framework, then compared in detail with cachegrind.

tools/cg/yaf_cg -d yaf

First, php-7.4, yaf-3.1.4, run 1000 times:

 valgrind --tool=cachegrind /path-to-php74/bin/php-cgi -T 1000 index.php
Elapsed time: 6.020169 sec
==11203==
==11203== I   refs:      232,666,798
==11203== I1  misses:      3,669,999
==11203== LLi misses:         10,163
==11203== I1  miss rate:        1.57%
==11203== LLi miss rate:        0.00%
==11203==
==11203== D   refs:      102,960,876  (61,041,772 rd   + 41,919,104 wr)
==11203== D1  misses:      5,065,700  ( 2,386,201 rd   +  2,679,499 wr)
==11203== LLd misses:        909,412  (   363,850 rd   +    545,562 wr)
==11203== D1  miss rate:         4.9% (       3.9%     +        6.3%  )
==11203== LLd miss rate:         0.8% (       0.5%     +        1.3%  )
==11203==
==11203== LL refs:         8,735,699  ( 6,056,200 rd   +  2,679,499 wr)
==11203== LL misses:         919,575  (   374,013 rd   +    545,562 wr)
==11203== LL miss rate:          0.2% (       0.1%     +        1.3%  )
==11203==
==11203== Branches:       44,906,888  (42,750,345 cond +  2,156,543 ind)
==11203== Mispredicts:     4,112,611  ( 3,492,036 cond +    620,575 ind)
==11203== Mispred rate:          9.1% (       8.1%     +       28.7%   )

Then php-7.4 yaf-3.2.0, also run 1000 times:

Elapsed time: 4.658264 sec
==18814==
==18814== I   refs:      171,236,262
==18814== I1  misses:      2,940,070
==18814== LLi misses:         10,007
==18814== I1  miss rate:        1.71%
==18814== LLi miss rate:        0.00%
==18814==
==18814== D   refs:       75,584,870  (44,264,393 rd   + 31,320,477 wr)
==18814== D1  misses:      4,302,787  ( 1,956,414 rd   +  2,346,373 wr)
==18814== LLd misses:        908,224  (   363,351 rd   +    544,873 wr)
==18814== D1  miss rate:         5.6% (       4.4%     +        7.4%  )
==18814== LLd miss rate:         1.2% (       0.8%     +        1.7%  )
==18814==
==18814== LL refs:         7,242,857  ( 4,896,484 rd   +  2,346,373 wr)
==18814== LL misses:         918,231  (   373,358 rd   +    544,873 wr)
==18814== LL miss rate:          0.3% (       0.1%     +        1.7%  )
==18814==
==18814== Branches:       32,043,848  (30,444,567 cond +  1,599,281 ind)
==18814== Mispredicts:     3,033,847  ( 2,460,365 cond +    573,482 ind)
==18814== Mispred rate:          9.4% (       8.0%     +       35.8%   )

Comparison:

3.1.4 3.2.0 Detal
IR 232,666,798 171,236,262 -26%
I1 Misses 3,669,999 2,940,070 -20%
DR 102,960,876 75,584,870 -27%
D1 Misses 5,065,700 4,302,787 -15%
Branch 44,906,888 32,043,848 -29%
MisPred 4,112,611 3,033,847 -26%

We can see that whether it's instructions executed, memory accesses, or cache miss / branch mispred, there's a very clear drop.
Of course, we can also see that the drop in D1 Misses relative to DR is disproportionate. The main reason here is the new delta caused by user scripts accessing Yaf class objects; there's more to think about for improving that part later.

Overall, this refactoring achieved more than 20% performance improvement, but that's only for the framework itself. For actual business using Yaf, it certainly won't be that pronounced — everyone can test for themselves to see the specifics.

OK, just this much introduction. For the detailed changes, see the Changelog. Welcome to download and test Yaf-3.2; I'll complete the documentation bit by bit afterwards.

As mentioned earlier, this release had a very large change in the amount of code — it's almost equivalent to a rewrite:

The amount of newly added code is close to 9000. Given this, 3.2.0 is currently released as beta. Although I did a lot of testing and did everything I could think of to ensure behavior doesn't change, I still can't guarantee full compatibility with previous versions. If you run into any problems during use, please feel free to report them on Github right away.

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.