Інкремент в PHP



Візьміть змінну і збільшення її на 1. Звучить просто, вірно? Ну… З точки зору PHP розробника, напевно, так. Але чи так це насправді? Тут можуть виникнути деякі труднощі. Існує кілька способів инкрементировать значення, вони можуть виглядати рівноцінними, але під капотом PHP працюють по-різному, що може призвести до, так би мовити, цікавих результатів.

Розглянемо три приклади додавання одиниці до змінної:

$a = 1;
$a++; # Операція плюс инкрементирования
var_dump($a);

$b = 1;
$b += 1; # Операція додавання присвоювання
var_dump($b);

$c = 1;
$c = $c + 1; # Операція додавання стандартного
var_dump($c);

Код різний, але в кожному випадку значення змінної збільшується. А який буде результат?

int(2)
int(2)
int(2)

Інтуїтивно всі три способи виглядають рівнозначно. Тобто для инкрементирования можна використовувати як
$a++
та
$a += 1
. Але давайте розглянемо інший приклад:

$a = "foo";
$a++;
var_dump($a);

$a = "foo";
$a += 1;
var_dump($a);

$a = "foo";
$a = $a + 1;
var_dump($a);

string(3) "fop"
int(1)
int(1)

Напевно багато хто з вас не очікували такого результату! Може бути, хтось вже знав, що додавання до строкової змінної призводить до зміни набору символів, але два
int(1)
? Звідки вони взялися? З точки зору PHP-програміста це виглядає дуже неузгоджено, і виходить, що наші три способи инкрементирования нерівнозначні. Давайте подивимося, що відбувається в надрах PHP виконання коду.

Байт-код
Під час запуску PHP-скрипта ваш код спочатку складений проміжний формат — байт-код. Цей факт спростовує думку, що PHP по-справжньому інтерпретується в microsoft мова, — інтерпретується байт-код, а не вихідний код PHP.

Наведений вище код перетворюється в такій байт-код:

compiled vars: !0 = $a, !1 = $b, !2 = $c
line #* E I O op fetch ext return operands
---------------------------------------------------------------------------
3 0 E > ASSIGN !0, 1
4 1 POST_INC ~1 !0
2 FREE ~1
5 3 SEND_VAR !0
4 DO_FCALL 1 'var_dump'

7 5 ASSIGN !1, 1
8 6 ASSIGN_ADD 0 !1, 1
9 7 SEND_VAR !1
8 DO_FCALL 1 'var_dump'

11 9 ASSIGN !2, 1
12 10 ADD ~7 !2, 1
11 ASSIGN !2, ~7
13 12 SEND_VAR !2
13 DO_FCALL 1 'var_dump'

14 > RETURN 1

Ви легко можете створити такі опкоды самостійно, скориставшись дебагером VLD або онлайн-сервісом 3v4l.org. Не думайте про те, що це все означає. Якщо позбутися від нецікавих речей, то залишаться тільки ці рядки:

compiled vars: !0 = $a, !1 = $b, !2 = $c
line #* E I O op fetch ext return operands
---------------------------------------------------------------------------
4 1 POST_INC ~1 !0
2 FREE ~1

8 6 ASSIGN_ADD 0 !1, 1

12 10 ADD ~7 !2, 1
11 ASSIGN !2, ~7

Таким чином,
$a++
перетворюється в два опкода (
POST_INC і FREE
),
$a += 1
— (
ASSIGN_ADD
)
$a = $a + 1
теж два. Зверніть увагу, що у всіх трьох випадках вийшли різні опкоды, що вже має на увазі різне виконання PHP.

Оператор плюс инкрементирования
Розглянемо перший спосіб инкрементирования — унарний оператор
$a++
). Цей PHP-код перетворюється в опкод
POST_INC
. До слова,
PRE_INC
виходить з
++$a
, і вам потрібно знати різницю між ними. Другий опкод —
FREE
— очищає результат після
POST_INC
, тому що ми не використовуємо його значення, що повертається:
POST_INC
на місці змінює актуальний операнд. В даному випадку можна проігнорувати цей опкод.

Причина відмінності у виконанні цих опкодов криється у файлі
zend_vm_def.h
, який ви можете знайти у вихідному З-коді PHP. Це великий заголовковий файл, наповнений макросами, тому його не так легко читати, навіть якщо ви знаєте С. При виклику опкода
POST_INC
виконується вміст рядка 971.

