company_banner
19 февраля 2015 в 17:07

Разбор вызовов функций в PHP перевод

Этот пост посвящён оптимизации PHP с помощью профайлера Blackfire в PHP-скрипте. Нижеприведённый текст является подробным техническим объяснением статьи в блоге Blackfire.

Обычно применяется метод strlen:

if (strlen($name) > 49) {
...
}

Однако такой вариант примерно на 20% медленнее этого:

if (isset($name[49])) {
...
}

Выглядит неплохо. Наверняка вы уже собрались открыть ваши исходники и заменить все вызовы strlen() на isset(). Но если внимательно прочитать оригинальную статью, то можно заметить, что причина 20-процентной разницы в производительности — многократные вызовы strlen(), порядка 60-80 тысяч итераций.

Почему?


Дело не в том, как strlen() вычисляет длины строк в PHP, ведь все они уже известны к моменту вызова этого метода. Большинство по возможности вычисляется еще во время компиляции. Длина PHP-строки, отправляемой в память, инкапсулируется в С-структуру, содержащую эту самую строку. Поэтому strlen() просто считывает эту информацию и возвращает как есть. Вероятно, это самая быстрая из PHP-функций, потому что она вообще ничего не вычисляет. Вот ее исходный код:

ZEND_FUNCTION(strlen)
{
    char *s1;
    int s1_len;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &s1, &s1_len) == FAILURE) {
        return;
    }

    RETVAL_LONG(s1_len);
}

Учитывая, что isset() не является функцией, причина 20-процентного проигрыша в производительности strlen() по большей части заключается в сопутствующих задержках при вызове функции в движке Zend.

Есть и ещё один момент: при сравнении производительности strlen() с чем-либо ещё добавляется дополнительный opcode. А в случае с isset() используется лишь один уникальный opcode.

Пример дизассемблированной структуры if(strlen()):

line     #* I O op                           fetch          ext  return  operands
-----------------------------------------------------------------------------------
   3     0  >   SEND_VAR                                                 !0
         1      DO_FCALL                                      1  $0      'strlen'
         2      IS_SMALLER                                       ~1      42, $0
         3    > JMPZ                                                     ~1, ->5
   5     4  > > JMP                                                      ->5
   6     5  > > RETURN                                                   1

А вот семантически эквивалентная структура if(isset()):

line     #* I O op                           fetch          ext  return  operands
-----------------------------------------------------------------------------------
   3     0  >   ISSET_ISEMPTY_DIM_OBJ                       33554432  ~0      !0, 42
         1    > JMPZ                                                  ~0, ->3
   5     2  > > JMP                                                       ->3
   6     3  > > RETURN                                                     1

Как видите, в коде isset() не задействуется вызов какой-либо функции (DO_FCALL). Также здесь нет opcode IS_SMALLER (просто проигнорируйте операторы RETURN); isset() напрямую возвращает булево значение; strlen() же сначала возвращает временную переменную, затем она передаётся в opcode IS_SMALLER, а уже финальный результат вычисляется с помощью if(). То есть в структуре strlen() используется два opcode, а в структуре isset() — один. Поэтому isset() демонстрирует более высокую производительность, ведь одна операция выполняется обычно быстрее, чем две.

Давайте теперь разберёмся, как в PHP работают вызовы функций и чем они отличаются от isset().

Вызовы функций в PHP


Сложнее всего анализировать ту часть виртуальной машины (момента исполнения кода PHP), которая связана с вызовами функций. Я постараюсь изложить самую суть, не углубляясь в моменты, имеющие отношение к вызовам функций.

Для начала разберём время выполнения (runtime) вызовов. Во время компиляции (compile time) на выполнение операций, связанных с PHP-функциями, требуется много ресурсов. Но если вы будете использовать кэш opcode, то во время компиляции у вас не будет проблем.

Предположим, что у нас скомпилировался некий скрипт. Давайте проанализируем только то, что происходит во время выполнения. Вот как выглядит дамп opcode вызова внутренней функции (в данном случае strlen()):

strlen($a);

line     #* I O op                           fetch          ext  return  operands
-----------------------------------------------------------------------------------
3     0  >   SEND_VAR                                                 !0
      1      DO_FCALL                                      1          'strlen'

Для понимания механизма вызова функции, необходимо знать две вещи:

  • вызов функции и вызов метода — это одно и то же
  • вызов пользовательской функции и вызов внутренней функции обрабатываются по-разному

Вот почему в последнем примере говорится о вызове «внутренней» функции: strlen() представляет собой PHP-функцию, являющуюся частью С-кода. Если бы мы сделали дамп opcode «пользовательской» PHP-функции (то есть функции, которая написана на языке PHP), то могли бы получить либо точно такой же, либо какой-то другой opcode.

Дело в том, что независимо от того, известна PHP эта функция или нет, он не генерирует такой же opcode во время компиляции. Очевидно, что внутренние PHP-функции известны во время компиляции, поскольку они объявляются до того, как запускается компилятор. Но ясности в отношении пользовательских функций может и не быть, потому что они могут вызываться ещё до своего объявления. Если говорить об исполнении, то внутренние PHP-функции эффективнее пользовательских, к тому же им доступно больше механизмов валидации.

Из приведённого выше примера видно, что для управления вызовами функций используется больше одного opcode. Также нужно помнить, что функции имеют собственный стек. В PHP, как и в любом другом языке, для вызова функции сначала нужно создать стековый кадр и передать в него аргументы функции. Затем вы вызываете функцию, которая эти аргументы подтянет из стека для своих нужд. По завершении вызова вам придётся уничтожить созданный ранее кадр.

Так в общем виде выглядит схема работы с вызовами функций. Однако в PHP предусмотрена оптимизация процедур создания и удаления стекового кадра; кроме того, можно отложить их выполнение, чтобы не приходилось делать все эти телодвижения при каждом вызове функции.

Opcode SEND_VAR отвечает за отправку аргументов в стековый кадр. Компилятор в обязательном порядке генерирует такой opcode до вызова функции. Причём для каждой переменной создаётся свой собственный:

