Pull to refresh
VK
Building the Internet

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

Reading time 15 min
Views 40K
Original author: Julien Pauli
Этот пост посвящён оптимизации 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 написаны именно на нем).
Tags:
Hubs:
+50
Comments 70
Comments Comments 70

Articles

Information

Website
vk.com
Registered
Founded
Employees
5,001–10,000 employees
Location
Россия
Representative
Миша Берггрен