PHP-ZPP : Transplanting zpp into PHP-CPP

Replacing Zval class

PHP-CPP Value class is a C++ virtual class, inheriting from HashParent. HashParent declares a number of abstract virtual functions. This means that Value objects are virtual function table pointer storage bigger than the zval that it wraps.

The Zval implementation of PHP-CPP is itself not compilable using C++23, because at least one deprecated code feature, is now disallowed. The PHP-CPP Zval contains a mysterious C++ template structure that has been deprecated in earlier versions, and it needs to be replaced.

// std::aligned_storage is not wanted by C++23
using aligned_zval_struct = typename std::aligned_storage<16>::type;
class Zval 
{
private:
    aligned_zval_struct _buffer;

/* Not only this is deprecated, but the Value object employs few boolean tests on the operator types returned to access the _buffer value. The boolean tests are used indicate if the value is zeroed out as in IS_UNDEFINED php type, or has a type. This was confusing when trying to understand the meaning of such code.
*/

The Zval class can be eliminated, which means something else needs to replace it inside the Value class, which is almost its only client. The alternate replacement is zpp::val_rc. This is a non-virtual C++ class that can both do its own reference counting, or receive external adjustments, via a wrap from zpp::val_ptr, or just returning the address of its zval structure.

zpp::val_rc, and its versatile zpp::val_ptr helper already have a large number of methods that can replace the work done on it by the Value wrapper class. This meant changing many Value methods so that it let existing val_rc methods do the work for it. Most important was ensuring the Value destructor did not try to dereference contained Zend Handles, as the val_rc destructor will be doing that.

The transplant went better and quicker than I expected, given this is still in a hacking operation status, after a number of code compiling adjustments. There may be some Value methods that I have underestimated the complexity and intention of its previous code in this transplant, so new bugs may be lurking. Zval had surprising few uses elsewhere in the PHP-CPP framework, which also required code adjustments. Interested people may have to look at the git change history.

The new PHP-CPP build

To distinguish the new code, this could use a new name. PHP-ZPP stands for PHP-"Zend Plus Plus", to indicate the greater lower level seperate Zend Handles Management implied in the zpp namespace classes.

In the zpp source folder, the file zhm.h includes the headers of the zpp namespace now incorporated directly into the PHP-ZPP build. The binary is already shared by the "wccz" extension.

Multithread support, redesign and testing required

This isn't enabled or tested. One current drawback is the state_init embedded functions cannot by used in current form, by a multi-thread enviroment, as they are using static shared memory for their function arguments, and even the zend_object* value passed to the fci structure. These will need to be reworked by using C++ thread local storage, and initialised by a request start handler. This would be over-written at random by multiple threads. The interned strings, and read-only data created at module init time should be Okay.

A preliminary change is to take all the embedded function execution objects and place them in their own structure, and declare an instance of it as "thread_local". All that remains to do is have wrapper functions in the zpp namespace, call the functionmember inside the new thread_local instance, which is done even by the test compiled version which runs as single thread process.

No doubt this adds to complexity, but this is why all extensions are configured and specifically compiled for the PHP version with settings for the environment that they are intended for, thanks to the phpize tool.

In the single thread process, the function objects will be configured by the state_init.init() call, and can be used for multiple requests. In the multithread process, this seems to be the RINIT call, for each thread, and so thread_local function object instances will need to initialized from the state_init.init_req() call, that is for each request. A possible trick to manage this, is to always use the init_req call, but store and check a flag in the thread_local storage block to indicate this thread storage has been initialized.

Test comparison, Wcp\XmlReader vs Wcc\XmlReader.

Time tests of the new version of Wcp\XmlReader, modified from old existing PHP-CPP extension code, from the previously shown comparison, now using PHP-ZPP framework code with zpp classes and techniques is shown below. While re-coding the Wcc\XmlReader code, as in remarks below

Versionms / iterationrelative
PHP script class1,9921.0
PHP-CPP version4,3552.8
PHP-ZPP version2,0241.02
ZPP version1,8190.91

Cached function call objects from zpp::fn_call have more than doubled the performance of this class, from the PHP-CPP to the PHP-ZPP version. These test objects embed the function call object in their class, so no problems should occur in a multithread process. Another place to be useful could be a stack allocated function call object to be used many times in a local loop.

The execution limits of this algorithm are set by the communication gulf to the XMLReader class, which acts as a pull parser for the underlying XML read library. To speed up, a more direct connection to XML parsing code is required.

Value class given push_back method

One shortcoming of Value that I wanted to overcome was to give it an obvious push_back method, corresponding to the packed array append expression in PHP. "$value[] = $extra". The point of the Value wrapper class is to emulate the functions of "dynamic type by assignment" variables of PHP. So that method now exists in PHP-ZPP Value class.

Parameters class is a std::vector<Value>

This class in PHP-CPP lets C++ functions with a fixed function parameter signature be called with a variable number of arguments from PHP. It is called in the class object methods, the ClassImpl::callMethod(INTERNAL_FUNCTION_PARAMETERS).

The Parameters class is an extension of std::vector<Value>. The Value class inherites from HashParent, and so has a pointer to a C++ class virtual function table, as well as its embedded zval structure. This is now a val_rc in PHP-ZPP, instead of Zval structure, even though they are really both a renamed zval.

In ClassImpl::callMethod, the derived ParametersImpl class, std::vector<Value> reserves space for the given number of arguments. It allocates on its call stack, via alocca(), a temporary std::vector<zval>, filled by a call to the zend_get_parameters_array_ex() API function to fetch each of the zend_execute_data arguments array. The arguments from the zend_execute_data structure are copied in a loop into their locations in the vector, emplaced then constructed as Value object, with its extra virtual function table pointer.

A slice is a simple way to copy access to an array, wihout copying all the array elements. Its briefest form is the address of the base of the array, followed by the number of valid elements. A C/C++ array slice is a slice, no matter what structure the address comes from.

Direct read access to the Parameters values can be given via const Value&, which gives access to only those Value methods that are declared as const.

The result of all this fuss is that two loops through copies of the same zval arguments are done. When the Value instances are created they will always try to increment reference counts on any passed zval handle structures, no matter how many times their buffer was copied.

This mechanism also bypasses, such final parameter type checks that PHP functions normally get by using the recommended macros designed for functions parameter parsing. By this standard practise, parameters get a final sanity check, and potentially throw errors, before further use and dispatch.

PHP-ZPP has tossed away the ParametersImpl class, and changed the Parameters class from a std::vector<Value> into a slice of the original zval array in zend_execute_data. This slice from the zend_execute_data buffer, will be present until the function returns.

There was maybe a small in the average improvement in the XmlRead comparison test after this. There is still about 5% performance gap remaining to be squeezed out.

// How to get a Value out of a slice of zend_execute_data
class PHPCPP_EXPORT Parameters : public zpp::zarg_rd {
private:
    /**
     *  The base (C++) object
     *  @var Base
     */
    Base *_object = nullptr;
public:
    Parameters(zend_execute_data* zexd);