$a = '/';
setcookie('foo', 'bar', 128, $a);

line     #* I O op                           fetch          ext  return  operands
-----------------------------------------------------------------------------------
   3     0  >   ASSIGN                                                   !0, '%2F'
   4     1      SEND_VAL                                                 'foo'
         2      SEND_VAL                                                 'bar'
         3      SEND_VAL                                                 128
         4      SEND_VAR                                                 !0
         5      DO_FCALL                                      4          'setcookie'

Здесь вы видите ещё один opcode — SEND_VAL. Всего существует 4 вида opcode для отправки чего-либо в стек функции:

  • SEND_VAL: отправляет значение константы (строковое, целочисленное и т.д.)
  • SEND_VAR: отправляет PHP-переменную ($a)
  • SEND_REF: отправляет PHP-переменную в виде ссылки в функцию, которая принимает аргумент ссылкой
  • SEND_VAR_NO_REF: оптимизированный обработчик, применяемый в случаях с вложенными функциями


Что делает SEND_VAR?

ZEND_VM_HELPER(zend_send_by_var_helper, VAR|CV, ANY)
{
    USE_OPLINE
    zval *varptr;
    zend_free_op free_op1;
    varptr = GET_OP1_ZVAL_PTR(BP_VAR_R);

    if (varptr == &EG(uninitialized_zval)) {
        ALLOC_ZVAL(varptr);
        INIT_ZVAL(*varptr);
        Z_SET_REFCOUNT_P(varptr, 0);
    } else if (PZVAL_IS_REF(varptr)) {
        zval *original_var = varptr;

        ALLOC_ZVAL(varptr);
        ZVAL_COPY_VALUE(varptr, original_var);
        Z_UNSET_ISREF_P(varptr);
        Z_SET_REFCOUNT_P(varptr, 0);
        zval_copy_ctor(varptr);
    }
    Z_ADDREF_P(varptr);
    zend_vm_stack_push(varptr TSRMLS_CC);
    FREE_OP1();  /* for string offsets */

    CHECK_EXCEPTION();
    ZEND_VM_NEXT_OPCODE();
}

SEND_VAR проверяет, является ли переменная ссылкой. Если да, то он её отделяет, тем самым создавая несоответствие ссылки. Почему это очень плохо, вы можете почитать в другой моей статье. Затем SEND_VAR добавляет количество ссылок на нее (ссылка здесь – это не ссылка в терминах PHP, то есть не та которая &, а просто показатель того, сколько кто использует это значение) — к переменной и отправляет в стек виртуальной машины:

Z_ADDREF_P(varptr);
zend_vm_stack_push(varptr TSRMLS_CC);

Каждый раз, вызывая функцию, вы увеличиваете на единицу refcount каждого переменного аргумента стека. Это происходит потому, что на переменную будет ссылаться не код функции, а её стек. Отправка переменной в стек слабо влияет на производительность, но стек занимает память. Он размещается в ней во время исполнения, но его размер высчитывается во время компиляции. После того как мы отправили в стек переменную, запускаем DO_FCALL. Ниже — пример того, какое количество кода и проверок используется только для того, чтобы мы считали вызовы PHP-функций «медленными» операторами (slow statement):

ZEND_VM_HANDLER(60, ZEND_DO_FCALL, CONST, ANY)
{
    USE_OPLINE
    zend_free_op free_op1;
    zval *fname = GET_OP1_ZVAL_PTR(BP_VAR_R);
    call_slot *call = EX(call_slots) + opline->op2.num;

    if (CACHED_PTR(opline->op1.literal->cache_slot)) {
        EX(function_state).function = CACHED_PTR(opline->op1.literal->cache_slot);
    } else if (UNEXPECTED(zend_hash_quick_find(EG(function_table), Z_STRVAL_P(fname), Z_STRLEN_P(fname)+1, Z_HASH_P(fname), (void **) &EX(function_state).function)==FAILURE)) {
        SAVE_OPLINE();
        zend_error_noreturn(E_ERROR, "Call to undefined function %s()", fname->value.str.val);
    } else {
        CACHE_PTR(opline->op1.literal->cache_slot, EX(function_state).function);
    }
    call->fbc = EX(function_state).function;
    call->object = NULL;
    call->called_scope = NULL;
    call->is_ctor_call = 0;
    EX(call) = call;

    FREE_OP1();

    ZEND_VM_DISPATCH_TO_HELPER(zend_do_fcall_common_helper);
}

Как видите, здесь проведены небольшие проверки и использованы различные кэши. Например, указатель обработчика нашёл самый первый вызов, а потом был закэширован в главный кадр виртуальной машины, чтобы каждый последующий вызов смог использовать этот указатель.

Далее мы вызываем zend_do_fcall_common_helper(). Я не буду выкладывать здесь код этой функции, он слишком объёмный. Я покажу только те операции, которые там были выполнены. Если вкратце, это множество различных проверок, сделанных во время исполнения. PHP — динамический язык, во время выполнения он может объявлять новые функции и классы, попутно автоматически загружая файлы. Поэтому PHP вынужден проводить во время выполнения множество проверок, что плохо сказывается на производительности. Но от этого никуда не деться.

