Эффективная работа с массивами в ПХП

Silent

Новичок
Эффективная работа с массивами в ПХП

Ниже приведены некоторые результаты работы ПХП кода с большими объемами данных. Под большим объемом можно понимать как однократную обработку одной строки, так и многократную обработку нескольких маленьких строк. Рассмотрим оба случая. В качестве теста решалась такая задача: дан текст (примем, что текст уже обработан, вырезаны все ненужные символы, слова отделены одним пробелом), нужно получить список уникальных слов. Измерения проводились на локальном компьютере (Athlon 1.4, WinME), версия ПХП - 4.1.0 и 4.2.3 (надо отметить, что новая версия гораздо быстрее старой). Использовались строки разной длины - 10, 150 и 500 килобайт. Данные строки обрабатывались в цикле несколько раз. Результаты приведены в следующем виде: тестируемый код, затем в первом столбце длина строки, во втором - число повторов, в третьем - время для старой версии, в четвертом - время для новой версии. При однократных повторах делалось несколько замеров, но все равно ошибка там может быть достаточно большая.

1) Самый очевидный код:
PHP:
$words = array();
$words = preg_split("/ /",$html_text);
$words = array_unique($words);

10kb  - 500 раз - 2.7 sek.   (3  sek)
150kb - 1 раз   - 4.3 sek.   (1  sek)
150kb - 5 раз   - 73 sek.    (5  sek)
150kb - 50 раз  -    -       (50 sek)
500kb - 1 раз   - 208 sek.   (93 sek)
Короткие строки обрабатываются достаточно быстро в обоих версиях. С увеличением длины строки время растет очень нелинейно, особенно это заметно у старой версии, в новой дело обстоит лучше. При обработке больших строк в цикле у старой версии время также растет далеко не линейно, очевидно имеются большие накладные расходы на создание и уничтожение больших массивов. Новая версия показывает идеальное линейное приращение времени - 1, 5 и 50 секунд на 1, 5 и 50 повторов.

2) Небольшое изменение кода приводит к странным результатам.
PHP:
$words_temp = array();
$words = array();
$words_temp = preg_split("/ /",$html_text);
$words = array_unique($words_temp);

10kb  - 500 раз - 2.7 sek.   (3    sek)
150kb - 1 раз   - 0.5 sek.   (0.35 sek)
150kb - 5 раз   - 60 sek.    (4.4  sek)
150kb - 50 раз  -    -       (49   sek)
500kb - 1 раз   - 29 sek.    (29   sek)
500kb - 5 раз   -    -       (>5   min.)
Если для маленьких и средних строк почти ничего не изменилось, то для большой строки введение временного массива дало значительное ускорение.

3) Откажемся от встроенной функции array_unique и применим классический прием, популярный в Перле.
PHP:
$words_temp = array();
$words = array();
$words_temp = preg_split("/ /",$html_text);
foreach ($words_temp as $word) {
    $words[$word] = 1;
}

10kb  - 500 раз - 2.1 sek.    (2.5   sek)
150kb - 1 раз   - 0.064 sek.  (0.065 sek)
150kb - 5 раз   - 60 sek.     (1.6   sek)
150kb - 50 раз  -    -        (14.4  sek)
500kb - 1 раз   - 0.9 sek.    (0.8   sek)
500kb - 5 раз   - >5 min.     (16.7  sek)
Опять для маленьких строк ничего не изменилось. Для средних и больших строк при многократном повторении кода новая версия невероятно быстрее старой. В среднем результаты быстрее, чем при использовании встроенной функции. Возникает вопрос, как же нужно програмировать, чтобы код на ПХП был быстрее "оптимизированной" функции на Си.

4) Наболее радикальный, почти сишный код:
PHP:
    $pos = 0;
    $words = array();
    do  {
        $new_pos = strpos($html_text," ",$pos);
        if ($new_pos === FALSE) {
            $word = substr($html_text,$pos);
            $words[$word] = 1;
            break;
        };
        $word = substr($html_text,$pos,$new_pos-$pos);
        $words[$word] = 1;
        $pos = $new_pos+1;
    } while (1>0);


