Страницы

четверг, 9 декабря 2010 г.

Оптимизация кода

Архитектурно правильный код - это хорошо. Он красиво выглядит, легко читается, все структурировано, а потом оказывается, что код работает долго, а все вроде бы красиво.
Так вот не всегда "правильный" с точки зрения стиля, архитектуры код самый быстрый.
Ну так получилось, так бывает, теория и практика знаете ли. А теперь идеи ускорения.

Идея 1. Назовем ее развертывание цикла.

Код :

my $ref = $dbh->selectall_arrayref(q[SELECT id FROM categories
WHERE c_status='active']);
my (@res);
my $count=10;
foreach my $tmp (@$ref){
my $ref=$dbh->selecall_hashref(qq[SELECT id,title,text,ts
FROM articles
WHERE category=$tmp
ORDER BY id DESC LIMIT $count],'id');
my @r;
push @r,$ref->{$_} foreach( key %$ref);
push @res,\@r;
}



То есть это простой пример вывода статей по 10 штук в каждой категорие. Он универсален, но его можно значительно ускорить потеряв при этом применимость нового алгоритма для некоторых частных случаев. "Развернув" внутренний цикл
мы получим ужасный код, но и более быстрый, избавившись в данном случае аж от 10 условных переходов (циклы это ничто иное как условные переходы) . Получим примерно следующее :
my $ref = $dbh->selectall_arrayref(q[SELECT id FROM categories
WHERE c_status='active']);
my @res;
my $count=10;
foreach my $tmp (@$ref){
my $ref=$dbh->selecall_arrayref(qq[SELECT id,title,text,ts
FROM articles
WHERE category=$tmp
ORDER BY id DESC LIMIT $count],'id');
my @r;
push @r,$ref->[0];
push @r,$ref->[1];
push @r,$ref->[2];
push @r,$ref->[3];
push @r,$ref->[4];
push @r,$ref->[5];
push @r,$ref->[6];
push @r,$ref->[7];
push @r,$ref->[8];
push @r,$ref->[9];
push @res,\@r;
}


Идея 2. Банальное кеширование

Не будем уходить далеко в мир высокого, а продолжим рассматривать пример выше.
Логично предположить, что новые категории добавляются не каждый день, потому от запроса на выборки списка надо избавиться путем сохранения его в памяти. Самый простой способ это Memcached, другой способ рассмотрим ниже.

sub get_cache_connection
{

require Cache::Memcached::Fast;
require Storable;
my $ref=new Cache::Memcached::Fast(
{
servers =>[{ address => '127.0.0.1:11211',weight=>2.5}],
namespace => 'sessions:',
connect_timeout => 0.2,
io_timeout => 0.5,
close_on_error => 1,
compress_threshold =>-1,
ketama_points => 150,
nowait => 1,
hash_namespace => 1,
serialize_methods => [ \&Storable::freeze, \&Storable::thaw ]
}
);

return $ref;

}

my $tabs;
my $md=get_cache_connection();
my $ref=$md->get('categories')) ;
foreach(@$ref){
###some code there

}
Кеширование применимо для данных, которые не часто меняются, при другом подходе может возникнуть проблема, что накладные расходы на работу с кешированием превысят затраты на работу с базой данных. Не забывайте, что mysql тоже кеширует запросы, и в предыдущем примере запрос на выборку категорий по сути не имеет смысла, потому что с огромной вероятностью он будет закеширован mysql. Другое дело, если вы генерируете из категорий свой особый html код, вот его можно и за кешировать.


Идея 3. Развертывание функций.


Думаю вы уже догадались в чем фишка. Рассмотрим предыдущие примеры, только добавим внутрь цикла функцию форматирования даты - ts.
my $ref = $mem_cache->get('categories')) ;
my @res;
my $count=10; foreach my $tmp (@$ref){
my $ref=$dbh->selecall_arrayref(qq[SELECT id,title,text,ts
FROM articles
WHERE category=$tmp
ORDER BY id DESC LIMIT $count],'id');
my @r;
format_date(\$ref->[0]->[3]);
push @r,$ref->[0];
format_date(\$ref->[1]->[3]);
push @r,$ref->[1];
format_date(\$ref->[2]->[3]);
push @r,$ref->[2];
format_date(\$ref->[3]->[3]);
push @r,$ref->[3];
format_date(\$ref->[4]->[3]);
push @r,$ref->[4];
format_date(\$ref->[5]->[3]);
push @r,$ref->[5];
format_date(\$ref->[6]->[3]);
push @r,$ref->[6];
format_date(\$ref->[7]->[3]);
push @r,$ref->[7];
format_date(\$ref->[8]->[3]);
push @r,$ref->[8];
format_date(\$ref->[9]->[3]);
push @r,$ref->[9];

push @res,\@r;
}


Заметка кстати, профайлинг Perl показал, что быстрее передавать параметры в функцию по значению, а не по указателю( то есть не так как здесь ;) ). И заменяем все format_date на их код.