if (UNEXPECTED((fbc->common.fn_flags & (ZEND_ACC_ABSTRACT|ZEND_ACC_DEPRECATED)) != 0)) {
        if (UNEXPECTED((fbc->common.fn_flags & ZEND_ACC_ABSTRACT) != 0)) {
            zend_error_noreturn(E_ERROR, "Cannot call abstract method %s::%s()", fbc->common.scope->name, fbc->common.function_name);
            CHECK_EXCEPTION();
            ZEND_VM_NEXT_OPCODE(); /* Never reached */
        }
        if (UNEXPECTED((fbc->common.fn_flags & ZEND_ACC_DEPRECATED) != 0)) {
            zend_error(E_DEPRECATED, "Function %s%s%s() is deprecated",
                fbc->common.scope ? fbc->common.scope->name : "",
                fbc->common.scope ? "::" : "",
                fbc->common.function_name);
        }
    }
    if (fbc->common.scope &&
        !(fbc->common.fn_flags & ZEND_ACC_STATIC) &&
        !EX(object)) {

        if (fbc->common.fn_flags & ZEND_ACC_ALLOW_STATIC) {
            /* FIXME: output identifiers properly */
            zend_error(E_STRICT, "Non-static method %s::%s() should not be called statically", fbc->common.scope->name, fbc->common.function_name);
        } else {
            /* FIXME: output identifiers properly */
            /* An internal function assumes $this is present and won't check that. So PHP would crash by allowing the call. */
            zend_error_noreturn(E_ERROR, "Non-static method %s::%s() cannot be called statically", fbc->common.scope->name, fbc->common.function_name);
        }
    }

Видите, сколько проверок? Идём дальше:

if (fbc->type == ZEND_USER_FUNCTION || fbc->common.scope) {
    should_change_scope = 1;
    EX(current_this) = EG(This);
    EX(current_scope) = EG(scope);
    EX(current_called_scope) = EG(called_scope);
    EG(This) = EX(object);
    EG(scope) = (fbc->type == ZEND_USER_FUNCTION || !EX(object)) ? fbc->common.scope : NULL;
    EG(called_scope) = EX(call)->called_scope;
}

Вы знаете, что каждое тело функции имеет собственную область видимости переменной. Движок переключает таблицы видимости перед вызовом кода функции, так что если он запросит переменную, то она будет найдена в соответствующей таблице. А поскольку функции и методы по сути одно и то же, можете почитать о том, как забиндить на метод указатель $this.

if (fbc->type == ZEND_INTERNAL_FUNCTION) {

Как я уже говорил выше, внутренние функции (являющиеся частью С) имеют другой путь выполнения, не такой, как у пользовательских функций. Обычно он короче и лучше оптимизирован, потому что мы можем сообщить движку информацию о внутренних функциях, чего нельзя сказать о пользовательских.

fbc->internal_function.handler(opline->extended_value, ret->var.ptr, (fbc->common.fn_flags & ZEND_ACC_RETURN_REFERENCE) ? &ret->var.ptr : NULL, EX(object), RETURN_VALUE_USED(opline) TSRMLS_CC);

Приведённая строка вызывает обработчик внутренней функции. В случае с нашим примером относительно strlen() данная строка вызовет код:

/* PHP's strlen() source code */
ZEND_FUNCTION(strlen)
{
    char *s1;
    int s1_len;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &s1, &s1_len) == FAILURE) {
        return;
    }

    RETVAL_LONG(s1_len);
}

Что делает strlen()? Он извлекает аргумент из стека с помощью zend_parse_parameters(). Это «медленная» функция, потому что ей приходится поднимать стек и конвертировать аргумент в ожидаемый функцией тип (в нашем случае — в строковый). Поэтому независимо от того, что вы передадите в стек для strlen(), ей может понадобиться конвертировать аргумент, а это не самый лёгкий процесс с точки зрения производительности. Исходный код zend_parse_parameters() дает неплохое представление о том, сколько операций приходится выполнить процессору во время извлечения аргументов из стекового кадра функции.

Переходим к следующему шагу. Мы только что выполнили код тела функции, теперь нам нужно «прибраться». Начнём с восстановления области видимости:

if (should_change_scope) {
        if (EG(This)) {
            if (UNEXPECTED(EG(exception) != NULL) && EX(call)->is_ctor_call) {
                if (EX(call)->is_ctor_result_used) {
                    Z_DELREF_P(EG(This));
                }
                if (Z_REFCOUNT_P(EG(This)) == 1) {
                    zend_object_store_ctor_failed(EG(This) TSRMLS_CC);
                }
            }
            zval_ptr_dtor(&EG(This));
        }
        EG(This) = EX(current_this);
        EG(scope) = EX(current_scope);
        EG(called_scope) = EX(current_called_scope);
    }

Затем очистим стек:

zend_vm_stack_clear_multiple(1 TSRMLS_CC);

И, наконец, если во время выполнения функции были сделаны какие-то исключения, нужно направить виртуальную машину на блок отлова этого исключения:

if (UNEXPECTED(EG(exception) != NULL)) {
        zend_throw_exception_internal(NULL TSRMLS_CC);
        if (RETURN_VALUE_USED(opline) && EX_T(opline->result.var).var.ptr) {
            zval_ptr_dtor(&EX_T(opline->result.var).var.ptr);
        }
        HANDLE_EXCEPTION();
    }

О вызовах PHP-функций


Теперь вы можете представить, сколько времени тратит компьютер на вызов «очень маленькой и простой» функции strlen(). А поскольку она вызывается многократно, увеличьте это время, скажем, в 25 000 раз. Вот так микро- и миллисекунды превращаются в полноценные секунды… Обратите внимание, что я продемонстрировал только самые важные для нас инструкции во время каждого вызова PHP-функции. После этого случается ещё много всего интересного. Также имейте в виду, что в случае с strlen() «полезную работу» выполняет лишь одна строка, а сопутствующие процедуры по подготовке вызова функции по объёму больше, чем «полезная» часть кода. Однако в большинстве случаев собственный код функций всё же больше влияет на производительность, чем «вспомогательный» код движка.

Та часть кода PHP, которая относится к вызову функций, в PHP 7 была переработана с целью улучшения производительности. Однако это далеко не конец, и исходный код PHP ещё не раз будет оптимизироваться с каждым новым релизом. Не были забыты и более старые версии, вызовы функций были оптимизированы и в версиях от 5.3 до 5.5. Например, в версиях с 5.4 до 5.5 был изменён способ вычисления и создания стекового кадра (с сохранением совместимости). Ради интереса можете сравнить изменения в исполняющем модуле и способе вызова функций, сделанные в версии 5.5 по сравнению с 5.4.