10kb  - 500 раз - 3.4 sek.   (3.5 sek)
150kb - 1 раз   - 0.1 sek.   (0.1 sek)
150kb - 5 раз   - 1 sek.     (1   sek)
150kb - 50 раз  - 4.9 sek.   (5.5 sek)
500kb - 1 раз   - 1 sek      (1   sek)
500kb - 5 раз   - 10.6 sek.  (3.5 sek)
При однократном выполнении кода он оказывается медленнее предыдущих (хотя и незначительно), но в цикле он обходит практически все остальные варианты. Единственной причиной этого явления может быть то, что в этом коде мы практически отказались от использования массивов (используется всего один массив). Отсюда вывод - для наиболее эффективной работы с массивами лучше всего по возможности отказаться от них. Лень было проводить дальнейшие замеры, но точно также не стоит использовать такие встроенные функции, как array_diff или array_intersect, их легко заменить парой строк на ПХП, как это было сделано в варианте 3.
 

.des.

Поставил пиво кому надо ;-)
Ну а это вообще не секрет, что массивы в пхп очень и очень тормозные.
Насчет заменить стандартные функции array_dif,array_unique может быть ты и прав, но прелесть пхп в том, чтобы решать типичные задачи веба легко и непринужденно (кстати и в других языках даже более низкоуровневых, зачастую встроенные стандартные функции реализованы с использованием самых примитивных и медленных алгоритмов). Дело в том, что задачи где пхп заставляют обрабатывать в n-мерных массивах мегабайтные данные это немножко изврат.

З.Ы. Но проведенные тесты как дополнительное доказательство очень и очень интересны
 

Макс

Старожил PHPClub
а ты уверен что такие цифры именно от этих функций?
Может preg_split() виноват.
Попробуй explode() вместо него использовть.

Я только что пару тестов провел, он на порядок быстрее (ИМХО)
 

Silent

Новичок
Не знаю как ты проверял, но у меня абсолютно одинаковые результаты для preg_split и explode. Тем более, что в третьем варианте получены достаточно нормальные результаты (для новой версии). Тут видимо две основные причины: плохой алгоритм выделения памяти и сборки мусора (улучшено в новой версии) и плохая реализация функций array_xxx (до сих пор не исправлено).

Я не хочу сказать, что ПХП обязан перемалывать мегабайты за доли секунды, но ужасная нелинейная зависимость у функции array_unique меня поразила. Ведь третий вариант работает гораздо лучше встроенных функций. Мне просто непонятно, как может быть устроена эта функция, если в ПХП уже есть все возможности для реализации линейного по сложности алгоритма (вариант 3), не надо ничего нового писать. А в array_unique просто страшный рост времени от объема данных.

А также можно отметить, что четвертый вариант совсем немного отстает от Перла. То есть потенциально скорость ПХП на данной задаче очень близка к Перлу, но реализован он криво, и без тестов выявить оптимальный вариант сложно. Где же тут легкость и непринужденность.

> зачастую встроенные стандартные функции реализованы с использованием самых примитивных и медленных алгоритмов

Если бы в фортране математические функции были бы реализованы с использованием медленных алгоритмов, разработчикам давно бы по голове настучали.

P.S. Я согласен, что мегабайты для вебскриптов - это достаточно редкий объем данных. Но 100 килобайт - это не так уж и много. И с развитием скоростного доступа к инету будет все больше и больше "тяжелых" страниц. Уже сейчас головная страница какого-нибудь новостного портала может иметь размер более 100 килобайт, поэтому работать с таким объемом быстро надо уметь уже сейчас.
 

AnToXa

prodigy-одаренный ребенок
покажи весь код теста.
имхо ты некорректно тестил
 

[VS]

Guest
Автор оригинала: Silent
плохая реализация функций array_xxx (до сих пор не исправлено).
А что ты хочешь, еси алгоритм Боуера-Мура был реализован в str_replace только после версии 4.

Я не хочу сказать, что ПХП обязан перемалывать мегабайты за доли секунды, но ужасная нелинейная зависимость у функции array_unique меня поразила.
Этими проблемами страдают и другие функции. Есть один важный комментарий к тестам. Где ты их гонял? На какой платформе?

А в array_unique просто страшный рост времени от объема данных.
Это можно обьяснить, так как array_unique может работать с массовом в котором что угодно (строки, числа, обьекты, массивы).
А твой метод работает только если в массиве строки или числа.

Если бы в фортране математические функции были бы реализованы с использованием медленных алгоритмов, разработчикам давно бы по голове настучали.
А в фортране есть встроенные матемачические функции? =)
 

.des.