Якщо коротко, то відбувається ось що:

  • Перевіряється, чи належить змінна (
    $a
    в PHP-коді, яка в байт-код перетворюється в
    !0
    ) до типу
    long
    . По суті, система перевіряє, чи має змінна число. Хоча PHP — мова з динамічною типізацією, кожна змінна все ж належить до якогось «типом». Типи можуть змінюватися, як ми побачимо далі. Якщо наша мінлива відноситься до
    long
    , то викликається З-функція
    fast_increment_function()
    і відбувається повернення до наступного опкоду.
  • Якщо змінна нечисловий, то виконуються базові перевірки на можливість инкрементирования. Наприклад, цього не можна зробити з рядковими зміщеннями (string offset)
    $a = "foobar"
    ;
    $a[2]++
    , ми отримаємо помилку.
  • Далі перевіряється, чи є змінна неіснуючим властивість об'єкта, що має чарівні PHP-методи
    __get
    та
    __set
    . Якщо це так, то з допомогою
    __get
    витягується правильне значення, викликається
    fast_increment_function()
    і значення зберігається за допомогою виклику методу
    __set
    . Ці методи викликаються з С, а не з PHP.
  • Нарешті, якщо змінна не є властивістю, то просто викликається
    increment_function()
    .
Як бачите, процес додавання числа залежить від типу змінної. Якщо це число, то напевно все зведеться до викликом
fast_increment_function
, а якщо це чарівне властивість, то до викликом
increment_function()
. Нижче ми поговоримо про роботу цих функцій.

fast_increment_function()
Функція
fast_increment_function()
відноситься до zend-операторам, і її завданням є максимально швидке инкрементирование конкретної змінної.

Якщо змінна відноситься до типу
long
, то для її инкрементирования використовується дуже швидкий асемблерний код. Якщо значення досягло максимального числа типу INT (
LONG_MAX
), то змінна автоматично перетвориться у подвійну (
double
). Це найшвидший спосіб збільшення числа, оскільки ця частина коду написана на асемблері. Вважається, що компілятор не може оптимізувати З-код краще, ніж асемблер. Але спосіб працює тільки в тому випадку, якщо змінна відноситься до типу
long
. Інакше буде виконаний редирект на функцію
increment_function()
. Оскільки инкрементирование (і декрементирование) найчастіше виконується в дуже маленьких внутрішніх циклах (наприклад,
for
), то необхідно робити це як можна швидше заради збереження високої продуктивності PHP.

increment_function()
Якщо
fast_increment_function()
— швидкий спосіб инкрементировать число, то
increment_function
— повільний (
slow
) спосіб. Сценарій процесу теж залежить від типу змінної.

  • Якщо змінна відноситься до типу
    long
    , то число просто збільшується (і перетворюється в
    double
    при досягненні максимального значення, яке вже не можна зберігати в
    long
    ). Найчастіше це вже буде зроблено за допомогою
    fast_increment_function
    , але може статися так, що цієї функції все одно буде передано
    long
    , так що і тут необхідна перевірка.
  • Якщо змінна відноситься до типу
    double
    , то вона просто збільшується.
  • Якщо змінна відноситься до типу
    NULL
    , то завжди повертається
    long 1
    .
  • Якщо змінна відноситься до типу
    string
    , то застосовується описана вище магія.
  • Якщо змінна — об'єкт і має функціональність оператора
    internal
    , то викликається оператор
    add
    для додавання
    long 1
    . Зверніть увагу, що це працює тільки для класів
    internal
    , які вручну визначають ці функції оператора, ви не можете визначати оператори об'єкта в PHP-коді простору користувача. Це реалізує єдиний клас у вихідному PHP-коді —
    GMP
    . Так що ви можете зробити
    $a = new gmp(1) + new gmp(3); // gmp(4)
    . Така можливість з'явилася починаючи з PHP 5.6, але перевантаження оператора неможлива в PHP безпосередньо.
  • Якщо змінна відноситься до якогось іншого типу, то її не можна инкрементировать і повертається код збою.
Отже, система перевіряє різні типи. Зауважте: тут немає перевірки, скажімо, на логічне значення, це говорить про те, що такий тип не можна инкрементировать.
$a = false; $a++
не тільки не буде працювати, але навіть помилки не поверне. Змінна просто не зміниться, а залишиться
false
.

