r6 - 18 Jul 2007 - AlceuJunior
Otimizando código Perl
Existem diversas formas de otimizar um programa, independentemente da linguagem de programação. Na maioria das vezes isso incluia a revisão de algorítmos, requisitos do programa e utilização de recursos da linguagem e do sistema operacional, por exemplo. A grande dificuldade em otimizar um programa está em determinar:- Qual parte do código precisa ser otimizada
- Quanto otimizar o código (isso inclui a capacidade de medir resultados antes e depois da otimização)
- Como otimizar o código
- Quando otimizar o código
Devel::DProf através de exemplos práticos.
Programa exemplo
O programa-exemplo desse artigo lê uma planilha MS Excel que possui atividades a serem executadas, verificar quais atividades estão atrasadas e envia emails para as pessoas definidas como responsáveis via MS Outlook (1). Para dar conta disso tudo o programa utiliza o móduloWin32::OLE para acessar o MS Excel e o MS Outlook. Esse módulo tem como requisito o MS Windows (versões 95 e posteriores, possivelmente) e do ActivePerl (ActivePerl 5.8.8; versões diferentes podem também funcionar) instalados, além desses dois programas que fazem parte do MS Office. O módulo Class::Accessor também deve estar disponível.
E finalmente, para a interface gráfica foi utilizado o toolkit WxPerl, também disponível para o ActivePerl.
O programa é constituído de duas partes: um módulo chamado Pending.pm e o script pending.pl.
Qual parte do código otimizar?
Essa é a primeira pergunta que o programador deve se fazer antes de começar a trabalhar no código. Sem verificar primeiro as áreas críticas do programa que precisam de uma revisão para melhorar a performance, o programador pode perder muito tempo otimizando partes do código que representam uma parte pequena ou insignificante no tempo de execução total do programa.DProf e dprofpp
Essas duas ferramentas ajudam a definir os tempos de execução de cada trecho do código. Para descobrir a quantas anda o programa, basta executar:C:\> perl -d:DProf pending.plO módulo
DProf vai gerar um arquivo com o nome tmon.out no mesmo diretório aonde foi executado. Esse arquivo possui uma série de informações sobre a execução do programa, mas ler essa informação diretamente não é nada prático:
#fOrTyTwO $hz=1000; $XS_VERSION='DProf 20050603.00'; # All values are given in HZ $over_utime=31; $over_stime=15; $over_rtime=48; $over_tests=10000; $rrun_utime=641; $rrun_stime=375; $rrun_rtime=8218; $total_marks=12819 PART2 @ 15 0 16 & 2 main BEGIN + 2 - 2 + 2 & 3 strict bitsO trecho exibido acima mostra que olhar diretamente no arquivo não é lá uma forma muito agradável de entender os resultados. Para esse fim existe o programa
dprofpp:
C:\> dprofpp
Total Elapsed Time = 8.157477 Seconds
User+System Time = 0.955477 Seconds
Exclusive Times
%Time ExclSec CumulS #Calls sec/call Csec/c Name
16.3 0.156 0.156 10 0.0156 0.0156 DynaLoader::dl_load_file
8.27 0.079 0.801 5 0.0158 0.1602 MyFrame::BEGIN
7.95 0.076 0.076 411 0.0002 0.0002 Params::Validate::_validate
4.92 0.047 0.077 4 0.0117 0.0193 DateTime::TimeZone::Local::Win32::
BEGIN
4.81 0.046 0.122 377 0.0001 0.0003 DateTime::Locale::_register
4.71 0.045 0.043 242 0.0002 0.0002 Win32::OLE::Dispatch
3.24 0.031 0.182 7 0.0044 0.0259 DateTime::Locale::BEGIN
3.24 0.031 0.031 8 0.0039 0.0038 Wx::BEGIN
3.24 0.031 0.305 23 0.0013 0.0133 DateTime::BEGIN
3.14 0.030 0.413 7 0.0043 0.0590 Pending::BEGIN
3.04 0.029 0.029 715 0.0000 0.0000 Win32::OLE::Tie::Fetch
3.04 0.029 0.138 1 0.0286 0.1378 Pending::send_warns
1.67 0.016 0.016 1 0.0160 0.0160 Wx::_boot_GDI
1.67 0.016 0.016 1 0.0160 0.0160 warnings::BEGIN
1.67 0.016 0.016 1 0.0160 0.0160 Wx::bootstrap
Muito mais fácil dessa maneira. A saída mostra as subrotinas executadas, qual o tempo que elas consumiram da execução total do programa (%Time) e as vezes que foram executadas (Calls) entre outros detalhes.
As subrotinas mais custosas foram as Win32::OLE::Const, então elas são as candidatas a serem modificadas ou substituídas. Como esse módulo é padrão do ActivePerl, tentar otimizá-lo diretamente pode não ser a melhor idéia: ela já está por aí a um bom tempo, e alguém já deve ter tentado fazer isso anteriormente. Nesse caso específico, as bibliotecas Win32::OLE não são muito rápidas mesmo, e Win32::OLE::Const só tem uma função: carregar constantes de um programa Microsoft para dentro do namespace de um programa.
Ainda que correndo o risco de diminuir a facilidade de manutenção do programa, foi apenas uma questão de remover o uso dos módulos:
#use Win32::OLE::Const 'Microsoft Excel'; #use Win32::OLE::Const 'Microsoft Outlook';E substituir as contantes por seus respectivos valores numéricos. Segue o trecho do código que usava as constantes do MS Excel:
# finding where the spreadsheet finishes
my $last_row = $sheet->UsedRange->Find(
{
What => "*",
# same as xlPrevious from Excel constants
SearchDirection => 2,
# same as xlByRows from Excel constants
SearchOrder => 1
}
)->{Row};
E o trecho correspondente do MS Outlook:
# setting the email body as HTML
# same as constant olFormatHTML
$item->{BodyFormat} = 2;
Ainda que fosse possível carregar apenas as constantes desejadas, para um programa que usa apenas três constantes não parece ser muita vantagem carregar esses módulos grandalhões. É claro, o programa poderia deixar de funcionar com uma versão diferente do MS Office. É por isso que se diz que otimizações prematuras são sempre um problema: é sempre bom verificar antes aonde se está pisando.
Agora é hora de testar o resultado das últimas alterações:
C:\> perl -d:DProf pending.pl C:\> dprofpp Total Elapsed Time = 4.137462 Seconds User+System Time = 0.637462 Seconds Exclusive Times %Time ExclSec CumulS #Calls sec/call Csec/c Name 12.0 0.077 0.507 5 0.0154 0.1015 MyFrame::BEGIN 7.37 0.047 0.046 242 0.0002 0.0002 Win32::OLE::Dispatch 7.37 0.047 0.047 10 0.0047 0.0047 DynaLoader::dl_load_file 7.06 0.045 0.045 411 0.0001 0.0001 Params::Validate::_validate 5.02 0.032 0.032 6 0.0053 0.0053 ActiveState::Path::BEGIN 4.86 0.031 0.275 7 0.0044 0.0393 Pending::BEGIN 4.86 0.031 0.076 377 0.0001 0.0002 DateTime::Locale::_register 4.71 0.030 0.160 1 0.0304 0.1603 Pending::send_warns 4.71 0.030 0.030 715 0.0000 0.0000 Win32::OLE::Tie::Fetch 2.51 0.016 0.016 1 0.0160 0.0160 Wx::_boot_Frames 2.51 0.016 0.016 1 0.0160 0.0160 Exporter::Heavy::_rebuild_cache 2.51 0.016 0.016 1 0.0160 0.0160 Win32::TieRegistry::TiedRef 2.51 0.016 0.016 3 0.0053 0.0053 Win32::OLE::GetActiveObject 2.51 0.016 0.016 1 0.0160 0.0160 Wx::TreeItemId::BEGIN 2.51 0.016 0.016 2 0.0080 0.0080 Win32::CopyFileNada mal para um começo rápido! Agora é preciso encontrar outras coisas aonde seja possível mexer sem muito estardalhaço. Olhando o último resultado, é possível ver que o campeão de chamadas e tempo utilizado é uma subrotina do módulo
Params::Validate. Eu não uso esse módulo diretamente dentro do meu programa, então eu preciso dar uma procurada nos módulos importados no programa e pesquisar pelo Params::Validate. O ActivePerl fornece uma ferramenta chamada ppm que ajuda nesse sentido:
C:\> ppm tree DateTime
package DateTime-0.35
needs DateTime-Locale (installed in site area)
package DateTime-Locale-0.33
needs Module::Build (v0.2806 installed in site area)
package Module-Build-0.2806 provide Module::Build
(no dependencies)
needs Params::Validate (v0.87 installed in site area)
package Params-Validate-0.87 provide Params::Validate
...
Analisando o código do programa, é possível verificar que o programa instancia objetos DateTime mais vezes do que seria realmente necessário:
# trecho do código da subrotina send_warns
if ( $status eq 'Ação Avisada' ) {
my $prazo =
$sheet->Cells( $row, $columns{PRAZO}->[0] )->{'Value'};
# not assuming that the column have only date values
if ( ( $prazo ne '' )
and ( ref($prazo) eq 'Win32::OLE::Variant' ) )
{
if ( $prazo->Type == VT_DATE ) {
my $variant = Variant( VT_DATE, $prazo );
my $date = DateTime->new(
year => $variant->Date('yyyy'),
month => $variant->Date('M'),
day => $variant->Date('dd')
);
# the $date variable will have 0 in the values below, so it necessary to force 0 onto $now too
# to be able to compare both correctly
my $now = DateTime->now();
$now->set_hour(0);
$now->set_minute(0);
$now->set_second(0);
my $result = DateTime->compare( $date, $now );
Mais tarde, em outra subrotina:
sub queue_row {
my $sheet = shift;
my $row = shift;
my $fields_ref = shift;
my $message = shift;
my $item;
my $today = DateTime->now();
$today->set_time_zone('local');
my $greetings;
$item = '<html><body>';
( $today->hour() < 12 )
? ( $greetings = 'Bom dia, ' )
: ( $greetings = 'Boa tarde, ' );
Sem muito esforço, seria possível instanciar apenas um objeto DateTime que represente o dia e horário atuais. Mais uma alteração no código a fazer, sendo a primeira declarar esse objeto único de forma que ele seja global para o pacote:
my $now = DateTime->now();
$now->set_time_zone('local');
Alterando o trecho correspondente da subrotina send_warns novamente:
my $date = DateTime->new(
year => $variant->Date('yyyy'),
month => $variant->Date('M'),
day => $variant->Date('dd'),
# forcing the same time to be able to compare dates correctly
hour => $now->hour(),
minute => $now->minute(),
second => $now->second()
);
my $result = DateTime->compare( $date, $now );
E também da subrotina queue_row:
sub queue_row {
# html code is based on the code generated when the email is created mannually in Outlook
my $sheet = shift;
my $row = shift;
my $fields_ref = shift;
my $message = shift;
my $item = '<html><body>';
my $greetings;
( $now->hour() < 12 )
? ( $greetings = 'Bom dia, ' )
: ( $greetings = 'Boa tarde, ' );
Mais um teste com o dprofpp para averiguar o resultado:
Total Elapsed Time = 3.983193 Seconds
User+System Time = 0.592193 Seconds
Exclusive Times
%Time ExclSec CumulS #Calls sec/call Csec/c Name
10.3 0.061 0.075 377 0.0002 0.0002 DateTime::Locale::_register
7.94 0.047 0.456 5 0.0094 0.0913 MyFrame::BEGIN
7.77 0.046 0.046 10 0.0046 0.0046 DynaLoader::dl_load_file
5.40 0.032 0.032 4 0.0080 0.0080 DynaLoader::bootstrap
5.23 0.031 0.046 4 0.0077 0.0116 DateTime::TimeZone::Local::Win32::
BEGIN
4.90 0.029 0.029 715 0.0000 0.0000 Win32::OLE::Tie::Fetch
4.90 0.029 0.136 1 0.0285 0.1360 Pending::send_warns
2.70 0.016 0.016 1 0.0160 0.0160 Wx::_boot_Controls
2.70 0.016 0.152 1 0.0160 0.1520 Wx::App::MainLoop
2.70 0.016 0.016 1 0.0160 0.0159 Wx::import
2.70 0.016 0.016 3 0.0053 0.0053 main::BEGIN
2.70 0.016 0.031 4 0.0040 0.0077 Config::BEGIN
2.70 0.016 0.016 6 0.0027 0.0027 Params::Validate::BEGIN
2.70 0.016 0.016 5 0.0032 0.0032 warnings::register::import
2.70 0.016 0.016 4 0.0040 0.0039 DateTime::TimeZone::America::Sao_P
aulo::BEGIN
Dessa vez o resultado não foi tão animador assim. Mesmo reduzindo a criação de um objeto DateTime, ainda assim vários objetos serão instanciados uma vez que o programa entre em loop. E falando em loop, uma nova análise no código sobre como evitar objetos sendo criados desnecessariamente mostram que o programa não precisa de um novo objeto que represente o MS Excel a cada vez que a subrotina create_email seja executada, como é mostrado abaixo:
sub create_email {
my $body = shift;
my $addresse = shift;
my $subject = shift;
my $Outlook = Win32::OLE->GetActiveObject('Outlook.Application')
|| Win32::OLE->new('Outlook.Application');
Movendo essa instanciação para fora da função e testando novamente:
Total Elapsed Time = 4.788211 Seconds
User+System Time = 0.849211 Seconds
Exclusive Times
%Time ExclSec CumulS #Calls sec/call Csec/c Name
9.18 0.078 0.077 411 0.0002 0.0002 Params::Validate::_validate
8.95 0.076 0.076 715 0.0001 0.0001 Win32::OLE::Tie::Fetch
7.42 0.063 0.641 5 0.0126 0.1282 MyFrame::BEGIN
7.07 0.060 0.137 377 0.0002 0.0004 DateTime::Locale::_register
5.65 0.048 0.393 7 0.0068 0.0562 Pending::BEGIN
5.53 0.047 0.210 7 0.0067 0.0300 DateTime::Locale::BEGIN
5.42 0.046 0.046 10 0.0046 0.0046 DynaLoader::dl_load_file
5.42 0.046 0.045 18 0.0025 0.0025 DateTime::TimeZone::BEGIN
5.18 0.044 0.042 242 0.0002 0.0002 Win32::OLE::Dispatch
3.77 0.032 0.032 3 0.0107 0.0107 Win32::OLE::GetActiveObject
3.65 0.031 0.062 4 0.0077 0.0155 DateTime::TimeZone::Local::Win32::
BEGIN
3.53 0.030 0.030 328 0.0001 0.0001 Params::Validate::_validate_pos
1.88 0.016 0.016 1 0.0160 0.0160 warnings::BEGIN
1.88 0.016 0.016 1 0.0160 0.0160 Wx::_boot_Events
1.88 0.016 0.016 1 0.0160 0.0160 Wx::Load
O resultado piorou? Como é possível?
Se for considerado que nem sempre um email será enviado, criar o objeto na inicialização do módulo só atrasaria as coisas. Teoricamente seria possível, por exemplo, implementar o padrão de projeto Singleton e sempre devolver a mesma instância do objeto do Outlook quando a função create_email for chamada mais de uma vez. Nesse caso, pode ser tentado algo bem mais simples:
# fora da subrotina create_email my $Outlook;Já dentro de
create_email:
sub create_email {
my $body = shift;
my $addresse = shift;
my $subject = shift;
unless ( defined($Outlook) ) {
$Outlook = Win32::OLE->GetActiveObject('Outlook.Application')
|| Win32::OLE->new('Outlook.Application');
}
Mais um teste:
Total Elapsed Time = 3.867208 Seconds
User+System Time = 0.805208 Seconds
Exclusive Times
%Time ExclSec CumulS #Calls sec/call Csec/c Name
9.69 0.078 0.594 5 0.0156 0.1188 MyFrame::BEGIN
9.44 0.076 0.121 377 0.0002 0.0003 DateTime::Locale::_register
7.70 0.062 0.061 411 0.0001 0.0001 Params::Validate::_validate
5.84 0.047 0.378 7 0.0067 0.0540 Pending::BEGIN
5.46 0.044 0.044 715 0.0001 0.0001 Win32::OLE::Tie::Fetch
3.97 0.032 0.032 10 0.0032 0.0032 DynaLoader::dl_load_file
3.97 0.032 0.271 23 0.0014 0.0118 DateTime::BEGIN
3.85 0.031 0.194 7 0.0044 0.0278 DateTime::Locale::BEGIN
3.85 0.031 0.046 4 0.0077 0.0115 DateTime::TimeZone::Local::Win32::
BEGIN
3.85 0.031 0.043 241 0.0001 0.0002 Win32::OLE::DESTROY
3.73 0.030 0.028 242 0.0001 0.0001 Win32::OLE::Dispatch
3.73 0.030 0.030 3 0.0100 0.0100 Win32::OLE::GetActiveObject
3.73 0.030 0.030 328 0.0001 0.0001 Params::Validate::_validate_pos
3.73 0.030 0.046 4 0.0075 0.0114 Config::BEGIN
1.99 0.016 0.016 1 0.0160 0.0160 warnings::BEGIN
Conclusão
A idéia desse artigo não é mostrar a forma "correta" de otimizar um programa, até porque isso não existe. Vários fatores influenciam no resultado final e nem sempre técnicas utilizadas anteriormente podem ser aplicadas diretamente em oportunidades futuras. Como, quando e quanto otimizar um programa são perguntas que poderão ser respondidas dependendo da experiência do programador. Poderíamos continuar tentando otimizar mais ainda o programa-exemplo, principalmente se for considerado que fatores indiretos (como otimizações no sistema operacional e no hardware), influenciam no resultado final do programa. É importante, portanto, saber quando já se fez o suficiente e parar por aí. Testar a performance de um programa pode ser mais difícil do que parece por conta dos fatores externos. Resultados diferentes podem ser mostrados pelodprofpp a cada execução, então sempre é aconselhável repetir os testes mais de uma vez, tentando manter a mesma situação de testes. Programas que necessitam de interação com o usuário são piores ainda de serem testados e nesse caso o recomendado seria desativar essa interação da melhor forma possível, automatizando opções que seria feitas pelo usuário. Testar otimizações quando o software utiliza um banco de dados também pode ser problemático devido ao cache automático que os SGBD modernos costumam fazer.
Outra observação importante é que executar um programa em máquinas diferentes com certeza apresentará resultados diferentes dependendo das modificações do código. Se você usa ambientes diferentes durante o processo de desenvolvimento (ambientes de desenvolvimento, testes e produção) o recomendável é que o programador otimize o código em um ambiente de testes muito similar ao que será utilizado em produção para maximizar as otimizações feitas.
Mais informações
- WxPerl (instalação da biblioteca gráfica): http://wxperl.sourceforge.net/
- Documentação do WxPerl: http://wxperl.pvoice.org/kwiki/index.cgi?
- Instalação do ActivePerl: http://www.activeperl.com
- Usando o MS Excel com Perl: http://www-128.ibm.com/developerworks/linux/library/l-pexcel/
- Usando o MS Outlook com Perl: http://www.perlmonks.org/?node_id=185757
- perldoc Devel::DProf
