PDA

Просмотр полной версии : Ошибки PHP - знаки чисел, int32 и int64


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 кстати ошибался меньше, но в побитовых операциях, вроде &, всё равно присутствовали ошибки. Будьте с ним тоже осторожнее.

astrologer
01.09.2009, 15:28
Случайно заметил в PHP такую вещь - он весьма посредственно обходится со знаковыми и беззнаковыми числамиОсобенно если учесть то, что беззнаковых в php нет совсем :)

d_x
01.09.2009, 15:31
Особенно если учесть то, что беззнаковых в php нет совсем
Возможно, я неправильно выражаюсь, немного поправил пост. Я имел в виду, что PHP произвольно меняет знаки числа, как показано в первых примерах. Perl так не делал.

oRb
01.09.2009, 16:20
d_x, дело не в пхп.

#include <stdio.h>

int main() {
printf("%d\n", 4294967294 & 4294967295);
return 0;
}

Тут так же получишь -2. Аналогично и с остальными примерами.
ps:
http://ru.php.net/manual/en/language.operators.bitwise.php
Warning

Don't right shift for more than 32 bits on 32 bits systems. Don't left shift in case it results to number longer than 32 bits. Use functions from the gmp extension for bitwise manipulation on numbers beyond PHP_INT_MAX.

d_x
01.09.2009, 16:23
Don't right shift for more than 32 bits on 32 bits systems. Don't left shift in case it results to number longer than 32 bits. Use functions from the gmp extension for bitwise manipulation on numbers beyond PHP_INT_MAX.


Помимо Win XP x86, тестировал и на Vista x64. Везде ошибки. Тем более, операторы арифметические работают с int64 нормально на x64 и x86, а вот % почему-то опять нет.

Gifts
01.09.2009, 17:57
d_x К сожалению, вы заблуждаетесь. Разделение int32 и int64 - в ПХП нет. int - может быть 64 разрядным на _некоторых_ системах, но на руках мы опять-таки будем иметь число от -2^63+1 до 2^63-1 (или -2^31+1 до 2^31-1 но никак не ^32) за счет того, что беззнаковых целых в ПХП - нет. Плюс сам ПХП для винды - x86 по дефолту, сделать поддержку 64 бит - поленились.

Далее шаманство с числами. Любое число выходящее за пределы PHP_INT_MAX - автоматически приводится к типу float. Возьмем любой пример:
$a=4294967294;
echo gettype($a)."<br>\n";
$b = pow(2,32); // Для виндов и прочих 32битов
echo gettype($b)."<br>\n";
// И даже так не прокатит, потому что мы уже выходим за пределы целых чисел
$c = pow(2,31)-1;
echo gettype($c)."<br>\n";
// Но при этом
$d = 2147483647;
echo gettype($d)."<br>\n";

Итак, раз переменные у нас с плавающей точкой, то и притензий предъявлять не можем:
1) Не получится играться с битовыми операциями, ибо Bitwise operators allow evaluation and manipulation of specific bits within an integer. (c) http://ru.php.net/manual/en/language.operators.bitwise.php Оператор приводит оба числа к целому типу, а тут уже имеет место то самое echo (int) ( (0.1+0.7) * 10 ); // Получаем 7!
2) Не получится сравнивать малые разряды числа с большим порядком. Под мантиссу выделяется строго определенное число бит, остальные числа теряются за счет погрешности

З.Ы. Погрешности компьютерных измерений - достаточно интересная тема, жалко экзамен по этой теме я завалил(

d_x
01.09.2009, 18:14
Gifts, спасибо, теперь всё понятно стало. Действительно, из-за float не работают битовые операторы и взятие остатка от деления. Преобразование неявное, поэтому было непонятно. Забавно, что ++ и -- работают с float.

Gifts
01.09.2009, 18:54
d_x А почему бы им не работать, обычный инкремент на единицу. Да и, например, можно сколько угодно применять $a-- к числу большему ~1E16 (для 32бит) или 1E20 (для 64 бит), оно от этого не изменится

З.Ы. в пхп почти все не явно и "как бы упрощая", но на самом деле плодя ошибки, в отличие от питона, например :p