Хочу подчеркнуть: все вышесказанное не значит, что PHP плох. Этот язык развивается уже 20 лет, над его исходным кодом работало множество очень талантливых программистов. За этот период он много раз перерабатывался, оптимизировался и улучшался. Доказательством этого служит тот факт, что вы сегодня используете PHP и он демонстрирует хорошую общую производительность в самых разных проектах.

А что насчёт isset()?


Это не функция, круглые скобки не обязательно означают «вызов функции». isset() включён в специальный opcode виртуальной машины Zend (ISSET_ISEMPTY), который не инициализирует вызов функции и не подвергается связанным с этим задержкам. Поскольку isset() может использовать параметры нескольких типов, его код в виртуальной машине Zend получается довольно длинным. Но если оставить только часть, относящуюся к параметру offset, то получится примерно так:

ZEND_VM_HELPER_EX(zend_isset_isempty_dim_prop_obj_handler, VAR|UNUSED|CV, CONST|TMP|VAR|CV, int prop_dim)
{
    USE_OPLINE zend_free_op free_op1, free_op2; zval *container; zval **value = NULL; int result = 0; ulong hval; zval *offset;

    SAVE_OPLINE();
    container = GET_OP1_OBJ_ZVAL_PTR(BP_VAR_IS);
    offset = GET_OP2_ZVAL_PTR(BP_VAR_R);

    /* ... code pruned ... */
    } else if (Z_TYPE_P(container) == IS_STRING && !prop_dim) { /* string offsets */
        zval tmp;
        /* ... code pruned ... */
        if (Z_TYPE_P(offset) == IS_LONG) { /* we passed an integer as offset */
            if (opline->extended_value & ZEND_ISSET) {
                if (offset->value.lval >= 0 && offset->value.lval < Z_STRLEN_P(container)) {
                    result = 1;
                }
            } else /* if (opline->extended_value & ZEND_ISEMPTY) */ {
                if (offset->value.lval >= 0 && offset->value.lval < Z_STRLEN_P(container) && Z_STRVAL_P(container)[offset->value.lval] != '0') {
                    result = 1;
                }
            }
        }
        FREE_OP2();
    } else {
        FREE_OP2();
    }

    Z_TYPE(EX_T(opline->result.var).tmp_var) = IS_BOOL;
    if (opline->extended_value & ZEND_ISSET) {
        Z_LVAL(EX_T(opline->result.var).tmp_var) = result;
    } else {
        Z_LVAL(EX_T(opline->result.var).tmp_var) = !result;
    }

    FREE_OP1_VAR_PTR();

    CHECK_EXCEPTION();
    ZEND_VM_NEXT_OPCODE();
}

Если убрать многочисленные точки принятия решения (конструкции if), то «основной» вычислительный алгоритм можно выразить строкой:

if (offset->value.lval >= 0 && offset->value.lval < Z_STRLEN_P(container))

Если offset больше нуля (вы не имели в виду isset($a[-42])) и строго меньше длины строки, результат будет принят равным 1. Тогда итогом операции будет булево TRUE. Не волнуйтесь насчёт вычисления длины, Z_STRLEN_P(container) ничего не вычисляет. Помните, что PHP уже известна длина вашей строки. Z_STRLEN_P(container) просто считывает это значение в память, на что тратится крайне мало ресурсов процессора.

Теперь вы понимаете, почему с точки зрения использования смещения строки обработка вызова функции strlen() требует ГОРАЗДО больше вычислительных ресурсов, чем обработка isset(). Последний существенно «легче». Пусть вас не пугает большое количество условных операторов if, это не самая тяжёлая часть С-кода. К тому же их можно оптимизировать с помощью С-компилятора. Код обработчика isset() не ищет в хэш-таблицах, не производит сложных проверок, не присваивает указателя одному из стековых кадров, чтобы позднее достать его. Код гораздо легче, чем общий код вызова функции, и намного реже обращается к памяти (это самый важный момент). И если закольцевать многократное выполнение такой строки, можно добиться большого улучшения производительности. Конечно, результаты одной итерации strlen() и isset() будут мало отличаться — примерно на 5 мс. Но если провести 50 000 итераций…

Также обратите внимание, что у isset() и empty() практически одинаковый исходный код. В случае со смещением строки empty() будет отличаться от isset() только дополнительным чтением, если первый символ строки не является 0. Поскольку коды empty() и isset() почти не отличаются друг от друга, то empty() будет демонстрировать такую же производительность, что и isset() (учитывая, что оба используются с одинаковыми параметрами).

Чем нам может быть полезен OPCache


Если кратко — ничем.

OPCache оптимизирует код. Об этом можно почитать в презентации. Часто спрашивают, можно ли добавить оптимизационный проход, при котором strlen() переключается в isset(). Нет, это невозможно.

Оптимизационные проходы OPCache осуществляются в OPArray до того, как он помещается в совместно используемую память. Это происходит во время компиляции, а не во время выполнения. Откуда нам знать во время компиляции, что переменная, которая передаётся в strlen(), является строкой? Это известная проблема PHP, и она отчасти решается с помощью HHVM/Hack. Если бы мы записали в PHP наши переменные со строгой типизацией, то во время проходов компилятора можно было бы оптимизировать гораздо больше вещей (как и в виртуальной машине). Так как PHP является динамическим языком, во время компиляции не известно почти ничего. OPCache может оптимизировать только статические вещи, известные к моменту начала компиляции. Например, вот это:

if (strlen("foo") > 8) {
/* do domething */
} else {
/* do something else */
}

Во время компиляции известно, что длина строки «foo» не больше 8, поэтому можно выкинуть все opcode if(), а от конструкции if оставить только часть с else.

if (strlen($a) > 8) {
/* do domething */
} else {
/* do something else */
}

Но что такое $a? Оно вообще существует? Это строка? К моменту прохода оптимизатора мы ещё не можем ответить на все эти вопросы — это задача для исполняющего модуля виртуальной машины. Во время компиляции мы обрабатываем абстрактную структуру, а тип и нужный объём памяти будут известны во время выполнения.