Поставил пиво кому надо ;-)
Silent АнтоХа прав. Тестил ты явно некорректно.
вот пример теста, который по приведенному тобой коду провел я.
PHP:
// В качестве текста я взял книгу Б.Срауструпа 
//(оставив в ней только слова разделенные одним пробелом)
//объем 800Кб
timestart("likec");
for($si=0;$si<3;$si++)
{
$pos = 0; 
    $words = array(); 
    do  { 
        $new_pos = strpos($html_text," ",$pos); 
        if ($new_pos === FALSE) { 
            $word = substr($html_text,$pos); 
            $words[$word] = 1; 
            break; 
        }; 
        $word = substr($html_text,$pos,$new_pos-$pos); 
        $words[$word] = 1;
        $pos = $new_pos+1; 
    } while (1>0); 
$words=array_keys($words);
}
timestop("likec");

timestart("array_unique");
$words=array();
$words=explode(" ",$html_text);
for($si=0;$si<3;$si++)
{
$worduniq=array_unique($words); 
}
timestop("array_unique");

timeprint();
Результаты
PHP:
likec	12.6852 сек
array_unique	9.8578 сек
вся пpогpамма pаботала 22.5817 сек
Если $words=explode(" ",$html_text);
внести в цикл то результаты получаются примерно одинаковые.
 

Silent

Новичок
> покажи весь код теста.
имхо ты некорректно тестил

Вышеприведенный код (каждый вариант отдельно) запускался в цикле несколько раз. Насколько это корректно, я не знаю, но реальные скрипты работают именно так, поэтому и цифры должны быть тоже более менее реальными. Если ты можешь проверить по другому, сделай и запости результаты, это будет полезнее, чем твое "имхо".

>На какой платформе?

Платформа вроде указана.

> А твой метод работает только если в массиве строки или числа.

Объяснить это можно чем угодно... Вспоминается старый анекдот:
Встречаются два политика, один и говорит другому:
- Послушай, я вот никак не могу понять, как это мы так быстро все развалили...
- Это просто, сейчас я тебе все объясню.
- Э-э-э нет, объяснить я и сам могу, я понять хочу.

А что, часто бывают реальные ситуации, когда нужно применять эту функцию к массивам, в которых лежит что угодно. Или все же чаще ее бездумно применяют везде, не задумываясь о том, насколько это может замедлить работу. И я не совсем понял, что означает можно применять в любом случае. "array_unique() sorts the values treated as string at first" - вроде написано, что никакие объекты оно не переваривает, только строки. Ну ладно, сортировка - это O(n*lg n), то есть линейной сложности в их реализации нет, но ведь результаты получаются гораздо хуже, чем O(n*lg n).

> А в фортране есть встроенные матемачические функции? =)

В каких-нибудь ранних версиях наверняка были. А вообще не знаю, все ли пусть даже современные процессоры поддерживают комлексные функции с двойной точностью? И если нет, тогда как это реализовано в фортране без использования собственных функций?
 

[VS]

Guest
Автор оригинала: Silent
Платформа вроде указана.
Сорри, не заметил.
На Win32 бенчмаркить PHP несколько не правильно. Гоняй на юниксе.

> А твой метод работает только если в массиве строки или числа.
Объяснить это можно чем угодно... Вспоминается старый анекдот:
Я тебе дал четкое и понятное обьяснение ограниченности твоего метода.

> А в фортране есть встроенные матемачические функции? =)

В каких-нибудь ранних версиях наверняка были. А вообще не знаю, все ли пусть даже современные процессоры поддерживают комлексные функции с двойной точностью? И если нет, тогда как это реализовано в фортране без использования собственных функций?
Никакие современные процессоры напрямую не поддерживают комплексные переменные, так как это абсурд. Комплексная переменная - это просто 2 вещественных переменных.
Двойная точность тут вообще не причем.
Работа с комплейксными переменными есть и в С++, причем не хуже чем в Фортране.
А именно специализированных математических функций в фортране нет. И вообще это уродский язык :)
 

Silent

Новичок
To .des.:

А твой вариант вообще не работает:)) Нет такой функции - timestart. А версия ПХП какая?

А если серьезно, то у меня до определенного числа килобайт array_unique быстрее, затем полнейший провал, а первый вариант (в твоем коде) отрабатывает за 3 секунды на этих же данных.

P.S. Я всего лишь привел разультаты своих замеров. В любом случае, как бы некоректно я не тестил, одну минуту от одной секунды я и без секундомера отличу. А если это только на моей машине ПХП отказывается работать (у него наверное чутье на перловиков), тогда это еще хуже. Ведь программист никогда не знает, на какой машине будет работать его код через полгода, а тут такая зависимость от компьютера.