Инкрементирование рядків
А тепер найцікавіше. Робота з рядками завжди сповнена нюансів, але в даному випадку відбувається ось що.

По-перше, перевіряється, чи містить рядок число. Наприклад, символьна 123 містить число 123. Таке «рядкове число» буде перетворено в нормальне число типу
long int(123)
). При конвертуванні використовується кілька прийомів:

  • Видаляються пробіли.
  • Підтримуються шістнадцяткові числа (
    0x123
    ).
  • Не підтримуються вісімкові і двійкові числа (
    0123
    та
    b11
    ).
  • Підтримується наукове уявлення (
    1E5
    ).
  • Підтримуються
    double
    .
  • Не підтримуються і не вважаються числами частині, що стоять на початку або в кінці рядковій (
    135abc
    або
    ab123
    ).
Якщо в результаті вийшло
long
або
double
, то число просто збільшується. Наприклад, якщо ми візьмемо рядковий 123 і інкрементуємо, то отримаємо
int(124)
. Зверніть увагу, що тип змінної змінюється зі строкової на цілочисельну!

Якщо символьна не може бути перетворена в
long
або
double
, то викликається функція
increment_string()
.

increment_string()
PHP використовує систему инкрементирования зразок Perl. Якщо символьна порожня, то просто повертається
string("1")
. В іншому випадку для инкрементирования строкової застосовується система переносу (carry system).

Починаємо з кінця змінної. Якщо символ
a
z
, то він инкрементируется (
a
стає
b
, і т. д.). Якщо символ
z
, то змінюється на
a
та «переноситься» на одну позицію перед.

Тобто:
a
стає
b
,
ab
стає
c
(перенесення не потрібен),
az
стає
ba
(
z
стає
a
,
a
стає
b
, тому що ми переносимо один символ).

Те ж саме відноситься і до прописних символів
A
Z
, а також до цифр від
0
9
. При инкрементировании
9
перетворюється в
0
і переноситься на попередню позицію.

Якщо ми досягли початку рядковій змінній і потрібно зробити перенесення, то просто додається ще один символ ПЕРЕД усієї рядковій. Тип той же, що і у стерпного символу:

"z" => "aa"
"9" => "00"
"Zz" => "AAa"
"9z" => "10a"

Так що при инкрементировании рядка неможливо змінити тип кожного символу. Якщо він був у нижньому регістрі, то в ньому і залишиться.

Але будьте обережні, якщо станете инкрементировать «число в рядку» кілька разів.

При инкрементировании
string("2D9")
вийде
string("2E0")
(
string("2D9"
) не є числом, тому буде виконуватися инкрементирование звичайної рядка). Але при инкрементировании
string("2E0")
ви вже отримаєте
double(3)
, тому що
2E0
— наукове уявлення
2
і вона буде перетворена в
double
, який потім може бути інкрементірован до 3. Так що будьте уважні з циклами инкрементирования!

Ця система инкрементирования рядків також пояснює, чому ми можемо инкрементировать «Z» до «AA», але не можемо декрементировать «AA» до «Z». Декрементіруется тільки останній символ «A», але що робити з першим? Його теж треба декрементировать до «Z» з допомогою (негативного) перенесення? А щодо «0A»? Воно повинно стати
Z
? І якщо так, то при новому инкрементировании ми отримаємо вже
AA
. Іншими словами, ми не можемо просто прибрати символи під час декрементирования, як ми додамо їх при инкрементировании.

Підсумовуючий оператор присвоювання
Розглянемо тепер другий приклад з початку статті — підсумовуючий оператор присвоювання (
$a += 1
). Виглядає аналогічно унарному оператору инкремента, але веде себе інакше з точки зору генеруються опкодов і фактичного виконання. Вираз повністю обробляється за допомогою zend_binary_assign_op_helper, який після низки перевірок викликає add_function з двома операндами:
$a
і нашим значенням
int(1)
.