    Base *object() const
    {
        return _object;
    }

    Value operator[](size_t index)
    {
        if (index < nargs_)
        {
            return Value(zptr0_ + ix);
        }
        throw Error("Parameters index error");
    }
}

There is a lot of code which uses the Parameters&, so it won't be possible to make a compatible replacement, except to have something with the same name. A replacement would behave something like the zpp::zarg_rd (zend arguments read), which stores a pointer to the zeroth argument in the zend_execute_data, and the number of parameters, and has a number of methods to fetch and check for various required and optional arguments, used according to whatever function expects to recieve. This hopes to emulate the PHP recommended parsing C macros provided for the same purpose, and obtain as direct as possible, values stored in declarations of individual zpp::*_ptr handle type wrapper classes.

So, a new transplant, a new declaration for Parameters. Keep the name, but it constructs itself with a pointer to zend_execute_data, and the pointer to the return_value zval*, and derives its utility entirely from this.

Using an STL allocator for PHP request memory

In using STL classes within the C++ std:: namespace, they are all being allocated using the std::allocator, which is not using the recommended PHP heap for memory allocations during request handling. This could be done, since such an allocator template exists, in the zpp folder. (alloc_phpreq.h) The Wcp\ReadXml class is using it. C++ classes for use during the request processing stage can also specify their operator::new and delete to use emalloc and efree.

class DStack {
    //... Use PHP request memory pool
    void* operator new(std::size_t msize){
        return emalloc(msize);
    }
    void operator delete(void* p){
            efree(p);
    }
};
// where to push and pop pointers to stack objects
std::vector<DStack*, alloc_phpreq<DStack*> > path_; 

Use std::string_view

std::string_view is a good example of a read-only slice. It has useful functions for string comparison, finding and making sub-strings, and can even be used for static memory constants. It never needs to allocate memory. This is one of the advantages of going beyond C++11. A std::string_view can be returned from a zpp::str_ptr, and therefore also indirectly from a zval.

Wcc\XmlRead has a few additional tricks which may be giving it the 5% edge in performance. String comparisons for tag identity uses std::string_view static constants.

Its DStack structure uses pointers for a double linked list, and therefore doesn't allocate an array to make a stack. What change could be made next to Wcp\XmlRead? I think only an internal simplified XML parser would make a significant boost to both implementations.

constexpr std::string_view pdoc_tag = "pdoc";
constexpr std::string_view s_tag = "s";

//...
bool Wcc_XmlRead::tag_start(
      str_ptr tag, 
      str_ptr key)
{
    std::string_view s = tag.vstr();
    if (s == root_tag)
    {
        pushRoot(key);
    }
    else if (s == tb_tag)
    {
        pushTable(XC_TABLE, key);
    }
    else if (s == a_tag)
    {
        pushTable(XC_ARRAY, key);
    }
    else if (s == i_tag)
    {
        setInteger(key);
    }
//...