PHP 5 Reflection API performance

前端 未结 10 980
不知归路
不知归路 2020-12-02 20:11

I\'m currently considering the use of Reflection classes (ReflectionClass and ReflectionMethod mainly) in my own MVC web framework, because I need to automatically instancia

相关标签:
10条回答
  • 2020-12-02 20:12

    The overhead is small so there is no big performance penalty other stuff like db, template processing etc are performance problems, test your framework with a simple action to see how fast it is.

    For example the code bellow (frontcontroller) which uses reflection does it jobs in a few miliseconds

    <?php
    require_once('sanitize.inc');
    
    /**
     * MVC Controller
     *
     * This Class implements  MVC Controller part
     *
     * @package MVC
     * @subpackage Controller
     *
     */
    class Controller {
    
        /**
         * Standard Controller constructor
         */
        static private $moduleName;
        static private $actionName;
        static private $params;
    
        /**
         * Don't allow construction of the controller (this is a singleton)
         *
         */
        private function __construct() {
    
        }
    
        /**
         * Don't allow cloning of the controller (this is a singleton)
         *
         */
        private function __clone() {
    
        }
    
        /**
         * Returns current module name
         *
         * @return string
         */
        function getModuleName() {
            return self :: $moduleName;
        }
    
        /**
         * Returns current module name
         *
         * @return string
         */
        function getActionName() {
            return self :: $actionName;
        }
    
        /**
         * Returns the subdomain of the request
         *
         * @return string
         */
        function getSubdomain() {
            return substr($_SERVER['HTTP_HOST'], 0, strpos($_SERVER['HTTP_HOST'], '.'));
        }
    
        function getParameters($moduleName = false, $actionName = false) {
            if ($moduleName === false or ( $moduleName === self :: $moduleName and $actionName === self :: $actionName )) {
                return self :: $params;
            } else {
                if ($actionName === false) {
                    return false;
                } else {
                    @include_once ( FRAMEWORK_PATH . '/modules/' . $moduleName . '.php' );
                    $method = new ReflectionMethod('mod_' . $moduleName, $actionName);
                    foreach ($method->getParameters() as $parameter) {
                        $parameters[$parameter->getName()] = null;
                    }
                    return $parameters;
                }
            }
        }
    
        /**
         * Redirect or direct to a action or default module action and parameters
         * it has the ability to http redirect to the specified action
         * internally used to direct to action
         *
         * @param string $moduleName
         * @param string $actionName
         * @param array $parameters
         * @param bool $http_redirect
    
         * @return bool
         */
        function redirect($moduleName, $actionName, $parameters = null, $http_redirect = false) {
            self :: $moduleName = $moduleName;
            self :: $actionName = $actionName;
            // We assume all will be ok
            $ok = true;
    
            @include_once ( PATH . '/modules/' . $moduleName . '.php' );
    
            // We check if the module's class really exists
            if (!class_exists('mod_' . $moduleName, false)) { // if the module does not exist route to module main
                @include_once ( PATH . '/modules/main.php' );
                $modClassName = 'mod_main';
                $module = new $modClassName();
                if (method_exists($module, $moduleName)) {
                    self :: $moduleName = 'main';
                    self :: $actionName = $moduleName;
                    //$_PARAMS = explode( '/' , $_SERVER['REQUEST_URI'] );
                    //unset($parameters[0]);
                    //$parameters = array_slice($_PARAMS, 1, -1);
                    $parameters = array_merge(array($actionName), $parameters); //add first parameter
                } else {
                    $parameters = array($moduleName, $actionName) + $parameters;
                    $actionName = 'index';
                    $moduleName = 'main';
                    self :: $moduleName = $moduleName;
                    self :: $actionName = $actionName;
                }
            } else { //if the action does not exist route to action index
                @include_once ( PATH . '/modules/' . $moduleName . '.php' );
                $modClassName = 'mod_' . $moduleName;
                $module = new $modClassName();
                if (!method_exists($module, $actionName)) {
                    $parameters = array_merge(array($actionName), $parameters); //add first parameter
                    $actionName = 'index';
                }
                self :: $moduleName = $moduleName;
                self :: $actionName = $actionName;
            }
            if (empty($module)) {
                $modClassName = 'mod_' . self :: $moduleName;
                $module = new $modClassName();
            }
    
            $method = new ReflectionMethod('mod_' . self :: $moduleName, self :: $actionName);
    
            //sanitize and set method variables
            if (is_array($parameters)) {
                foreach ($method->getParameters() as $parameter) {
                    $param = current($parameters);
                    next($parameters);
                    if ($parameter->isDefaultValueAvailable()) {
                        if ($param !== false) {
                            self :: $params[$parameter->getName()] = sanitizeOne(urldecode(trim($param)), $parameter->getDefaultValue());
                        } else {
                            self :: $params[$parameter->getName()] = null;
                        }
                    } else {
                        if ($param !== false) {//check if variable is set, avoid notice
                            self :: $params[$parameter->getName()] = sanitizeOne(urldecode(trim($param)), 'str');
                        } else {
                            self :: $params[$parameter->getName()] = null;
                        }
                    }
                }
            } else {
                foreach ($method->getParameters() as $parameter) {
                    self :: $params[$parameter->getName()] = null;
                }
            }
    
            if ($http_redirect === false) {//no redirecting just call the action
                if (is_array(self :: $params)) {
                    $method->invokeArgs($module, self :: $params);
                } else {
                    $method->invoke($module);
                }
            } else {
                //generate the link to action
                if (is_array($parameters)) { // pass parameters
                    $link = '/' . $moduleName . '/' . $actionName . '/' . implode('/', self :: $params);
                } else {
                    $link = '/' . $moduleName . '/' . $actionName;
                }
                //redirect browser
                header('Location:' . $link);
    
                //if the browser does not support redirecting then provide a link to the action
                die('Your browser does not support redirect please click here <a href="' . $link . '">' . $link . '</a>');
            }
            return $ok;
        }
    
        /**
         * Redirects to action contained within current module
         */
        function redirectAction($actionName, $parameters) {
            self :: $actionName = $actionName;
            call_user_func_array(array(&$this, $actionName), $parameters);
        }
    
        public function module($moduleName) {
            self :: redirect($moduleName, $actionName, $parameters, $http_redirect = false);
        }
    
        /**
         * Processes the client's REQUEST_URI and handles module loading/unloading and action calling
         *
         * @return bool
         */
        public function dispatch() {
            if ($_SERVER['REQUEST_URI'][strlen($_SERVER['REQUEST_URI']) - 1] !== '/') {
                $_SERVER['REQUEST_URI'] .= '/'; //add end slash for safety (if missing)
            }
    
            //$_SERVER['REQUEST_URI'] = @str_replace( BASE ,'', $_SERVER['REQUEST_URI']);
            // We divide the request into 'module' and 'action' and save paramaters into $_PARAMS
            if ($_SERVER['REQUEST_URI'] != '/') {
                $_PARAMS = explode('/', $_SERVER['REQUEST_URI']);
    
                $moduleName = $_PARAMS[1]; //get module name
                $actionName = $_PARAMS[2]; //get action
                unset($_PARAMS[count($_PARAMS) - 1]); //delete last
                unset($_PARAMS[0]);
                unset($_PARAMS[1]);
                unset($_PARAMS[2]);
            } else {
                $_PARAMS = null;
            }
    
            if (empty($actionName)) {
                $actionName = 'index'; //use default index action
            }
    
            if (empty($moduleName)) {
                $moduleName = 'main'; //use default main module
            }
            /* if (isset($_PARAMS))
    
              {
    
              $_PARAMS = array_slice($_PARAMS, 3, -1);//delete action and module from array and pass only parameters
    
              } */
            return self :: redirect($moduleName, $actionName, $_PARAMS);
        }
    }
    
    0 讨论(0)
  • 2020-12-02 20:13

    In my case reflection it is only 230% slower than calling class method directly, which as fast as call_user_func function.

    0 讨论(0)
  • 2020-12-02 20:19

    Calling a static function 1 million times will cost ~ 0.31 seconds on my machine. When using a ReflectionMethod, it costs ~ 1.82 seconds. That means it is ~ 500% more expensive to use the Reflection API.

    This is the code I used by the way:

    <?PHP
    
    class test
    {
        static function f(){
                return;
        }
    }
    
    $s = microtime(true);
    for ($i=0; $i<1000000; $i++)
    {
        test::f('x');
    }
    echo ($a=microtime(true) - $s)."\n";
    
    $s = microtime(true);
    for ($i=0; $i<1000000; $i++)
    {
        $rm = new ReflectionMethod('test', 'f');
        $rm->invokeArgs(null, array('f'));
    }
    
    echo ($b=microtime(true) - $s)."\n";
    
    echo 100/$a*$b;
    

    Obviously, the actual impact depends on the number of calls you expect to do

    0 讨论(0)
  • 2020-12-02 20:20

    I wanted something newer, so take a look at this repo. From the summary:

    • PHP 7 is almost twice as fast as PHP 5 in case of reflections - This does not directly indicate that reflections are faster on PHP7, the PHP7 core have just received a great optimization and all code will benefit from this.
    • Basic reflections are quite fast - Reading methods and doc comments for 1000 classes cost just a few milliseconds. Parsing/Autoloading the classfiles does take a lot more time than the actual reflection mechanics. On our testsystem it takes about 300ms to load 1000 class files into memory (require/include/autoload) - And than just 1-5ms to use reflection parsing (doc comments, getMethods, etc...) on the same amount of classes.
    • Conclusion: Reflections are fast and in normal use cases you can ignore that performance impact. However, it is always recommended to only parse what is necessary. And, caching reflections doesn't give you any noticeable benefit on performance.

    Also, check out another benchmark.

    Those results were obtained on a development OS X machine using PHP 5.5.5. [...]

    • Read a single property on one object: The closure is slightly faster.

    • Read a single property on many objects: Reflection is way faster.

    • Reading all the properties of an object: The closure is faster.

    • Writing a single property on one object: Reflection is slightly faster.

    • Writing a single property on many objects: Reflection is way faster.

    0 讨论(0)
  • 2020-12-02 20:21

    based on the code that @Alix Axel provided

    So for completeness I decided to wrap each option in a class and include caching of objects where applicable. here was the results and code The results on PHP 5.6 on an i7-4710HQ

    array (
      'Direct' => '5.18932366',
      'Variable' => '5.62969398',
      'Reflective' => '6.59285069',
      'User' => '7.40568614',
    )
    

    Code:

    function Benchmark($callbacks, $iterations = 100, $relative = false)
    {
        set_time_limit(0);
    
        if (count($callbacks = array_filter((array) $callbacks, 'is_callable')) > 0)
        {
            $result = array_fill_keys(array_keys($callbacks), 0);
            $arguments = array_slice(func_get_args(), 3);
    
            for ($i = 0; $i < $iterations; ++$i)
            {
                foreach ($result as $key => $value)
                {
                    $value = microtime(true); call_user_func_array($callbacks[$key], $arguments); $result[$key] += microtime(true) - $value;
                }
            }
    
            asort($result, SORT_NUMERIC);
    
            foreach (array_reverse($result) as $key => $value)
            {
                if ($relative === true)
                {
                    $value /= reset($result);
                }
    
                $result[$key] = number_format($value, 8, '.', '');
            }
    
            return $result;
        }
    
        return false;
    }
    
    class foo {
        public static function bar() {
            return __METHOD__;
        }
    }
    
    class TesterDirect {
        public function test() {
            return foo::bar($_SERVER['REQUEST_TIME']);
        }
    }
    
    class TesterVariable {
        private $class = 'foo';
    
        public function test() {
            $class = $this->class;
    
            return $class::bar($_SERVER['REQUEST_TIME']);
        }
    }
    
    class TesterUser {
        private $method = array('foo', 'bar');
    
        public function test() {
            return call_user_func($this->method, $_SERVER['REQUEST_TIME']);
        }
    }
    
    class TesterReflective {
        private $class = 'foo';
        private $reflectionMethod;
    
        public function __construct() {
            $this->reflectionMethod = new ReflectionMethod($this->class, 'bar');
        }
    
        public function test() {
            return $this->reflectionMethod->invoke(null, $_SERVER['REQUEST_TIME']);
        }
    }
    
    $testerDirect = new TesterDirect();
    $testerVariable = new TesterVariable();
    $testerUser = new TesterUser();
    $testerReflective = new TesterReflective();
    
    fputs(STDOUT, var_export(Benchmark(array(
        'Direct' => array($testerDirect, 'test'),
        'Variable' => array($testerVariable, 'test'),
        'User' => array($testerUser, 'test'),
        'Reflective' => array($testerReflective, 'test')
    ), 10000000), true));
    
    0 讨论(0)
  • 2020-12-02 20:22

    I benchmarked these 3 options (the other benchmark wasn't splitting CPU cycles and was 4y old):

    class foo {
        public static function bar() {
            return __METHOD__;
        }
    }
    
    function directCall() {
        return foo::bar($_SERVER['REQUEST_TIME']);
    }
    
    function variableCall() {
        return call_user_func(array('foo', 'bar'), $_SERVER['REQUEST_TIME']);
    }
    
    function reflectedCall() {
        return (new ReflectionMethod('foo', 'bar'))->invoke(null, $_SERVER['REQUEST_TIME']);
    }
    

    The absolute time taken for 1,000,000 iterations:

    print_r(Benchmark(array('directCall', 'variableCall', 'reflectedCall'), 1000000));

    Array
    (
        [directCall] => 4.13348770
        [variableCall] => 6.82747173
        [reflectedCall] => 8.67534351
    )
    

    And the relative time, also with 1,000,000 iterations (separate run):

    ph()->Dump(Benchmark(array('directCall', 'variableCall', 'reflectedCall'), 1000000, true));

    Array
    (
        [directCall] => 1.00000000
        [variableCall] => 1.67164707
        [reflectedCall] => 2.13174915
    )
    

    It seems that the reflection performance was greatly increased in 5.4.7 (from ~500% down to ~213%).

    Here's the Benchmark() function I used if anyone wants to re-run this benchmark:

    function Benchmark($callbacks, $iterations = 100, $relative = false)
    {
        set_time_limit(0);
    
        if (count($callbacks = array_filter((array) $callbacks, 'is_callable')) > 0)
        {
            $result = array_fill_keys($callbacks, 0);
            $arguments = array_slice(func_get_args(), 3);
    
            for ($i = 0; $i < $iterations; ++$i)
            {
                foreach ($result as $key => $value)
                {
                    $value = microtime(true);
                    call_user_func_array($key, $arguments);
                    $result[$key] += microtime(true) - $value;
                }
            }
    
            asort($result, SORT_NUMERIC);
    
            foreach (array_reverse($result) as $key => $value)
            {
                if ($relative === true)
                {
                    $value /= reset($result);
                }
    
                $result[$key] = number_format($value, 8, '.', '');
            }
    
            return $result;
        }
    
        return false;
    }
    
    0 讨论(0)
提交回复
热议问题