my $ref = $mem_cache->get('categories')) ;
my @res;
my $count=10;
foreach my $tmp (@$ref){
my $ref=$dbh->selecall_arrayref(qq[SELECT id,title,text,ts
FROM articles
WHERE category=$tmp
ORDER BY id DESC LIMIT $count],'id');
my @r;

$ref->[0]->[3]=~/(\d{1,4})-(\d{1,2})-(\d{1,2})/;
$ref->[0]->[3]="$3.$2.$1";
push @r,$ref->[0];
$ref->[1]->[3]=~/(\d{1,4})-(\d{1,2})-(\d{1,2})/;
$ref->[1]->[3]="$3.$2.$1";


push @r,$ref->[1];
$ref->[2]->[3]=~/(\d{1,4})-(\d{1,2})-(\d{1,2})/;
$ref->[2]->[3]="$3.$2.$1";
push @r,$ref->[2];
$ref->[3]->[3]=~/(\d{1,4})-(\d{1,2})-(\d{1,2})/;
$ref->[3]->[3]="$3.$2.$1";
push @r,$ref->[3];
$ref->[4]->[3]=~/(\d{1,4})-(\d{1,2})-(\d{1,2})/;
$ref->[4]->[3]="$3.$2.$1";
push @r,$ref->[4];
$ref->[5]->[3]=~/(\d{1,4})-(\d{1,2})-(\d{1,2})/;
$ref->[5]->[3]="$3.$2.$1";
push @r,$ref->[5];
$ref->[6]->[3]=~/(\d{1,4})-(\d{1,2})-(\d{1,2})/;
$ref->[6]->[3]="$3.$2.$1";
push @r,$ref->[6];
$ref->[7]->[3]=~/(\d{1,4})-(\d{1,2})-(\d{1,2})/;
$ref->[7]->[3]="$3.$2.$1";
push @r,$ref->[7];
$ref->[1]->[8]=~/(\d{1,4})-(\d{1,2})-(\d{1,2})/;
$ref->[1]->[8]="$3.$2.$1";
push @r,$ref->[8];
$ref->[1]->[9]=~/(\d{1,4})-(\d{1,2})-(\d{1,2})/;
$ref->[1]->[9]="$3.$2.$1";
push @r,$ref->[9];
push @res,\@r;
}

В итоге мы види ужасный неудобоворимый код, который вызывает рвотные рефлексы при одном своем виде. Дальше агрументов много
1) А что делать, если полей в таблице дофига, и все надо форматировать, это ж "каша"
2) А если надо увеличить/уменьшить количество статей, снова лезть в код!
И так далее.. И так далее...Все эти вопросы можно решить при помощи "админки". А админка есть у любого уважающего себя сайта статусом выше, персональная страничка.
На нужно при изменение нужных вам параметров сделать генерацию нужного вам кода.
Например добавить изменение количества выводимых статей с 10 на 5( вполне обычная функция для администраторского интерфейса). Помещаем функцию обработки вывода статей в отдельный модуль например "/lib/Article.pm". Даем права на работу с этим файлом пользователю от которого работает наш скрипт
( ну или обычные авось chmod 777 /lib/Artcle.pm).
ну и в функцию изменения количества статей пишем примерно следующее:
open(Fl,">document_root/lib/Article");

print FL, q{
package lib::Artcle;
use strict;
use base qw[Exporter];
use SiteDB;##connecting to the database $dbh
our @EXPORT = qw(
list
);





sub list{
my $ref = $mem_cache->get('categories')) ;
my @res;
my $count=}.$new_count_value.q{;

foreach my $tmp (@$ref){
my $ref=$dbh->selecall_arrayref(qq[SELECT id,title,text,ts
FROM articles
WHERE category=$tmp
ORDER BY id DESC LIMIT $count],'id');
my @r;

$ref->[0]->[3]=~/(\d{1,4})-(\d{1,2})-(\d{1,2})/;
$ref->[0]->[3]="$3.$2.$1";
push @r,$ref->[0];
$ref->[1]->[3]=~/(\d{1,4})-(\d{1,2})-(\d{1,2})/;
$ref->[1]->[3]="$3.$2.$1";


push @r,$ref->[1];
$ref->[2]->[3]=~/(\d{1,4})-(\d{1,2})-(\d{1,2})/;
$ref->[2]->[3]="$3.$2.$1";
push @r,$ref->[2];
$ref->[3]->[3]=~/(\d{1,4})-(\d{1,2})-(\d{1,2})/;
$ref->[3]->[3]="$3.$2.$1";
push @r,$ref->[3];
$ref->[4]->[3]=~/(\d{1,4})-(\d{1,2})-(\d{1,2})/;
$ref->[4]->[3]="$3.$2.$1";
push @r,$ref->[4];
$ref->[5]->[3]=~/(\d{1,4})-(\d{1,2})-(\d{1,2})/;
$ref->[5]->[3]="$3.$2.$1";
push @r,$ref->[5];
$ref->[6]->[3]=~/(\d{1,4})-(\d{1,2})-(\d{1,2})/;
$ref->[6]->[3]="$3.$2.$1";
push @r,$ref->[6];
$ref->[7]->[3]=~/(\d{1,4})-(\d{1,2})-(\d{1,2})/;
$ref->[7]->[3]="$3.$2.$1";
push @r,$ref->[7];
$ref->[1]->[8]=~/(\d{1,4})-(\d{1,2})-(\d{1,2})/;
$ref->[1]->[8]="$3.$2.$1";
push @r,$ref->[8];
$ref->[1]->[9]=~/(\d{1,4})-(\d{1,2})-(\d{1,2})/;
$ref->[1]->[9]="$3.$2.$1";
push @r,$ref->[9];
push @res,\@r;
\}

return \@res;
\}

1;
};
close(FL);
Ну и далее можем использовать этот модуль у себя в скрипте обычным способом :



sub some_sub{
my $self=shift;
require lib::Article;
my $ref=lib::Article::list();
###some code there
####
}


Всем удачи.

p.s
При помощи генерации кода можно избавиться вообще от последнего цикла обхода категорий.

2 комментария:

DiAmon комментирует...

Интересная статья. С большим удовольствием почитал и оценил авторский стиль описания. А вы могли бы добавить замеры времени на выполнение каждой функции. Хочется оценить в временном выражении выигрыш. А каков расход памяти у каждого варианта?

Богдан комментирует...

ооо про самый главный довод " замеры " я и забыл. сделаем...