d_x
01.09.2009, 15:01
В этой небольшой статейке расскажу о некоторых неприятных ошибках (багофичах) PHP, которых нужно иногда остерегаться.
Случайно заметил в PHP такую вещь - он весьма посредственно обходится со знаками чисел, а также путает int32 и int64, неправильно их определяя.
Пример:
$a=4294967294; //0b1111111....11110
print $a.'<br>'; //тут мы видим 4294967294 - int32 без знака
$a &= pow(2,32)-1; //0b11111111....1111 - казалось бы - ничего не изменилось, число в бинарном виде хранится то же самое, но:
print $a; //тут мы видим -2. Почему PHP стал вдруг считать это число как int32 со знаком?
Но это еще не самое удивительное. Можно взять такой пример:
$a=4294967298; //это уже 33-х разрядное число. Видимо, PHP выделяет под него int64. Проверим:
print $a.'<br>'; //Да, выводится 4294967298. Если бы было отведено только 32 разряда, то вывелось бы 2 (на рисунке показано)
/*
1 |00000000000000000000000000000010
^ |----- 32 разряда ------
^
1 доп. разряд, 33й.
*/
//а теперь сделаем так - используем побитовое И
$a &= pow(2,33)-1; //0b11111..111 - 33 единицы. По сути, опять, все разряды сохраняются... Мы сделали вот что:
/*
1 |00000000000000000000000000000010 <- $a
& &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& <- &
1 |11111111111111111111111111111111 <- 2^32 - 1
= ================================
1 |00000000000000000000000000000010 - число такое же, как и в изначальном варианте
*/
print $a; //PHP выводит число 2! Почему? Он внезапно просто отбросил 33й и все последующие разряды и сделал число снова int32, т.е. оставил 10b?
Еще несколько примеров:
$a=0; //понятно, 0.
$a |= pow(2,32)-1; //32 единицы
print $a; //-1. На каком основании PHP посчитал число $a как signed?
$a=18446744073709551610; //это уж точно int64, занимает 64 разряда
$b=18446744073709551610; //абсолютно такое же число
print $a.'<br>'; //нормально, выводится
$a &= $b; //умножаем друг на друга два int64 побитово!
print $a; //0?? Что PHP на этот раз сделал с числом? Куда делись вообще все разряды? Должно было получиться такое же число, как $a и $b в результате!
$a=(1<<31); //это 0b10000.....000, или 2147483648
print $a.'<br>'; //а тут оно стало числом со знаком... -2147483648.
printf('%u<br>',$a); //забавно, если принудительно сказать о том, что число беззнаковое.
$a=(1<<63); //это число должно быть ровно в 2^32 раза больше, чем предыдущее.
print $a; //ах ты ж... опять то же самое... В операции << явно не воспринимаются числа больше тридцати двух разрядов, и PHP просто циклически сдвинул число дважды по кругу...
Поэкспериментировав, я сделал вывод, что PHP:
1. Способен спутать знак числа при побитовых операциях.
2. В операциях сдвига и побитовых операциях всегда принимаются во внимание только 32 разряда числа, даже если это int64. Остальные разряды просто отбрасываются.
К этим операторам относятся все вроде &, |, ^, ~, <<, >>.
Вот кстати еще пример:
$a=4294967298;
$a=~$a; //побитово отрицаем все разряды числа
$a=~$a; //снова делаем то же самое. По идее, должны получить исходный результат?
print $a; //а вот хрен, int32 на выходе
3. Ошибка происходит даже в арифметической операции % (взятие остатка от деления)!
Пример:
$a=4294967298; //снова int64
$a=$a % 10; //должны получить 8 - это остаток от деления $a на 10
print $a; //получили 2.. снова та же история, потеряны старшие разряды, мда
4. Советую также не сравнивать большие числа, если требуется большая точность. Скажем, PHP посчитает числа 18446744073709551610 и 18446744073709551619 равными, так как у него в памяти они представлены в виде 1.84467440737E+19 оба.
Не ошибались в размерностях только операторы +, -, *, /, ++, --.
Не производите сложных вычислений на PHP, особенно с большими числами, это может закончиться плачевно.
PS. Если что по операциям непонятно, или если вас бинарный вид числа пугает - спрашивайте, поясню.
Perl кстати ошибался меньше, но в побитовых операциях, вроде &, всё равно присутствовали ошибки. Будьте с ним тоже осторожнее.
Случайно заметил в PHP такую вещь - он весьма посредственно обходится со знаками чисел, а также путает int32 и int64, неправильно их определяя.
Пример:
$a=4294967294; //0b1111111....11110
print $a.'<br>'; //тут мы видим 4294967294 - int32 без знака
$a &= pow(2,32)-1; //0b11111111....1111 - казалось бы - ничего не изменилось, число в бинарном виде хранится то же самое, но:
print $a; //тут мы видим -2. Почему PHP стал вдруг считать это число как int32 со знаком?
Но это еще не самое удивительное. Можно взять такой пример:
$a=4294967298; //это уже 33-х разрядное число. Видимо, PHP выделяет под него int64. Проверим:
print $a.'<br>'; //Да, выводится 4294967298. Если бы было отведено только 32 разряда, то вывелось бы 2 (на рисунке показано)
/*
1 |00000000000000000000000000000010
^ |----- 32 разряда ------
^
1 доп. разряд, 33й.
*/
//а теперь сделаем так - используем побитовое И
$a &= pow(2,33)-1; //0b11111..111 - 33 единицы. По сути, опять, все разряды сохраняются... Мы сделали вот что:
/*
1 |00000000000000000000000000000010 <- $a
& &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& <- &
1 |11111111111111111111111111111111 <- 2^32 - 1
= ================================
1 |00000000000000000000000000000010 - число такое же, как и в изначальном варианте
*/
print $a; //PHP выводит число 2! Почему? Он внезапно просто отбросил 33й и все последующие разряды и сделал число снова int32, т.е. оставил 10b?
Еще несколько примеров:
$a=0; //понятно, 0.
$a |= pow(2,32)-1; //32 единицы
print $a; //-1. На каком основании PHP посчитал число $a как signed?
$a=18446744073709551610; //это уж точно int64, занимает 64 разряда
$b=18446744073709551610; //абсолютно такое же число
print $a.'<br>'; //нормально, выводится
$a &= $b; //умножаем друг на друга два int64 побитово!
print $a; //0?? Что PHP на этот раз сделал с числом? Куда делись вообще все разряды? Должно было получиться такое же число, как $a и $b в результате!
$a=(1<<31); //это 0b10000.....000, или 2147483648
print $a.'<br>'; //а тут оно стало числом со знаком... -2147483648.
printf('%u<br>',$a); //забавно, если принудительно сказать о том, что число беззнаковое.
$a=(1<<63); //это число должно быть ровно в 2^32 раза больше, чем предыдущее.
print $a; //ах ты ж... опять то же самое... В операции << явно не воспринимаются числа больше тридцати двух разрядов, и PHP просто циклически сдвинул число дважды по кругу...
Поэкспериментировав, я сделал вывод, что PHP:
1. Способен спутать знак числа при побитовых операциях.
2. В операциях сдвига и побитовых операциях всегда принимаются во внимание только 32 разряда числа, даже если это int64. Остальные разряды просто отбрасываются.
К этим операторам относятся все вроде &, |, ^, ~, <<, >>.
Вот кстати еще пример:
$a=4294967298;
$a=~$a; //побитово отрицаем все разряды числа
$a=~$a; //снова делаем то же самое. По идее, должны получить исходный результат?
print $a; //а вот хрен, int32 на выходе
3. Ошибка происходит даже в арифметической операции % (взятие остатка от деления)!
Пример:
$a=4294967298; //снова int64
$a=$a % 10; //должны получить 8 - это остаток от деления $a на 10
print $a; //получили 2.. снова та же история, потеряны старшие разряды, мда
4. Советую также не сравнивать большие числа, если требуется большая точность. Скажем, PHP посчитает числа 18446744073709551610 и 18446744073709551619 равными, так как у него в памяти они представлены в виде 1.84467440737E+19 оба.
Не ошибались в размерностях только операторы +, -, *, /, ++, --.
Не производите сложных вычислений на PHP, особенно с большими числами, это может закончиться плачевно.
PS. Если что по операциям непонятно, или если вас бинарный вид числа пугает - спрашивайте, поясню.
Perl кстати ошибался меньше, но в побитовых операциях, вроде &, всё равно присутствовали ошибки. Будьте с ним тоже осторожнее.