Розбір викликів функцій в 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 check that won't. 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 as an integer 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 безглуздо, і профайлер вам це доведе.

Що стосується викликів функцій, є маленькі хитрощі, з допомогою яких можна запобігти виконання деяких викликів, тому що інформація доступна десь ще.

php_version() => use the constant PHP_VERSION
php_uname() => use the constant PHP_OS
php_sapi_name() => use the constant PHP_SAPI
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 написані саме на ньому).

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.