OPCache оптимизирует многие вещи, но из-за самой природы PHP он не может оптимизировать всё подряд. По крайне мере не столько, сколько в компиляторе Java или С. Увы, PHP никогда не будет языком со строгой типизацией. Также периодически высказываются предложения по введению в декларирование свойств класса указания read-only:

class Foo {
    public read-only $a = "foo";
}

Если не касаться функциональности, подобные предложения имеют смысл с точки зрения оптимизации производительности. Когда мы компилируем подобный класс, нам известно значение $a. Мы знаем, что оно не изменится, поэтому его можно где-нибудь хранить, использовать кэшированный указатель и оптимизировать каждую итерацию обращения к подобной переменной при проходе компилятора или OPCache. Здесь основная мысль в том, что чем больше информации о типе и использовании ваших переменных или функций вы можете дать компилятору, тем больше OPCache сможет оптимизировать, тем ближе будет результат к тому, что требуется процессору.

Подсказки по оптимизации и заключительное слово


Первым делом рекомендую не менять бездумно свой код в тех местах, где он вам кажется по-настоящему хорошим. Профилируйте и смотрите результаты. Благодаря профайлерам вроде Blackfire вы сможете сразу увидеть процесс исполнения своего скрипта, поскольку автоматически отбрасывается вся нерелевантная информация, мешающая воспринимать суть. Вы поймёте, с чего начинать работу. Ведь ваш труд стоит денег, и его тоже необходимо оптимизировать. Это хороший баланс между средствами, которые вы потратите на оптимизацию скрипта, и суммой, которую вы сэкономите, снизив расходы на содержание облака.

Вторая подсказка: PHP работает действительно быстро, эффективно и надёжно. Возможностей оптимизации PHP-скриптов не так много — например, их меньше, чем в более низкоуровневых языках вроде С. Поэтому усилия по оптимизации нужно направлять на циклы без конкретных условий на выход из них. Если профайлер покажет узкое место скрипта, вероятнее всего, оно будет внутри цикла. Именно здесь крохотные задержки накапливаются в полновесные секунды, поскольку количество итераций в циклах измеряется десятками тысяч. В PHP такие циклы одинаковы, за исключением foreach(), и ведут к одному и тому же opcode. Менять в них while на for бессмысленно, и профайлер вам это докажет.

Что касается вызовов функций, есть маленькие хитрости, с помощью которых можно предотвратить исполнение некоторых вызовов, потому что информация доступна где-то ещё.

phpversion() => use the PHP_VERSION constant
php_uname() => use the PHP_OS constant
php_sapi_name() => use the PHP_SAPI constant
time() => read $_SERVER['REQUEST_TIME']
session_id() => use the SID constant

Приведённые примеры не всегда полностью эквивалентны, за подробностями обратитесь к документации.

Да, и старайтесь избегать таких глупостей, как:

function foo() {
    bar();
}

Или того хуже:

function foo() {
    call_user_func_array('bar', func_get_args());
}

Всегда критически подходите к оптимизации, не делайте её только потому, что вы где-то что-то услышали или вам это кто-то посоветовал. Не подчиняйте структуру своего приложения достижению максимальной производительности, лучше работайте над этим в рамках отдельных возможностей вашего детища.

Чаще профилируйте свой скрипт и проверяйте каждое предположение, не следуйте слепо чужим инструкциям. Всегда всё проверяйте.

Разработчики профайлера Blackfire заложили в свой продукт механизм сбора интересных метрик, способных помочь пользователям в их работе. Регистрируется множество параметров (хотя GUI показывает ещё не все): например, когда запускается сборщик мусора, что он делает, сколько объектов создано/уничтожено в функциях, сколько несоответствий ссылок было создано во время вызовов функций, время сериализации, неправильное поведение foreach() и т.д. и т.п.

Также не забывайте, что в один прекрасный момент вы столкнётесь с ограничениями языка. Возможно, тогда будет целесообразно выбрать какой-то другой. PHP не годится для создания ORM, видеоигр, HTTP-серверов и многих других задач. Его можно для этого использовать, но это будет неэффективно. Так что для каждой задачи лучше выбирать наиболее подходящий язык, благо сегодня есть из чего выбрать: Java, Go или, наверное, самый эффективный язык нашего времени — C/C++ (Java и Go написаны именно на нем).
Автор: @AloneCoder Julien Pauli
Mail.Ru Group
рейтинг 587,29
Строим Интернет
Похожие публикации

