Otimizando código Perl

Alceu Junior
Publicado em 18/07/2007

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:

1. Qual parte do código precisa ser otimizada
2. Quanto otimizar o código (isso inclui a capacidade de medir resultados antes e depois da otimização)
3. Como otimizar o código
4. Quando otimizar o código

Perl possui ferramentas padrões que podem ajudar com os dois primeiros itens. Nesse artigo será mostrado como usar o módulo 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ódulo Win32::OLE para acessar o MS Excel e o MS Outlook. Esse módulo tem como requisito o MS Windows (versões 95 e posteriores, possívelmente) 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.pl

O 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 bits

O 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::CopyFile

Nada 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 = '';

     ( $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 = '';
     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 pelo dprofpp 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

Notes

1 : Bem, isso é uma pequena mentira: atualmente patches de segurança do Outlook não permitem que isso seja feito diretamente, portanto o programa irá salvar cada mensagem criada na pasta Drafts e o usuário terá que clicar cada mensagem individualmente. Se isso é impraticável para você (por exemplo, você não quer revisar milhares de mensagens) eu recomendo fortemente que você procure usar SMTP para enviar esses emails.

----

AUTHOR

Alceu Junior

blog comments powered by Disqus