P.P.S. Может нужны какие особые настройки, но у меня вроде оперативки для ПХП выделяется достаточно, на таких объемах это не должно сказываться.
 

Silent

Новичок
>Я тебе дал четкое и понятное обьяснение ограниченности твоего метода.

А я привел выдержку из документации, где говорится, что array_unique работает ТОЛЬКО со строками, поэтому если не трудно, объясни подробнее.
 

.des.

Поставил пиво кому надо ;-)
Ну почему за три секунды понятно у тебя Атлон 1.4 а у меня П2 350 разница есть.
версия пхп 4.2.3
Насчет функции timestart :) конечно нет такой функции.
это ДиМина функция с php.spb.ru
http://php.spb.ru/php/speed(2001nov13).exe
но суть не меняется и там и там.. microtime() :)
перепроверь еще раз!
покажи как ты замеряешь.
 

Silent

Новичок
> Ну почему за три секунды понятно у тебя Атлон 1.4 а у меня П2 350 разница есть.

Нет, это потому, что у меня входные данные другие. Но для 150 кб соотношение было как у тебя, а потом каюк. И не важно как я замеряю (как в мануале написано), но, еще раз повторюсь, одну секунду от одной минуты (примерно столько я ждал и не дождался) я отличаю и без секундомера. Завтра, если будет не лень, попробую на сервере.

P.S. А вообще, для себя я выводы сделал. Если у кого-нибудь есть желание убеждать меня в обратном, с удовольствием почитаю, ответов не обещаю.
 

Silent

Новичок
Кстати, для твоего второго варианта внесение двух строк внутрь цикла (а ведь в первом твоем варианте все находится внутри цикла) меняет картину очень сильно:
PHP:
for($si=0;$si<3;$si++) 
{ 
$words=array(); 
$words=explode(" ",$html_text); 
$worduniq=array_unique($words);  
}
А если добавить еще одну строку, опять будет быстрее, но не до прежнего уровня:
PHP:
for($si=0;$si<3;$si++) 
{ 
$words=array(); 
$worduniq=array(); 
$words=explode(" ",$html_text); 
$worduniq=array_unique($words);  
}
P.S. Закачал на сервер. Полнейшая ерунда получилась, практически никаких отличий. Значит это глюки работы с памятью Виндовс версии. Все вышенаписанное оказалось бредом, только зря день потерял.
 

.des.

Поставил пиво кому надо ;-)
да внесение двух строк в цикл меняет картину(но все равно отставание unique на текстах до мегабайта не превышает 20 %) а вынесение строк за цикл выводит внутреннюю функцию вперед по скорости.
Кстати я заглянул в сорцы пхп и посмотрел алгоритм array_unique
массив сначала сортируется zend_qsort (алгоритм быстрой сортировки) а потом проходятся по нему еще раз удаляя дубликаты..
то есть особых изъянов в реализации не видно.

а вот что касается метода с помощью strpos -снова старый вопрос... к разработчикам пхп.. почему такие странные функции.. strpos находит позицию строки, а strrpos позицию символа..
бред.
 

Silent

Новичок
> то есть особых изъянов в реализации не видно.

Ну да, на сервере я их тоже не заметил. Кто же знал, что использовать ПХП на локальном компьютере вредно для здоровья? Я привык как в Перле, там особых различий нет (хотя надавно нашел один баг в ActivePerl, при особых условиях он при откытии файла может потереть последний символ). Но, поскольку ставить себе на рабочий компьютер Линукс я не собираюсь, ПХП придется забросить.
 

.des.

Поставил пиво кому надо ;-)
у меня нет разницы windows и unix показывают одно и тоже..
// более того unix отдает предпочтение твоему методу
 

.des.

Поставил пиво кому надо ;-)
Немножко поразмыслив предлагаю свой вариант array_unique
по всем тестам он обошел все другие.. :) (правда не на много :(но на больших файлах (2,5 мб) уже почти в полтора раза!!
PHP:
$worduniq=array();
$words=explode(" ",$html_text);
for($j=1;$j<sizeof($words)-1;$j++)
	$worduniq[$words[$j]]=1;
// Ну и чтобы получить массив со значениями
$words=array_keys($words);
PHP:
// Файл 2,5 МБ
explode_keys	9.3931	// этот метод
likec	14.0616	// strpos
array_unique	13.6491	// встроенная функция
 

tony2001

TeaM PHPClub
>Измерения проводились на локальном компьютере (Athlon 1.4, WinME)
на тесты под Вин можно просто внимание не обращать.
 
Сверху