Комментарии (70)

  • –14
    Однако такой вариант примерно на 20% медленнее этого:

    if (isset($name[49]))
    Лолшто?
    А если $name в utf?
    • +13
      Так и функция strlen количество байт возвращает.
      • –14
        Так и функция strlen количество байт возвращает.
        php> $a = 'асдфгчйкл';
        
        php> echo strlen($a)
        18
        php> var_dump(isset($a[10]));
        bool(true)
        
        php> var_dump(isset($a[15]));
        bool(true)
        
        php> var_dump(isset($a[20]));
        bool(false)
        
        php> echo mb_strlen($a);
        9
        


        Ничего странного не находите?
        • +11
          нет
          • –8
            То что предложенный метод проверки длины строки не работает, вас не смущает.
            Окей.
            • +5
              Я думаю автор привел это в качестве примера для последующего разбора, а не как лучший способ проверки длины строки
            • +3
              strlen возвращает кол-во байт в строке, если через mbstring.overload вы его не сломали. В статье приведены конкретные примеры кода до и после.
              Я не очень понимаю в чем проблема?
              • –8
                если под длиной строки подразумевать кол-во байт, то проблем нет.
                • +8
                  strlen() returns the number of bytes rather than the number of characters in a string.

                  Само описание функции это предполагает.
            • +2
              Он работает в том контексте, в котором привел его автор. В контексте «длиннее ли строка определенной длины».
              if (strlen($name) > 49)
              вполне себе аналог
              if(isset($name[49]))
              • –11
                вы тоже под длиной строки подразумеваете кол-во байт?
                приведенный код отлично решает задачу при использовании однобайтовой кодировки, но фейлит при других.
                • +2
                  strlen (без оверрайдинга) считает именно количество байт, не совершайте ошибку используя ее для измерения длины строки в utf
                  для многобайтовых есть mb_strlen, iconv_strlen и так далее.
                  • –6
                    я в курсе, что возвращает strlen. и именно об этом писал.
                • +9
                  Статья не про подсчет длины строки.
        • +1
          А что тут противоречит моему утверждению?
    • +1
      Тут бы наверное лучше подошел пример с isset() vs array_key_exists(), хоть они и по-разному себя ведут с null
      • –8
        Тут не в этом дело. Нельзя предложенным способом проверять длину строки или получать отдельный символ строки.
        • +2
          С чего вы взяли, что нельзя? В любом коде немало мест, где используется только ascii-часть. Особенно это касается кодовой базы различных фреймворков
      • +1
        gist.github.com/bwg/14a06d3ba51675e514f1
        PHP Version 5.5.20-1~dotdeb.1

        — Testing isset over 100,000 iterations: (total time in ms)

        isset_true 5.856990814209
        isset_false 4.7221183776855
        array_key_true 16.274929046631
        array_key_false 13.417959213257

        То есть 10 мс на 100000 операций или 100 нс на одну, в общем в большинстве случаев штуки вроде:

        isset($var[$name]) || array_key_exists($name, $var)

        лучше всё таки не писать
        • 0
          isset($var[$name]) || array_key_exists($name, $var)

          лучше всё таки не писать

          Простите, можно уточнить почему так лучше не писать?

          Буквально на днях еще заметил приличную разницу между
          is_null($var) и $var === null

          Может есть какой-то большой список с такими примерами?
          • +1
            Вот прямо в IDE.
          • 0
            Да пишите вы код который легко читать и работает, а не оптимизированный. Оптимизировать надо архитектуру, а не вызовы array_key_exists
            • +2
              В моем случаи уже до меня все «написали», сейчас в основном просят сделать чтоб «не тормозил сайт», переписывать все никто не даст и желаний особо ни у кого нет, поэтому сижу с профайлером и смотрю где проседает скорость, что в кэш, что такими «финтами» заменяю. В целом успехи на лицо. Я не говорю что нужно по всему проекту использовать такое, но если «узкое» место и в кэш не положить, то приходится такое делать.
            • 0
              Да пишите вы код который легко читать и работает

              Это вполне сочитается с оптимизацией кода, с оговоркой что лучше это делать на этапе ревью кода.
  • +13
    Даже если вариант с isset был бы на 200 процентов быстрее, то существует 2 основных причины не переходить на него:
    • Сложнее понять что делается в этом коде
    • Основные задерки времени выполнения кода — это не использование менее произодительных конструкций, мы всё-таки не олимпиадным программированием занимаемся и пытаемся прооптимизировать рекурсию на миллион вызовов, а создание конекшенов, выполнение запросов, подключение 100-500 классов фреймворка и т.п.
    • –2
      Тут можно долго спорить, куда уходит процессорное время и память. По моему опыту в 80% случаев можно обойтись isset-ом. Если есть возможность не вызывать функции, лучше этого не делать: xdebug trace скажет только спасибо :)
      • 0
        Но возможно тут немалое значение имеют инструменты, с которыми работаешь. На меня и мой код наибольшее влияение оказали Zend1 и Symfony2, поэтому такие конструкции для меня являются нормой, а для кого-то они покажутся бесполезной или вредной оптимизацией.
      • 0
        Перечитал ваш комментарий, я так понимаю он про isset/strlen. Мой про 80% — про array_key_exists, если что :)
      • 0
        Вот у вас уже есть проект… если смотреть на него снизу, то да, он весь тормозит сплошь из-за того, что strlen вместо isset. Но если на него взглянуть сверху, станет понятно, что сайт — это не только и не столько php, а еще кучища всяких особенностей. Например, можно убрать apache, поставить nginx, включить сжатие контента, слить js (что уменьшит общую нагрузку на сервер и снизит конкурирующие запросы), и вот сайт оживает.

        А еще можно вопхнуть балансировщик и смасштабировать горизонтально… Потратить, ну наверное, день. (хотя справедлива цитата одного товарища на эту тему: «если я разжирею, то, конечно, в спортзал не пойду, а куплю себе штаны попросторнее»).

        Ну либо можно сидеть и переписывать миллион вызовов strlen на isset… Развитие сайта — это бесконечная погоня за идеалом при изменяющихся вкусах. Во всем: начиная с дизайна, заканчивая подходами к разработке. За последние 5 лет подход к организации среднестатистического проекта поменялся до неузнаваемости. Подходы к верстке — то же самое. Возможности браузеров. Возможности самого php. Вечным остается вопрос конкуренции запросов, задержки при запросах из БД (криво организованная БД) и т.д.
        • +1
          Глупые fb и vk зачем-то решили отказаться от такого классного php в пользу своих решений.
          • +3
            У них другие цели — в первую очередь строгая типизация. Без нее поддержка больших проектов на PHP стоит очень дорого.
            • 0
              Откуда вы это знаете? Hack появился много позже, чем hhvm и тем более hiphop
              • +1
                Это звенья одной цепочки: HipHop -> HHVM -> Hack

                HipHop: компилим бинарник через трансформацию исходников (экономия ресурсов, почти мгновенные результаты)

                HHVM: свой интепретатор PHP, более производительный. Но это, скорее всего, переходная платформа, т.к. она поддерживает Hack. Обратная совместимость будет развиваться силами комьюнити, ФБ будет дальше развивать Hack.

                Hack: уже интереснее, т.к. он, впринципе, может компилироваться в JVM байт-код — строгая типизация и возвращаемые типы же. Ну и есть свой интерпретатор, опробованный на продакшн серверах.

                Вообще ожидайте процесс перетаскивания фич Hack в PHP =)
                • –1
                  1. а зачем JVM байт код? у HHVM свой байт-код и свой JIT
                  2. dim_s со своим JPHP вроде как доказывает что и сейчас ничего не мешает выполнять PHP в JVM

                  Hack слишком узко применим, по сути кроме как в facebook врятли где еще стоит его использовать + даже внутри facebook я думаю задача взять и переписать все на hack не стоит
                  • 0
                    1. а зачем JVM байт код? у HHVM свой байт-код и свой JIT
                    2.… сейчас ничего не мешает выполнять PHP в JVM


                    1) HHVM — поддерживает и PHP (слабая типизация) и Hack (стогая типизация). Поэтому всё своё (JVM — строгая типизация, поэтому нет полной совместимости).
                    2) JPHP — «We don’t plan to implement the zend runtime libraries», не для больших продакшн проектов. Да и HHVM выглядит привлекательнее, плюс мощные оптимизации c++ компиляторов.

                    Hack слишком узко применим, по сути кроме как в facebook врятли где еще стоит его использовать + даже внутри facebook я думаю задача взять и переписать все на hack не стоит

                    Тут я своего мнения ещё не сформировал окончательно. Но я надеюсь, что примитивные и возращаемые типы перекочуют в PHP как можно быстрее.
                  • 0
                    JVM байт-код: это так, хотелка. Не обращайте внимания =)
                    • 0
                      Facebook отказался от использования JVM, где-то в недрах интернета была про это новость, кажется еще в 2012 году. Причин я не помню.
                      • 0
                        Не думаю — у них же на github хватает проектов на Java.

                        Плюс когда мы с их рекрутерами общались в Декабре, они чётко обозначили Java + PHP в своих требованиях.
                    • 0
                      JVM-байткод в… php… Ну да — костылить свой проект граблями из выше перечисленных хипстерских решений (реально… ну кому еще, кроме FB всерьез сдались эти мытарства) вместо того, чтобы переписать его на что-нибудь попроизводительнее — это да. Это оптимизация.
                      • 0
                        JVM-байткод в… php…


                        Ну как бы канитель с PHPNG (проект по рефакторингу ядра PHP который будет использоваться в PHP7) как раз таки и затевался что бы можно было внедрить JIT, который я надеюсь, все же добавят. JVM-байткод это конечно тоже вариант что бы не писать свою виртуальную машину… Пользуются же люди JRuby.
                        • 0
                          Я не хочу сейчас никаких холиваров, но как по мне — если уж и тратить время на что-то, так это на развитие. Лично мое мнение. Если так хочется впердолить php в именно ява-машину, ну не лучше ли заняться явой? К чему это все? У всего есть свои границы, php я обязан очень многим, но это уже сродни силиконовым сиськам каким-то…

                          А потом начинается «ООО Ромашка требуется программист, умеющий читать и писать чужой код»…

                          Нет, я понимаю еще некоторые вещи типа react — это попытка, скорее, полностью переписать ноду на php, но как по мне — момент вполне себе упущен. Нода уже заняла свою нишу и идет в своем направлении (я не говорю, что она захватит мир, просто они, как я думаю, достигают именно того, на что рассчитывали".
                      • 0
                        Проекты разные бывают, и они живут и развиваются в опредленной инфраструктуре. Большие проекты и инфраструктуру не переписать и не выбросить — это огромные деньги и риски для бизнеса.

                        Я подозреваю, что для ФБ дешевле допилить Hack, чтобы:
                        — сохранить команду;
                        — сохранить работающие решения;
                        — уменьшить стоимость разработки и сопровождения;

                        Поэтому будут самые разнообразные проекты PHP/JVM, .NET/JVM, LLVM =)
                        • 0
                          Ой, я вас умоляю… ну сколько времени и средств займет раздвоить команду и реализовать это все в догоняющем ключе параллельно? Наверняка найдутся тысячи подобных историй успеха.

                          Но вообще… я понял еще раз одну вещь — в программировании царит вкусовщина. Ну если не нравится человеку руби (как, например, мне), ну не смогу я на нем писать. Смогу на go… вчера смог чуть-чуть (на самом деле, довольно серьезно) познакомиться и перестать бояться rust… но понять, что еще слишком рано.

                          Так и, может быть, у вас (может, ошибаюсь)… ну не лежит душа к революции — значит будет оптимизация того, что есть. Наверное, в этом нет ничего плохого.
                          • 0
                            Ой, я вас умоляю… ну сколько времени и средств займет раздвоить команду и реализовать это все в догоняющем ключе параллельно?

                            Вы задали правильный вопрос, именно в него и упираются многие решения =)

                            Но вообще… я понял еще раз одну вещь — в программировании царит вкусовщина.

                            И авторитаризм, если говорить о процессе разработки софта. Вы правы.
                            • 0
                              > Вы задали правильный вопрос, именно в него и упираются многие решения =)

                              Ну мы ж про Фейсбук говорим ))) на чашечку весов кладем затраты на разработку и поддержку HHVM…
          • 0
            Вы не fb, и даже не vk )))
  • +3
    Протестировал на JPHP, вариант с isset работает на 8.5% быстрее strlen, вместо 20%. Итог прост, вызов прямых инструкций байткода будет всегда быстрее вызова метода или функции, даже нативной.
  • +1
    В PHP7 разница 7%:

    $ /opt/php-7.0a/bin/php a.php
    2.9582719802856
    2.7427408695221
    

    Выполняемый код
    <?php
    $test = str_repeat(0, 100);
    
    $t = microtime(1);
    for ($i = 0; $i<1e8; $i++) {
    	if (strlen($test) > 49) {
    		// nop
    	}
    }
    echo microtime(1) - $t, "\n";
    
    $t = microtime(1);
    for ($i = 0; $i<1e8; $i++) {
    	if (isset($test[49])) {
    		// nop
    	}
    }
    echo microtime(1) - $t, "\n";
    • +1
      2мс разницы для 1кк вызовов?! да уж…
      • +2
        Для 100 кк, если быть точным :)
        • +2
          Ах, в миллисекундах… Хитро…
    • +2
      php 5.4 — разница 150%

      # php test.php
      25.701895952225
      10.758450031281

      # php test.php
      22.53199005127
      9.3954191207886

      # php -v
      PHP 5.4.24 (cli) (built: Jan 13 2014 12:43:37)
      Copyright © 1997-2013 The PHP Group
      Zend Engine v2.4.0, Copyright © 1998-2013 Zend Technologies
      • –3
        Привет, Николай!

        У тебя же вышло 58% разницы(100 × (1 — 9,39/22,53))? Для интереса запустил на PHP 5.6.5:
        $ php a.php
        11.312389135361
        3.878565788269

        66%
    • 0
      FYI В PHP7 strlen и еще пара горячих функций были вынесены в отдельный опкод, так что это технически больше не вызов функции, а медленнее из-за `> 49`
      • 0
        Да, так и есть. Но там в планах удешевить вызов всех функций: wiki.php.net/rfc/fast_zpp
        • 0
          Это относится только к функциям которые используют зпп, то есть функции в расширениях (не в пхп коде) и ускорение там совсем небольшое будет, там больше времени съедается на других вещах. Но вообще в 7 уже довольно хорошо ускорили вызов функций.

          Кстати эта рфц должна быть уже в апстриме, так что это уже не планы :)
    • 0
      А я еще решил посмотреть насколько свои функции медленнее встроенных. Получилось вот так:
      7.34437894821
      1.37132811546
      20.6537799835
      10.7494640350

      Версия 5.2

      Код
      <?php
      $test = str_repeat(0, 100);
      
      function myisset($str)
      {
      	return (isset($test[49]));
      }
      
      function mystrlen($str)
      {
      	return (strlen($test));
      }
      
      $t = microtime(1);
      for ($i = 0; $i<1e6; $i++) {
          if (strlen($test) > 49) {
              // nop
          }
      }
      echo microtime(1) - $t, "\n";
      
      $t = microtime(1);
      for ($i = 0; $i<1e6; $i++) {
          if (isset($test[49])) {
              // nop
          }
      }
      echo microtime(1) - $t, "\n";
      
      $t = microtime(1);
      for ($i = 0; $i<1e6; $i++) {
          if (mystrlen($test)) {
              // nop
          }
      }
      echo microtime(1) - $t, "\n";
      
      $t = microtime(1);
      for ($i = 0; $i<1e6; $i++) {
          if (myisset($test)) {
              // nop
          }
      }
      echo microtime(1) - $t, "\n";
      

  • +2
    Я выскажу лично свое мнение. Я согласен с автором, что бездумное изменение кода — это вред. Все понятно. Но ровно с того момента, когда вам придет в голову сидеть и бесконечно профилировать свои скрипты, стоит задуматься — а не выбрать ли другую платформу. Как по мне такое профилирование — трата времени ради экономии на спичках…

    Хотя… я когда-то прочитал об особенностях двойных кавычек, о сравнении конкатенации со sprintf, о разнице в скорости sizeof и count (может, и мифической), и это где-то в подсознании отложилось…
    • 0
      del, извините
    • +1
      По сути да. Преимущество PHP — скорость разработки. Если приходится постоянно сидеть в профилировщике и экономить на вызовах функций, то это помножает на ноль основное достоинство PHP. В таких условиях сменой платформы можно ускорить и работу приложения, и разработку. Другой вопрос, что возможности переписать всё с нуля обычно нет. :)
      • +1
        Ситуация, как мне кажется, меняется. Все больше компаний обновляет своих сотрудников, все больше сотрудников понимают, что новые технологии придумываются не всегда ради прикола… которые готовы на авантюры. Нет, я думаю, все не так плохо. Я думаю, мы еще на своем веку напишем идеальную штуку. И не раз.
  • 0
    Не понял насчет:
    time() => read $_SERVER['REQUEST_TIME']

    Это ж вроде разные вещи.
    • +1
      Для подавляющего большинства применений этого достаточно. Стоит продолжить список, кстати:

      is_null(x) → x === null
      microtime(true) → $_SERVER["REQUEST_TIME_FLOAT"] (PHP 5.4+)
      array_key_exists(…) → isset (с некоторым оговорками, в случае null функция вернёт true, isset — false, если это важно, замена невозможна)
      dirname(__FILE__) → __DIR__ (PHP 5.3+)
      intval(x) → (int) x (плюс boolval, strval и т.д.)
      
    • 0
      echo time().PHP_EOL;
      echo $_SERVER['REQUEST_TIME'];
      

      1424412119
      1424412119

      Применительно к указанному вызову time() это одинаковые вещи.
      • 0
        несовсем:

        echo $_SERVER['REQUEST_TIME']. PHP_EOL;
        sleep(1);
        echo time(). PHP_EOL;

        даже без sleep этот код может выдавать разные значения
        • 0
          С какой целью вам может потребоваться время ПОСЛЕ выполнения скрипта, длящегося больше секунды?
          Обычный кейс для time() — это записать это время в БД, когда произошло что-то типа вставки комментария или чего-то еще.
          Логично это время взять как раз таки из времени запроса к серверу.
          • 0
            например если этот скрипт запущен постоянно и через какойнить сервер очередей получает задания для выполнения.
            и секунда это достаточно долго, скрипт может быть запущен в 1424412119.95 и уже через 50мс у вас будет разное время
            • –1
              Мы тут об очевидных кейсах использования PHP же говорим, да? В 99% случаев это web-сервер, выполняющий запросы пользователей, в котором php «умирает». И в таких применениях скрипт работающий больше секунды, да еще использующий нестабильное время своего выполнения для чего-то еще — вообще существовать не должен
  • +2
    Статья о том, как смазывать лыжи, чтобы по асфальту они ехали на 20% быстрее?

    По-моему, если лишняя микросекунда, потраченная на вызов strlen, существенна в конкретной задаче — то эту задачу не стоило решать на PHP.
    • 0
      Если эта задача (не для РНР) всего одна, опять взникает вопрос о целесообразности иного решения

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка