www.samag.ru
      Get it on Google Play
Поиск  
              
 www.samag.ru    Web  0 товаров , сумма 0 руб.
E-mail
Пароль  
 Запомнить меня
Регистрация | Забыли пароль?
Сетевой агент
О журнале
Журнал «БИТ»
Информация для ВАК
Звезды «СА»
Подписка
Где купить
Авторам
Рекламодателям
Магазин
Архив номеров
Мероприятия
Форум
Опросы
Ищу/Предлагаю работу
Спроси юриста
Игры
Контакты
   
Слайд шоу  
Представляем работы Виктора Чумачева
Виктор Чумачев – известный московский художник, который сотрудничает с «Системным администратором» уже несколько лет. Именно его забавные и воздушные, как ИТ, иллюстрации украшают многие серьезные статьи в журнале. Работы Виктора Чумачева хорошо знакомы читателям в России («Комсомольская правда», «Известия», «Московские новости», Коммерсант и др.) и за рубежом (США, Германия). Каждый раз, получая новый рисунок Виктора, мы в редакции улыбаемся. А улыбка, как известно, смягчает душу. Поэтому смотрите на его рисунки – и пусть у вас будет хорошее настроение!

  Опросы
Дискуссии  
17.09.2014г.
Просмотров: 12694
Комментарии: 3
Красть или не красть? О пиратском ПО как о российском феномене

Тема контрафактного ПО и защиты авторских прав сегодня актуальна как никогда. Мы представляем ...

 Читать далее...

03.03.2014г.
Просмотров: 17556
Комментарии: 1
Жизнь под дамокловым мечом

Политические события как катализатор возникновения уязвимости Законодательная инициатива Государственной Думы и силовых структур, ...

 Читать далее...

23.01.2014г.
Просмотров: 25167
Комментарии: 3
ИТ-специалист будущего. Кто он?

Так уж устроен человек, что взгляд его обращен чаще всего в Будущее, ...

 Читать далее...

1001 и 1 книга  
16.02.2017г.
Просмотров: 1923
Комментарии: 0
Опоздавших не бывает, или книга о стеке

 Читать далее...

17.05.2016г.
Просмотров: 5451
Комментарии: 0
Теория вычислений для программистов

 Читать далее...

30.03.2015г.
Просмотров: 7555
Комментарии: 0
От математики к обобщенному программированию

 Читать далее...

18.02.2014г.
Просмотров: 9828
Комментарии: 0
Рецензия на книгу «Читаем Тьюринга»

 Читать далее...

13.02.2014г.
Просмотров: 6793
Комментарии: 0
Читайте, размышляйте, действуйте

 Читать далее...

Друзья сайта  

Форум системных администраторов  

sysadmins.ru

 Эффективное использование памяти в Perl при работе с большими строками

Архив номеров / 2002 / Выпуск №1 (1) / Эффективное использование памяти в Perl при работе с большими строками

Рубрика: Администрирование /  Продукты и решения

ДАНИИЛ АЛИЕВСКИЙ

Эффективное использование памяти в Perl при работе с большими строками

Обычно при программировании в Perl не приходится задумываться о расходе памяти. Этот язык содержит достаточно качественную систему сборки мусора. Кроме того, при исполнении Perl-программ как обычных CGI-сценариев, с запуском интерпретатора Perl на каждое обращение к скрипту, вся использованная память гарантированно освобождается при завершении скрипта.

Но если Perl-скрипт обрабатывает действительно большие данные,— скажем, мегабайтные текстовые файлы – проблема разумного использования памяти может стать достаточно актуальной. Особенно это важно, если скрипт исполняется под управлением mod_perl или аналогичной среды. Если всецело положиться на встроенный сборщик мусора, может неожиданно оказаться, что процессы веб-сервера, исполняющие скрипты с помощью mod_perl, с каждым вызовом начинают занимать все больше памяти – вплоть до десятков мегабайт, постепенно поглощая всю свободную RAM.

Я столкнулся с этой проблемой, когда реализовывал под mod_perl сложный скрипт, предназначенный для обработки и парсинга произвольных HTML-страниц. Основным типом данных в скрипте были обычные текстовые строки. Поначалу я обращался со строками очень свободно, как это и принято в Perl и подобных языках, не задумываясь пользовался функцией substr; конкатенацией строк; регулярными выражениями; писал функции, возвращающие в результате строку (HTML-текст веб-страницы), и т.п. Привело это к тому, что типичный HTTPD-процесс с mod_perl (веб-сервер Apache на UNIX) тратил только на обрабатываемые данные в среднем несколько мегабайт. Это при том, что типичный размер HTML-страницы, которую следовало обработать, составлял всего 20-30 Кб. А когда я попробовал «пропустить» через свою программу 10-мегабайтный HTML – HTTPD-процесс «съел» 100 Мб. При этом возникало впечатление утечки памяти – процессы, по мере своей «жизни», занимали все больше и больше обьема памяти.

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