add_function()
Метод
add_function
працює по-різному в залежності від типів змінних. Здебільшого він складається з перевірки типів операндів:

  • Якщо вони обидва належать до
    long
    , то їх значення просто збільшуються (при переповненні перетворюються в
    double
    ).
  • Якщо
    long
    , а другий
    double
    , то обидва перетворюються в
    double
    і инкрементируются.
  • Якщо вони обидва належать до
    double
    , то просто підсумовуються.
  • Якщо вони обидва є масивами, то будуть об'єднані на основі ключів:
    $a = [ 'a', 'b' ] + [ 'c', 'd' ];
    . Вийде
    [ 'a', 'b']
    , як якщо б об'єднали другий масив, але у них виявилися однакові ключі. Зверніть увагу, що об'єднання відбувається не за значенням, а за ключем.
  • Якщо операнди є об'єктами, то перевіряється, чи має перший з них внутрішню функціональність оператора (як у випадку з методом
    increment_function()
    ). У вас не вийде зробити так в PHP самостійно, це підтримується тільки внутрішніми класами начебто GMP.
Якщо операнди відносяться до якихось інших типів (наприклад,
string + long
), то за допомогою методу
zendi_convert_scalar_to_number
вони обидва будуть перетворені у скаляри. Після перетворення знову буде застосована функція
add_function
, і в цей раз напевно буде виявлено відповідність одній з описаних пар.

zendi_convert_scalar_to_number()
Перетворення скаляр в число залежить від типу скаляр. Зазвичай все зводиться до одного з таких алгоритмів:

  • Якщо скаляр — рядок, то за допомогою
    is_numeric_string
    перевіряється, чи містить вона число. Якщо немає, то повертається
    int(0)
    .
  • Якщо скаляр —
    null
    або логічне
    false
    , то повертається
    int(0)
    .
  • Якщо скаляр — логічне
    true
    , то повертається
    int(1)
    .
  • Якщо скаляр — ресурс (resource), то повертається цифрове значення номера ресурсу (resource number).
  • Якщо скаляр — об'єкт, то робиться спроба перетворити його в
    long
    (як і у випадку з внутрішніми операторами, тут може бути функціональність внутрішнього перетворення (internal cast functionality), але вона не завжди реалізована і доступна тільки для основних класів, а не для PHP-класів у просторі користувача).
Оператор суми
Це найпростіший з усіх трьох варіантів. При його виконанні викликається функція
fast_add_function()
. Як і
fast_increment_function()
, вона безпосередньо використовує асемблерний код для збільшення чисел, якщо обидва операнди відносяться до
long
або
double
. Якщо це не так, то здійснюється редирект на функцію
add_function()
, використовувану виразом присвоювання.

Оскільки і оператор додавання, що підсумовує оператор присвоювання використовують одну і ті ж базову функціональність, то
$a = $a + 1 і $a += 1
працюють однаково. Єдина відмінність полягає в тому, що оператор додавання МОЖЕ виконуватися швидко, якщо обидва операнди відносяться до
long
або
double
. Так що якщо ви хочете зробити микрооптимизацию, то
$a = $a + 1
буде працювати швидше, ніж
$a += 1
. Не тільки завдяки
fast_add_function()
, але і тому, що нам не потрібно обробляти додатковий байт-код для збереження результатів назад в
$a
.

Висновок
Инкрементирование значення відрізняється від простого додавання:
add_function
перетворює типи сумісні пари, а
increment_function
цього не робить. Тепер ми можемо пояснити отримані результати:

$a = false;
$a++;
var_dump($a); // bool(false)

$a = false;
$a += 1;
var_dump($a); // int(1)

$a = false;
$a = $a + 1;
var_dump($a); // int(1)

Оскільки
increment_function
не перетворює логічне значення (це не число і не рядок, яку можна перетворити в число), то відбувається тихий збій і значення не инкрементируется. Тому залишилося
bool(false)
. У випадку з
add_function
робиться спроба знайти відповідність пари
boolean
та
long
, яке не існує. В результаті обидва значення перетворюються в
long
:
bool(false)
стає
int(0)
, а
int(1)
залишається
int(1)
. Тепер у нас є пара
long
&
long
,
add_function
просто підсумовує їх і виходить
int(1)
. (Питання: у що перетвориться логічне
true
+
int(1)
?)

Також ми можемо пояснити ще одну особливість:

$a = "foo";
$a++;
var_dump($a); // string("fop")

$a = "foo";
$a += 1;
var_dump($a); // int(1)

$a = "foo";
$a = $a + 1;
var_dump($a); // int(1)

Оскільки рядок не виходить перетворити на число, то виконується звичайне инкрементирование рядка. Вираз додавання перетворює рядка
long
після перевірки на наявність чисел. Оскільки їх немає, то виконується конвертування рядка
int(0)
і до неї додається
int(1)
.
Джерело: Хабрахабр

0 коментарів

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