zephir-lang / zephir

Zephir is a compiled high-level language aimed to ease the creation of C-extensions for PHP
https://zephir-lang.com
MIT License
3.31k stars 466 forks source link

When does Zephir Shine? Only 10% Performance Gain? #2119

Closed ajhalls closed 3 years ago

ajhalls commented 4 years ago

I am looking to optimize my web API which receives a steady stream of medical data from hundreds of offices. As a proof of concept, I rewrote a portion of the API in Zephir to build the Patient object.

The Zephir Code:

    #under class Checks
    public static function PhoneOrNull(incoming=NULL)
    {
        if(empty(incoming)){
            return NULL;
        }

            if(strlen(incoming)>=10){
                var original_phone = urldecode(incoming);

                var phone = preg_replace("/[^0-9]/", "", original_phone);
                if (strlen(phone) == 10 && str_split(phone)[0] != 1) {
                    return "+1" . phone;
                }
                if (strlen(phone) == 11 && str_split(phone)[0] == 1) {
                    return "+" . phone;
                }

                return original_phone;

            }
        return NULL;
    }

    #under class Patients
    public static function NewPatient(incoming=NULL)
    {
            if(incoming == NULL){
            return NULL;
            }
            var gender = 0;
            var NewRecord=    [
                    "reference_id" : incoming->reference_id,
                    "parent_id" : Checks::StringOrNull(incoming->parent_id),
                    "family_id" : Checks::StringOrNull(incoming->family_id),
                    "first_name" : incoming->first_name,
                    "last_name" : incoming->last_name,
                    "email" : Checks::StringOrNull(incoming->email),
                    "home_phone" : Checks::PhoneOrNull(incoming->home_phone),
                    "work_phone" : Checks::PhoneOrNull(incoming->work_phone) ,
                    "wireless_phone" : Checks::PhoneOrNull(incoming->wireless_phone) ,
                    "preferred_confirm" : Checks::PhoneOrNull(incoming->preferred_confirm),
                    "preferred_contact" : Checks::PhoneOrNull(incoming->preferred_contact),
                    "preferred_recall" : Checks::PhoneOrNull(incoming->preferred_recall) ,
                    "preferred_confidential" : Checks::PhoneOrNull(incoming->preferred_confidential),
                    "address" : Checks::StringOrNull(incoming->address),
                    "city" : Checks::StringOrNull(incoming->city),
                    "state" : Checks::StringOrNull(incoming->state),
                    "zip" : Checks::StringOrNull(incoming->zip),
                    "birthday" : isset(incoming->birthday) && strtotime(incoming->birthday) > strtotime("-100 years") ? incoming->birthday : null,
                    "gender" : gender,
                    "is_text" : 1,
                    "is_email" : 1,
                    "is_phone" : 1,
                    "is_mail" : 1,
                    "reference_status" : Checks::StringOrNull(incoming->reference_status),
                    "first_visit" : isset(incoming->first_visit) && strtotime(incoming->first_visit) > strtotime("-100 years") ? incoming->first_visit : null,
                    "appointment_total" : Checks::IntOrNull(incoming->appointment_total),
                    "appointment_missed" : Checks::IntOrNull(incoming->appointment_missed),
                    "appointment_show_rate" : Checks::IntOrNull(incoming->appointment_show_rate),
                    "ssn" : isset(incoming->ssn) && strpos(incoming->ssn, "-") === false && (incoming->ssn > 0 && incoming->ssn <= 9999) ? incoming->ssn : null
                ];
                return NewRecord;
    }

Our old way was:

$createPatients[] = new Patient([
                    'reference_id' => $record->reference_id,
                    'parent_id' => isset($record->parent_id) ? $record->parent_id : null,
                    'family_id' => isset($record->family_id) ? $record->family_id : null,
                    'office_id' => $user->office_id,
                    'first_name' => $record->first_name,
                    'last_name' => $record->last_name,
                    'email' => isset($record->email) ? $record->email : null,
                    'home_phone' => isset($record->home_phone) ? databasePhone($record->home_phone) : null,
                    'work_phone' => isset($record->work_phone) ? databasePhone($record->work_phone) : null,
                    'wireless_phone' => isset($record->wireless_phone) ? databasePhone($record->wireless_phone) : null,
                    'preferred_confirm' => isset($record->preferred_confirm) ? databasePhone($record->preferred_confirm) : null,
                    'preferred_contact' => isset($record->preferred_contact) ? databasePhone($record->preferred_contact) : null,
                    'preferred_recall' => isset($record->preferred_recall) ? databasePhone($record->preferred_recall) : null,
                    'preferred_confidential' => isset($record->preferred_confidential) ? databasePhone($record->preferred_confidential) : null,
                    'address' => isset($record->address) ? $record->address : null,
                    'city' => isset($record->city) ? $record->city : null,
                    'state' => isset($record->state) ? $record->state : null,
                    'zip' => isset($record->zip) ? $record->zip : null,
                    'birthday' => isset($record->birthday) && strtotime($record->birthday) > strtotime('-100 years') ? $record->birthday : null,
                    'gender' => $gender,
                    'is_text' => 1,
                    'is_email' => 1,
                    'is_phone' => 1,
                    'is_mail' => 1,
                    'language_id' => $language_id,
                    'communication_id' => $user->office->communication_id,
                    'reference_status' => isset($record->reference_status) ? $record->reference_status : null,
                    'status_type_id' => $type_id,
                    'first_visit' => isset($record->first_visit) && strtotime($record->first_visit) > strtotime('-100 years') ? $record->first_visit : null,
                    'appointment_total' => isset($record->appointment_total) ? $record->appointment_total : null,
                    'appointment_missed' => isset($record->appointment_missed) ? $record->appointment_missed : null,
                    'appointment_show_rate' => isset($record->appointment_show_rate) ? $record->appointment_show_rate : null,
                    'ssn' => isset($record->ssn) && strpos($record->ssn, '-') === false && ($record->ssn > 0 && $record->ssn <= 9999) ? $record->ssn : null,
                ]);

I had stripped out a couple keys in the beginning of the test such as 'communication_id' => $user->office->communication_id which wasn't part of the incoming data just to be simpler. So the Zephir one was doing about 3 less keys than the raw PHP per cycle.

To test, I used the following:

$times=array();
for ($f=0; $f < 50; $f++) { 

    $time_start = microtime(true); 
    for ($i=0; $i < 500; $i++) { 
                      $createPatients[] = new Patient(\Rd\Patients::NewPatient($record));
    }

    $time_end = microtime(true);
    //dividing with 60 will give the execution time in minutes otherwise seconds
    $times[] = ($time_end - $time_start);

}
//execution time of the script
print_r($times);

The average times were: ~0.068428039550781 Seconds - Zephir ~0.076008081436157 Seconds - PHP

I have run the test a number of times and it seems Zephir gives me about a 10% boost, is that expected? Is there something more I should be doing to get better performance gains? Seems like I could almost get that just optimizing what I have a little more.

The tasks I was looking to move over would be string manipulations such as the PhoneOrNull, and those hit most often by the APIs. I am looking to optimize which processes are handling data to minimize calls to the database, so I would be passing in 2 objects, cross comparing values, and only sending updates to the database when records actually change.

Running on Ubuntu 18.04, PHP 7.2, Zephir 0.12.19

cypherbits commented 4 years ago

Hi, I discovered this project just a few days ago and I thought it was a great idea and could benefit with performance.

But just learned the project is officially abandoned...

Besides, your tests show it actually does not improves performance too much...

You tested on PHP 7.2 while on the latest 7.4 it should be even faster.

Are you using Opcache or Preload in the test?

ajhalls commented 4 years ago

I didn't realize it was abandoned (https://www.reddit.com/r/PHP/comments/iogji4/zephir_will_not_be_maintained_any_longer_and_will/), thank you for letting me know. I wasn't using Opcache or Preload.

I know I can upgrade the PHP for additional performance, part of why I was building this into modules was to be able to whitelabel my application while maintaining some control over it through compiled obfuscation, the other was performance. Looking into other options.

Other options seem to be to build it in Rust and then use PHP FFI, or build a standalone API application server to handle those transformations for me. While I do a fair amount of .NET C# work, I haven't done much with C or C++ and feel that Rust may be more beneficial to learn if I have to learn something new. I realize PHP has a .NET option, but only if you run on a Windows server, which I will not likely ever do.

Jeckerson commented 3 years ago

@cypherbits

But just learned the project is officially abandoned...

@ajhalls

I didn't realize it was abandoned

Reddit

Zephir will not be maintained any longer and will not be compatible with PHP 8

I completely understand that we live in era of fake news, but on original news was written, quote: https://blog.phalcon.io/post/the-future-of-phalcon

Now that Serghei has stepped down, we have an issue with Zephir. We no longer have any active maintainers for the project to assume the lead and help with development/bugs etc.

and more important, quoute:

With no other options available, we will put active development of Zephir on hold, and we do not expect it to be compatible with PHP 8. Maintainers are always welcome to step up and help with Zephir.


@ajhalls Now about performance diff only 10%. You won't gain any extra performance in loops and cycles like yours due array as char and massive overhead, so you have near the same performance in PHP or in C. Single difference will be in memory usage, ~ x2 more in PHP.