Результаты своих исследований я предлагаю вашему вниманию.

Итак, имеют место 2 основные общие проблемы.

Проблема I

Свободное употребление Perl-средств для работы со строками – regexp, substr, конкатенаций типа $a.$b или "$a$b" – приводит к порождению лишних копий строки, т.е. там, где по логике вещей алгоритму должно хватить 2 Мб, будет потрачено 5 или 10 Мб.

Проблема II

Если не предпринять специальных усилий, то после завершения Perl-функции рабочая память, израсходованная в этой функции, не будет освобождена! (Ситуация совершенно отличная от традиционной практики в языках без сборки мусора типа C++ или Pascal, когда все рабочие переменные, созданные внутри функции, уничтожаются при выходе из функции.)

Это не так важно в обычном CGI-скрипте, исполняемом внешним интерпретатором Perl. По завершении скрипта процесс будет полностью уничтожен вместе со всей своей памятью. Но в mod_perl или FastCGI, или в независимых приложениях, или серверах на Perl это очень существенно.

Обратите внимание – описанная проблема не есть истинная утечка памяти. Встроенный сборщик мусора действительно обеспечивает утилизацию ненужных переменных. Просто он делает это не совсем так, как можно было бы ожидать. А именно: занятая память будет использована повторно при следующем вызове той же самой функции, т.е. многократные повторные вызовы функции не будут приводить к постепенному исчерпанию RAM – явлению, которое традиционно называется утечкой памяти. Зато многократные вызовы приведут к другому: со временем будет занят наибольший объем памяти из всех, которые были нужны при различных вариантах вызова этой функции. В моем случае, после того как мои Perl-функции один раз обработали HTML-страницу размером 10 Мб и соответствующий процесс с mod_perl «съел» 100 Мб, он так и продолжал всегда занимать 100 Мб, хотя все последующие обрабатываемые страницы были небольшими. Внешне такое поведение очень похоже на утечку – объем памяти, занятый процессом, никогда не уменьшается, но постепенно медленно увеличивается – по мере того как этому процессу случайно попадаются данные все большего размера.

Теперь рассмотрим конкретные типовые задачи, возникающие при обработке данных в Perl. Я приведу примеры традиционного решения этих задач – неправильного в свете описанных проблем – и возможные варианты аккуратного решения, не приводящие к перерасходу памяти.

1. Как завести внутри функции большую временную текстовую переменную, а перед выходом из функции освободить память из-под нее?

Неправильное решение:

sub a {

  my $text= "very large string.... (1 MB)";

  работаем с $text;

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

    #память из-под $text (как это происходит со стековыми переменными в C++ и Pascal)

}

Правильное решение – добавить перед выходом вызов undef:

sub a {

  my $text= "very large string.... (1 MB)";

  работаем с $text;

  undef $text;

}

Вызов undef освободит память, занятую переменной $text. Без такого вызова получаем общую проблему II).

2. Функция должна создать большую строку и вернуть ее в результате

Неправильное решение:

sub a {

  my $text= "very large string.... (1 MB)";

  return $text;

}

my $v= a();

работаем с $v;

Такой Perl-код «съест» не 1 Мб, действительно необходимый для сохранения переменной $v, а 2 Мб. Лишний мегабайт будет занят интерпертатором Perl при вычислении строкового выражения «a()» для последующего копирования этих данных в переменную $v.

Мегабайт, занятый $v, можно впоследствии освободить вызовом «undef $v», но мегабайт, занятый при вычислении строкового выражения в правой части, по-моему, уже не освободить никак.

Правильное решение – функция должна вернуть ссылку на созданную большую строку:

sub a {

  my $text= "very large string.... (1 MB)";

  return $text;

}

my $v= a();

работаем с $$v;

undef $$v; #освобождаем память, отведенную функцией a

Такой код «съест» только 1 Мб, который освободится при вызове undef.

Проблема на самом деле довольно общая: никогда не следует писать выражение, результат которого – большая строка. Нельзя писать даже так:

my $v= $text." ";

если строка $text потенциально может быть большой (десятки килобайт или больше).

3. Как передать большую строку в функцию?

Неправильное решение:

sub a {

  my $text= $_[0]; #параметр $_[0] содержит строку длиной 1 MB

  работаем с $text;

  undef $text;

}

my $text= "very large string.... (1 MB)";

a($text);

