Странное потребление памяти

Тема в разделе "PHP", создана пользователем babahalki, 4 ноя 2016.

Модераторы: latteo
  1. babahalki

    babahalki

    Регистр.:
    6 май 2016
    Сообщения:
    246
    Симпатии:
    98
    Всем привет.
    Столкнулся с нехваткой 512мб для выполнения скрипта. Расстановка get_memory_usage() показала, что памяти хватает на выполнение главного цикла, но на втором цикле память не освобождалась, в результате fatal error.

    Сломал себе всю голову. Сначала делал обнуление всех переменных после их использования. через $var = ''; $var = null;
    Потом делал unset($var);
    Потом сразу и то и другое. Ничего не помогало.

    Дальше сделал следующее, поделил весь скрипт на 3 функции. Насколько я правильно понял, после выполнения функции все ее локальные переменные и их данные выгружаются из памяти.

    Получилось вот такие 3 функции.

    Первая функция делает многомерный массив с набором параметров id, name, value. Она выполняется дважды с разными параметрами, чтобы получить 2 массива.
    Код:
    function feat_opt ($f0_ids, $f0_object, $c) {
           $okay = new Okay();
           $o0_object = $okay->features->get_categories_options(array('category_id'=>$c->children, 'feature_id'=>$f0_ids));
           dtimer::reset();
           foreach($o0_object as $o0) {
             foreach($f0_object as $f0) {
               if ($f0->id == $o0->feature_id) {
               //$features_array[] = array($f0->id=>array($o0->translit));
               $feat_opt[] = array('name'=>$f0->name, 'url'=>$f0->url, 'id'=>$f0->id, 'value'=>$o0->value, 'translit'=>$o0->translit);
               }
             }
           }
           return $feat_opt;
    }
    
    Вторая функция перемножает 2 массива между собой и выдает массив. id, id2, name, name2, но так чтобы если есть пара id=>1 id2=>2, пара id=>2 id2=>1 не создавалась.
    Код:
    function feat_mix ($feat0, $feat1) {
           $okay = new Okay();
           $keys = array();
           $k1 = 0;
         foreach($feat0 as $opt0) {
           $k1++;
           $k2 = 0;
           foreach($feat1 as $opt1) {
             $k2++;
             if ($opt0['id'] != $opt1['id']) {
               $key = $k1 * $k2 + $k1 + $k2;
    
               if (!array_key_exists($key,$keys)) {
               $keys[$key] = 1;
               $feat_opt_mix[] = array(
               'key' => $key,
               'name'=>$opt0['name'],
               'url'=>$opt0['url'],
               'id'=>$opt0['id'],
               'value'=>$opt0['value'],
               'translit'=>$opt0['translit'],
               'name2'=>$opt1['name'],
               'url2'=>$opt1['url'],
               'id2'=>$opt1['id'],
               'value2'=>$opt1['value'],
               'translit2'=>$opt1['translit']
               );
               }
             }
           }
         }
           return $feat_opt_mix;
    }
    

    3 функция пишет массив в xml файл.
    Код:
    function write_file ($feat_mix) {
         foreach($feat_mix as $opt) {
           $f0_name = $opt['name'];
           $f0_url = $opt['url'];
           $o0_value = $opt['value'];
           $o0_translit = $opt['translit'];
           $f1_name = $opt['name2'];
           $f1_url = $opt['url2'];
           $o1_value = $opt['value2'];
           $o1_translit = $opt['translit2'];
           $url = '/catalog/'.esc($c->url).'/'.esc($f0_url).'-'.esc($o0_translit).'/'.esc($f1_url).'-'.esc($o1_translit);
           $last_modify =  $c->last_modify;
           $last_modify = substr($last_modify, 0, 10);
           file_put_contents("temp.xml", "<url>"."\n", FILE_APPEND);
           file_put_contents("temp.xml", "<loc>$url</loc>"."\n", FILE_APPEND);
           file_put_contents("temp.xml", "<lastmod>$last_modify</lastmod>"."\n", FILE_APPEND);
           file_put_contents("temp.xml", "</url>"."\n", FILE_APPEND);
           }
         return null;
         }
    
    в самой программе следующее.

    Код:
    print "before write_file memory usage: ".memory_get_usage(true)." bytes\r\n";
    
    write_file(feat_mix(feat_opt($f0_ids, $f0_object, $c), feat_opt($f1_ids, $f1_object, $c)));
         }
    print "after write_file memory usage: ".memory_get_usage(true)." bytes\r\n";
    
    Потребление памяти не изменилось, до функции 80мб, после функции 170мб, дальше fatal error.

    Стоило мне поменять цикл с foreach на while, и все заработало, поменял только в третьей функции, которая писала большой массив в файл.

    Код:
    function write_file ($feat_mix) {
         while(count($feat_mix) > $i) {
           $f0_name = $feat_mix[$i]['name'];
           $f0_url = $feat_mix[$i]['url'];
           $o0_value = $feat_mix[$i]['value'];
           $o0_translit = $feat_mix[$i]['translit'];
           $f1_name = $feat_mix[$i]['name2'];
           $f1_url = $feat_mix[$i]['url2'];
           $o1_value = $feat_mix[$i]['value2'];
           $o1_translit = $feat_mix[$i]['translit2'];
           $url = '/catalog/'.esc($c->url).'/'.esc($f0_url).'-'.esc($o0_translit).'/'.esc($f1_url).'-'.esc($o1_translit);
           $last_modify =  $c->last_modify;
           $last_modify = substr($last_modify, 0, 10);
           file_put_contents("temp.xml", "<url>"."\n", FILE_APPEND);
           file_put_contents("temp.xml", "<loc>$url</loc>"."\n", FILE_APPEND);
           file_put_contents("temp.xml", "<lastmod>$last_modify</lastmod>"."\n", FILE_APPEND);
           file_put_contents("temp.xml", "</url>"."\n", FILE_APPEND);
           $i++;
           }
         return null;
         }
    

    Вопрос заключается в следующем. Чем отличаются циклы foreach и while с точки зрения потребления памяти?

    Почему локальные переменные и их данные после выполнения функции не освобождали память?
     
  2. Den1xxx

    Den1xxx

    Moderator
    Регистр.:
    15 янв 2014
    Сообщения:
    278
    Симпатии:
    151
    Они отличаются в способе работы.
    While до выполнения условия, foreach — пока не переберёт весь массив.
    Возможно, в случае с while просто не весь массив перебирается, потому потребление памяти меньше.

    В функции мне непонятно:
    1. Почему 4 раза надо открывать файл на запись? Почему нельзя сделать это 1 раз?
    2. Зачем присваивать переменные внутри цикла, зачем вообще их создавать, если они используются 1 раз внутри функции?
    3. Нет проверки на ошибки. Уверены, что входящий массив обладает тему ключами, что используете? Нет ли там вложенного массива среди
    $feat_mix?

    Вообще, после перебора массива рекомендуют уничтожать временные переменные https://habrahabr.ru/post/136835/
    Но как бы у Вас это не должно отражаться, т.к. они будут уничтожаться автоматом — вы же не по ссылке передаёте, да и перебор массива внутри функции.

    Ещё причина может быть в том, что при манипуляции с массивом при переборе часто создаётся копия массива. Я не помню точно, но, кажется, в PHP 7 некоторые стремные куски кода работы с массивами были переписаны.
     
    Последнее редактирование: 4 ноя 2016
    babahalki нравится это.
  3. babahalki

    babahalki

    Регистр.:
    6 май 2016
    Сообщения:
    246
    Симпатии:
    98
    Насчёт записи файла, пожалуй, быстрее будет 1 раз открыть и 1 раз закрыть. Спасибо.

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

    С уничтожением переменных - бесполезно. Я их и unset и обнулял, и unset по элементам массива. Сработало только если отказаться от foreach. While и for работает нормально - отработала функция и память освободилась.
    Массив feat_mix, как видно из кода является индексным двумерным. Внутри него набор массивов ассоциативных по 11 элементов в каждом.

    Как-то особенно работает foreach, видимо там не только 1 копия создаётся, которая меняется в цикле. Как бы отследить что именно в памяти остаётся?
     
  4. Den1xxx

    Den1xxx

    Moderator
    Регистр.:
    15 янв 2014
    Сообщения:
    278
    Симпатии:
    151
    Мне кажется, надо сначала убедиться, что while и for действительно перебирают весь массив.
    А где создается объект $c?
    Значит как-то не так «обнуляли».

    Вообще, всю функцию write_file можно было записать в 1 строчку...
     
  5. latteo

    latteo Эффективное использование PHP, MySQL

    Moderator
    Регистр.:
    28 фев 2008
    Сообщения:
    1.609
    Симпатии:
    1.538
    Дамп вот этих объектов покажите
    PHP:
    $o0_object $okay->features->get_categories_options(array('category_id'=>$c->children'feature_id'=>$f0_ids));
    2е, а как вы определять собрались, где затык?
    Правильный тест:
    PHP:
    print "start memory usage: ".memory_get_usage(true)." bytes\r\n";

    $optOne feat_opt($f0_ids$f0_object$c);

    print 
    "feat_opt 1 memory usage: ".memory_get_usage(true)." bytes\r\n";

    $optTwo feat_opt($f1_ids$f1_object$c);

    print 
    "feat_opt 2 memory usage: ".memory_get_usage(true)." bytes\r\n";

    $mix feat_mix($optOne$optTwo);

    print 
    "feat_mix memory usage: ".memory_get_usage(true)." bytes\r\n";

    write_file($mix);

    print 
    "write_file memory usage: ".memory_get_usage(true)." bytes\r\n";

     
  6. babahalki

    babahalki

    Регистр.:
    6 май 2016
    Сообщения:
    246
    Симпатии:
    98
    Затык происходит на write_file($mix);
    Память не освобождается почти после этой функции, только мегабайт 20-30, и на втором цикле уже die.

    Вот print_r o0_object
    http://pastebin.com/DU4eU4tr

    Объект $c создается в цикле foreach.
    Код:
    [QUOTE="Den1xxx, post: 2640255, member: 354212"]Мне кажется, надо сначала убедиться, что while и for действительно перебирают весь массив.[/QUOTE]
    Так в условиях на for и стоит счетчик. 
    
    Это кусок кода целиком, где память не течет и все ок. 
    [code]
    //*
    // функции для объединения массивов со значениями и массива с названиями опций
    function feat_opt($f0_ids, $f0_object, $c, $b) {
           $okay = new Okay();
           $o0_object = $okay->features->get_categories_options(array('category_id'=>$c->children, 'brand_id'=>$b->id, 'feature_id'=>$f0_ids));
           file_put_contents('o0_object'.$c->url.'txt', print_r($o0_object, true));
           dtimer::reset();
           for($i = 0, $count = count($o0_object); $count > $i; $i++) {
             for($i2 = 0, $count2 = count($f0_object); $count2 > $i2; $i2++) {
               if ($f0_object[$i2]->id == $o0_object[$i]->feature_id) {
               //$features_array[] = array($f0_object[$i2]->id=>array($o0_object[$i]->translit));
               $feat_opt[] = array(
               'name'=>$f0_object[$i2]->name,
               'url'=>$f0_object[$i2]->url,
               'id'=>$f0_object[$i2]->id,
               'value'=>$o0_object[$i]->value,
               'translit'=>$o0_object[$i]->translit);
               }
             }
           }
           return $feat_opt;
    }
    
    // функция для перемножения массивов
    function feat_mix($feat0, $feat1) {
           $okay = new Okay();
           $keys = array();
           $k1 = 0;
         for($i =0, $count = count($feat0); $count > $i; $i++) {
           $k1++;
           $k2 = 0;
           for($i2 = 0, $count2 = count($feat1); $count2 > $i2; $i2++) {
             $k2++;
             if ($feat0[$i]['id'] != $feat1[$i2]['id']) {
               $key = $k1 * $k2 + $k1 + $k2;
    
               if (!array_key_exists($key,$keys)) {
               $keys[$key] = 1;
               $feat_opt_mix[] = array(
               'key' => $key,
               'name'=>$feat0[$i]['name'],
               'url'=>$feat0[$i]['url'],
               'id'=>$feat0[$i]['id'],
               'value'=>$feat0[$i]['value'],
               'translit'=>$feat0[$i]['translit'],
               'name2'=>$feat1[$i2]['name'],
               'url2'=>$feat1[$i2]['url'],
               'id2'=>$feat1[$i2]['id'],
               'value2'=>$feat1[$i2]['value'],
               'translit2'=>$feat1[$i2]['translit']
               );
               }
             }
           }
         }
           return $feat_opt_mix;
    }
    ////*/
    
    
    //*
    // Категории + feature + feature
    
    
    $c = $okay->categories->get_categories();
    dtimer::reset();
    for (reset($c); $i3 = key($c); next($c)){
    if($c[$i3]->visible) {
       //print "before category cycle + f + f memory usage: ".memory_get_usage(true)." bytes\r\n";
       $c_name = $c[$i3]->name;
       $f0_ids = array();
       $f0_object = array();
       $f1_ids = array();
       $f1_object = array();
       $f0 = $okay->features->get_features(array('category_id'=>$c[$i3]->children, 'in_filter'=>1));
       for($i = 0; count($f0) > $i; $i++) {
         // массив $f_ids из небольшого фильтра $filter_minus
         if (!in_array($f0[$i]->id, $filter_minus)) {
           $f0_ids[$f0[$i]->id] = $f0[$i]->id;
           $f0_object[] = $f0[$i];
         }
         // массив $f_ids2 из жесткого фильтра $filter_minus_hard
         if (!in_array($f0[$i]->id, $filter_minus)) {
           $f1_ids[$f0[$i]->id] = $f0[$i]->id;
           $f1_object[] = $f0[$i];
         }
       }
    
       $feat_mix = feat_mix(feat_opt($f0_ids, $f0_object, $c[$i3], null), feat_opt($f1_ids, $f1_object, $c[$i3], null));
       for($i = 0, $count = count($feat_mix); $count > $i; $i++) {
         $f0_name = $feat_mix[$i]['name'];
         $f0_url = $feat_mix[$i]['url'];
         $o0_value = $feat_mix[$i]['value'];
         $o0_translit = $feat_mix[$i]['translit'];
         $f1_name = $feat_mix[$i]['name2'];
         $f1_url = $feat_mix[$i]['url2'];
         $o1_value = $feat_mix[$i]['value2'];
         $o1_translit = $feat_mix[$i]['translit2'];
         $url = $okay->config->root_url.'/catalog/'.esc($c[$i3]->url).'/'.esc($f0_url).'-'.esc($o0_translit).'/'.esc($f1_url).'-'.esc($o1_translit);
         $last_modify =  $c[$i3]->last_modify;
         $last_modify = substr($last_modify, 0, 10);
         file_put_contents($smap_n_prefix.$sitemap_index.$smap_n_ext, "<url>"."\n", FILE_APPEND);
         file_put_contents($smap_n_prefix.$sitemap_index.$smap_n_ext, "<loc>$url</loc>"."\n", FILE_APPEND);
         file_put_contents($smap_n_prefix.$sitemap_index.$smap_n_ext, "<lastmod>$last_modify</lastmod>"."\n", FILE_APPEND);
         file_put_contents($smap_n_prefix.$sitemap_index.$smap_n_ext, "</url>"."\n", FILE_APPEND);
         if (++$url_index == 50000) {
           file_put_contents($smap_n_prefix.$sitemap_index.$smap_n_ext, '</urlset>'."\n", FILE_APPEND);{}
           $url_index=0;
           $sitemap_index++;
           $sitemaps[] = $sitemap_index;
           if (file_exists($okay->config->root_dir.'/'.$smap_n_prefix.$sitemap_index.$smap_n_ext)) {
             @unlink($okay->config->root_dir.'/'.$smap_n_prefix.$sitemap_index.$smap_n_ext);
           }
           file_put_contents($smap_n_prefix.$sitemap_index.$smap_n_ext, '<?xml version="1.0" encoding="UTF-8"?>'."\n");
           file_put_contents($smap_n_prefix.$sitemap_index.$smap_n_ext, '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'."\n", FILE_APPEND);
    
    
         }
       }
       $feat_mix = '';
       //print "after category cycle + f + f memory usage: ".memory_get_usage(true)." bytes\r\n";
    }
    }
    $c = '';
    
    //*/
    
    


    Как можно сделать не так?
    У меня стояло сразу все по очереди и по отдельности.
    была такая последовательность, чтобы наверняка.
    в конце цикла:

    foreach ($feat_mix as $k=>$f) {
    unset($feat_mix[$k]);
    }
    unset($feat_mix);
    $feat_mix = array();

    Я реально не изменял кода за исключением способа перебора массива. foreach и память течет, for или while и все ок. В итоге остановился на for, чтобы удобнее было делать нумератор элементов $i++.
    Есть ли какой-то способ посмотреть процесс выполнения кода по этапам? get_memory_usage() показывает в каком месте утечка, я написал это. А как бы увидеть чем именно забивается память?
     
    Последнее редактирование модератором: 29 ноя 2016
  7. lemurs

    lemurs Постоялец

    Регистр.:
    8 ноя 2015
    Сообщения:
    75
    Симпатии:
    18
    foreach создает в определенных условия копию массива, если он итерационный, что приводит к дополнительным расходам памяти
    более того, тут то же не без греха, использование в while постоянный пересчет count($feat_mix) - будет замедлять цикл
     
  8. alex_me

    alex_me

    Регистр.:
    25 янв 2017
    Сообщения:
    167
    Симпатии:
    113

    Массив ни причем
    Никогда не используйте file_put_contents() в цикле, тем более во вложенном!! цикле
    PHP создает под нее буфер записи

    Вместо этого используйте последовательно fopen() , fwrite() и fclose() в конце цикла
    Примерно так:

    PHP:
    function write_file ($feat_mix) {
         foreach(
    $feat_mix as $opt) {
           
    $file fopen('temp.xml''w')

    .....
           
    fwrite($file"<url>"."\n");
           
    fclose($file);
           }
         }
     
    Последнее редактирование модератором: 2 мар 2017
  9. mSnus

    mSnus Постоялец

    Регистр.:
    4 дек 2015
    Сообщения:
    76
    Симпатии:
    28
    Кроме всего прочего, строка while(count($feat_mix) > $i) будет пересчитывать каждый раз этот самый count при каждой итерации.
    Оно вам надо? Правильнее посчитать количество один раз перед циклом, и сравнивать с ним, раз уж оптимизируете.

    Естественно, что for/while будут быстрее и легче foreach, если в параметре у вас многомерный массив, а цикл надо сделать по линейному индексу.

    И ещё, делая посреди цикла unset($feat_mix[$someindex]), вы можете порушить цикл - количество-то поменяется...
     
  10. s0urce

    s0urce Писатель

    Регистр.:
    11 июл 2016
    Сообщения:
    4
    Симпатии:
    1
    Это не ответы на заданные вопросы, но сдается мне, вопросы поставлены не правильные, не ведущие к результату.
    Задача похожа на ребус, который для того, чтобы голову сломать себе и окружающим :)
    Зачем так писать программы?

    1. Вложенные циклы - не есть хорошо... Это зло. Нужно стараться писать без вложенных циклов.
    Создание объектов, заполнение массива во вложенном цикле - это зло в квадрате.
    Если уж совсем невозможно без вложенных циклов, то уходить от большого количества итераций в этих циклах.

    2. Нужно тестировать (человек выше правильно написал) - надо _измерять_, как растет потребляемая память, и как она освобождается при unset, и освобождается ли на самом деле.
    (Реализация движка php в данной конкретной версии вполне может быть такой, что unset не сразу освобождает память).
    "То, что не измеряешь, тем не управляешь". (c)

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

    3. Можно потратить неделю чтобы найти ответы, а можно не искать ответы, а решить задачу по-другому (не особо думая) в течение двух часов.
    Например, сделать два скрипта. Один подготавливает данные и сохраняет их во временные файлы.
    Второй скрипт - обрабатывает эти временные файлы. Может не надо всё в памяти стараться держать?

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