Alceu JuniorPublicado em 18/07/2007
r6 - 18 Jul 2007 - AlceuJunior
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:
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.
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
.
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.
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
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.
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.
----
Alceu Junior