В этом примере общей проблемыII нет, но память расходуется напрасно. Оператор присваивания $text= $_[0] расходует второй мегабайт под копию $text переменной $_[0] (который освобождается в конце вызовом «undef»).

Если есть возможность, лучше работать непосредственно с $_[0] – т.е. с алиасом внешней переменной. А еще лучше – нагляднее – всегда передавать большие строки по ссылке.

Предлагаемое правильное решение:

sub a {

  my $text= $_[0]; #параметр $_[0] содержит ССЫЛКУ на строку

  работаем с $$text;

}

my $text= "very large string.... (1 MB)";

a($text);

4. Как выполнить конкатенацию нескольких строк, одна из которых может быть очень большой?

Неправильное решение:

my $newtext= "$a$text$b";

или

my $newtext= $a.$text.$b;

Если строка $text велика, то подобный код «съест» память, которую нельзя освободить (см. задачу 2).

Правильное решение – конкатенировать по очереди:

my $newtext= $a;

$newtext.= $text;

$newtext.= $b;

5. Как удалить/заместить небольшую подстроку в очень большой строке?

Неправильное решение:

my $text= "very large string.... (1 MB)";

$text= substr($text,10);

Такой код потратит лишний неосвобождаемый мегабайт при вычислении выражения substr($text,10) – см. задачу 2.

Правильное решение – использовать так называемую «магию lvalue»:

my $text= "very large string.... (1 MB)";

substr($text,0,10)= "";

Правда, в документации написано, что Perl 5.004 в этом случае работал неэффективно. Но начиная с Perl 5.005 это работает прекрасно: лишняя память не расходуется.

Эквивалентное правильное решение – использовать 4-й параметр substr:

my $text= "very large string.... (1 MB)";

substr($text,0,10,"");

Но если предыдущий вариант в Perl 5.004 работает неэффективно, то такой вариант в Perl 5.004 вообще не скомпилируется.

6. Как выделить в большой строке большую подстроку? Скажем, как в мегабайтной строке выделить полумегабайтную подстроку, начиная со смещения 100 000?

По описанным выше причинам следующий очевидный код неправилен:

my $text= "very large string.... (1 MB)";

$text= substr($text,100000,500000);

В таком решении при вычислении «substr($text,100000,500000)» расходуются лишние полмегабайта, которые впоследствии невозможно освободить.

Для этой задачи я не нашел краткого и изящного решения. Возможный корректный подход использует следующую функцию substrlarge:

sub substrlarge {

# - Returns a reference to substr($_[0],$_[1],$_[2])

# and doesn"t use extra memory when $len is very large

# Example:

#     my $ps= substrlarge($text,500,1000000);

#     some actions with $$ps;

#     undef $$ps;

# - it is an economical equivalent for

#     my $s= substr($text,500,1000000);

#     some actions with $s;

  my $offset= $_[1];

  my $len= $_[2];

  $len= length($_[0])-$offset unless defined $len;

  if ($len*2

    my $k= 0;

    my $r= "";

    for (;$k<$len;$k+=32768) {

      $r.= substr($_[0],$offset+$k,$k+32768<=$len?32768:$len-$k);

    }

    return $r;

  } else {

    my $r= $_[0];

    substr($r,0,$offset)= "";

    substr($r,$len)= "" if defined $_[2];

    return $r;

  }

}

Если нужно выделить сравнительно небольшой фрагмент исходной строки (в данной реализации – меньше половины общей длины), то нужный фрагмент конструируется циклом, блоками по 32 Кб. Потеря памяти при этом составляет порядка 32 Кб – столько расходует вычисление выражения «substr($_[0],...)» внутри цикла.

Если же требуется получить большой фрагмент – больше половины исходной строки – то используется иной, более быстрый алгоритм. Создается полная копия исходной строки, после чего у нее обрезаются конец и начало, как описано в задаче 5. При этом временно занимается память под целую копию, но затем – при обрезании – занятый объем уменьшается. Так как требуемый объем памяти под конечный результат в данной ветке сравним с размером полной копии, то кратковременный расход памяти под полную копию представляется разумной платой за более высокую скорость.

Обратите внимание: функция substrlarge работает непосредственно с аргументом $_[0], не копируя его во временную переменную – как это обычно делается в начале Perl-функций. Копирование типа "my $s= $_[0]" привело бы к напрасному расходу памяти под лишнюю копию исходной строки (см. также задачу 3).

С использованием функции substrlarge правильное решение будет таким:

my $text= "very large string.... (1 MB)";

my $v= substrlarge($text,100000,500000);

работаем с $$v;

7. При использовании регулярных выражений, с большой строкой нельзя генерировать переменные $1,$2 и пр.

Скажем, следующий код неэффективен:

my $text= "very large?12?12 string.... (1 MB)";

$text=~s/^(.*??15??12?15??12)//s;

my $prefix= $1; #предполагается, что этот префикс невелик

Хотя от этого регулярного выражения нам требуется, очевидно, только префикс строки $1, который может быть и небольшим, Perl все равно заполнит переменные $&, $` и $". А одна из них будет большой – сравнимой с самой $text. Причем память из-под этих переменных автоматически не освободится.

Здесь единственное известное мне правильное решение – избегать применения регулярных выражений к потенциально большим строкам. В данном случае можно было написать цикл поиска пары переводов строки на основе вызовов функции index.

Можно также пользоваться «статическими» регулярными выражениями – не использующими скобок (или использующими только (?:...) ). Такие регулярные выражения не заполняют переменных $1,$2,...,$&,$`,$" и соответственно не расходуют много памяти.

8. Нужно прочитать из файла или сокета большой текст

Типичное решение выглядит примерно так:

my $text= "";

for (;есть что читать;) {

  my $buf= читаем очередные 32 KB;

  $text.= $buf;

  undef $buf;

}

работаем с $text;

undef $text;

Хотя на вид этот код вполне аккуратный и следует приведенным выше рекомендациям, на самом деле он все-таки может привести к проблеме. А именно, если общий объем читаемого текста порядка 1 Мб, то в процессе чтения в пике может израсходоваться не 1, а 2 Мб. Второй мегабайт потом обычно освобождается, но не гарантированно.

Эта тонкая проблема, по-видимому, связана с механикой переотведения памяти в Perl. Оператор «$text.= $buf» время от времени увеличивает память, занятую переменной $text. В процессе такого переотведения интерпретатору Perl, вероятно, требуется двойной объем памяти: под прежнюю строку $text и под новый, увеличенный буфер для этой переменной. В этот момент процесс и занимает лишний мегабайт. Видимо, если переотведение происходит в конце цикла, второй мегабайт может и не освободиться: в соответствии в общей идеологией Perl «запасать буфера памяти на будущее повторное использование».

Правильное решение описанной задачи – взять отведение памяти на себя. Например:

аккуратно отвести под $text 1 MB (1000000 байтов);

for ($n=0; есть что читать; $n+=32768) {

  my $buf= читаем очередные 32 KB;

  substr($text,$n,32768)= $buf; #"магия lvalue"

}

if ($n<1000000) {

  substr($text,$n)=""; #очищаем ненужный "хвост" $text

}

Если заранее неизвестно, что предстоит читать именно 1 Мб, можно изредка (именно изредка!) аккуратно выполнять самостоятельное переотведение памяти.

Для аккуратного отведения памяти можно предложить один из следующих приемов:

$text=" "; $text x= 1000000;

или

$text=" "; vec($text,1000000-1,8)= 32;

#код пробела; можно использовать любой другой символ

Оба способа отводят ровно 1 000 000 байтов памяти, ничего не тратя зря. Второй способ («магия lvalue» для функции vec) можно использовать также для переотведения памяти.

Все вышеописанное протестировано и неплохо работает в ActivePerl 5.005 на NT 4.0 и в стандартном Perl из FreeBSD 4.2. Под ActivePerl 5.6 в Windows 2000 все оказалось несколько хуже: undef не освобождает память. (По крайней мере, TaskManager не показывает сокращения памяти у процесса Perl, пока длится 10-секундный sleep, следующий за вызовом undef.) Впрочем, к моменту, когда вы будете читать эту статью, возможно, этот недостаток уже будет исправлен фирмой ActiveState.

В завершение хотелось бы сделать небольшое замечание. Если вас действительно интересует эффективность работы вашей программы – в плане экономии памяти, в плане быстродействия или в любом другом смысле – никогда не стоит полностью полагаться на документацию, общие рекомендации и советы. В том числе, приведенные в этой статье. Всегда измеряйте эффективность сами! Если реальная эффективность программы не соответствует вашим априорным оценкам, ищите «узкое место» – тот «плохой оператор», который отвечает за перерасход памяти или долгое выполнение. После чего создайте тест – минимальную программу, в которой «плохой оператор» проявляет свои скверные качества, – и ищите более качественное эквивалентное решение. Именно так были найдены все описанные выше приемы.


Комментарии отсутствуют

Добавить комментарий

Комментарии могут оставлять только зарегистрированные пользователи

               Copyright © Системный администратор

Яндекс.Метрика
Tel.: (499) 277-12-41
Fax: (499) 277-12-45
E-mail: sa@samag.ru