Introdução
Este tópico está entre “Dicas Valiosas” e “Boas Práticas de Programação”, de qualquer modo aborda um exemplo de código, ainda em desenvolvimento por um colega que está iniciando no “Mundo do AdvPL”. Partindo deste fonte de exemplo, vamos analisar o que o fonte faz, como faz, e inclusive avaliar algumas opções de escrever um algoritmo que faça a mesma coisa, usando funções diferentes ou escritos de uma outra forma, e se existe algum ganho ou perda, direto ou indireto ao optarmos por uma ou outra forma de resolução.
O Fonte em AdvPL – Primeira versão
#INCLUDE "PROTHEUS.CH" #INCLUDE "TOTVS.CH" #INCLUDE "FILEIO.CH" USER FUNCTION TESTE() local nArquivo local i local nNumero local nNum := 7 msgAlert("Início.") nArquivo := fcreate("C:\devstudio\Melhorias\src\arquivo.txt") if ferror() != 0 msgalert ("ERRO GRAVANDO ARQUIVO, ERRO: " + str(ferror())) return endif for i:=1 to 500 nNumero = randomize(100000,999999) if (mod(nNumero,nNum)) == 0 //dividendo e divisor respectivamente fWrite(nArquivo, "O numero < " + alltrim(str(nNumero)) + " > é divisível por " + alltrim(str(nNum)) + chr(13) + chr(10)) else fwrite(nArquivo, "O numero <" + alltrim(str(nNumero)) + "> não é divisível " + alltrim(str(nNum)) + chr(13) + chr(10)) Endif next fclose(nArquivo) msgAlert("Fim.") Return
Engenharia Reversa
Olhando o fonte linha por linha, podemos deduzir o que ele faz ao ser executado. Inicialmente, o fonte declara as variáveis utilizadas com escopo “Local”, mostra uma mensagem na tela do SmartClient informando que o programa está no “Início”, após pressionar OK ou o simplesmente fechar a janela a aplicação continua, criando um arquivo em um path específico na máquina onde está sendo executado o SmartClient –que pode ser a mesma máquina onde o SmartClient pode estar sendo executado, ou uma outra máquina conectando neste Protheus Server via REDE TCP/IP. — Arquivo “C:\devstudio\Melhorias\src\arquivo.txt””.
A aplicação usa fCreate() para criar o arquivo, verifica se houve falha na criação verificando o retorno da função fError(), faz um loop de 500 iterações, sorteia um número entre 100000 e 999999, e escreve no arquivo o número sorteado usando fWrite(), e antes de escrever determina se o número sorteado é múltiplo de 7 ou não, usando a função Mod().
Primeira Revisão
Logo nas primeiras linhas do arquivo, foram usados dois includes: “protheus.ch” e “totvs.ch”. Internamente, ambos são o mesmo arquivo. Na verdade, o include totvs.ch inclui internamente o arquivo protheus.ch. Foi criado um include com o nome de “totvs.ch” apenas para padronização de código. Logo, tanto faz qual deles for usado, precisamos somente de um deles.
O include fileio.ch possui algumas constantes que opcionalmente podem ser usadas pelas funções de baixo nível de arquivo, como a fCreate(), fOpen() e fSeek(). No caso do fonte acima, como nenhuma constante foi usada, fazer referência a este arquivo também é desnecessário.
Quanto a verificação de erro de criação de arquivo, normalmente verificamos se o resultado da função fCreate() é diferente de -1 (menos um), pois este é o valor retornado em caso de falha da função. Porém, não há nada de mais em usar a função fError() — desde que logo após a chamada da fCreate() — pois em caso de sucesso, fError() retorna 0 (zero).
Dento do laço for…next de 500 iterações, cada volta sorteia um número e verifica se ele é ou não múltiplo de 7. Quando queremos uma conversão de um valor numérico para string, sem espaços adicionais, podemos usar tranquilamente a expressão alltrim(str(nNumero)). De qualquer modo, no AdvPL temos uma função básica da linguagem que realiza exatamente este tipo de conversão, inclusive para outros tipos de dados de entrada, resultando em uma string — chama-se cValToChar(). A utilização dela neste cenário apenas economiza a chamada de funções — ao invés de duas, realizamos a conversão com apenas uma.
Na declaração de variáveis, foi usada corretamente a notação húngara — a primeira letra do nome da variável indica o seu tipo. Isto é “praxe” em AdvPL, ajuda bastante. E, outra coisa que ajuda é colocar nomes que tenham relação com o conteúdo. Olhando o fonte, sabemos que nNumero foi gerado por sorteio, e nNum é o número para verificar se o primeiro é múltiplo dele. Porém, ficaria mais “elegante” nomear nNum com, por exemplo “nDivTst” — divisor de teste.
Outros modos de fazer a mesma coisa
Existem duas formas de verificar se um número tem resto inteiro após uma divisão por outro. Podemos usar a função “mod()” ou o operador “%“, o resultado é o mesmo, ambos estão certos.
if ( mod(nNumero, nNum ) == 0) if ( ( nNumero % nNum ) == 0 )
Porém, existe também uma forma de redução de código, que eu particularmente acho feia, pois dificulta a legibilidade do código, e não há ganho NENHUM em desempenho, por exemplo:
if (mod(nNumero,nNum)) == 0 //dividendo e divisor respectivamente fWrite(nArquivo, "O numero < " + alltrim(str(nNumero)) + " > é divisível por " + alltrim(str(nNum)) + chr(13) + chr(10)) else fwrite(nArquivo, "O numero <" + alltrim(str(nNumero)) + "> não é divisível " + alltrim(str(nNum)) + chr(13) + chr(10)) Endif
O código acima poderia ser escrito de outra forma, com a mesma funcionalidade, porém com uma sintaxe totalmente diferente.
fWrite(nArquivo, ; "O numero <" + cValToChar(nNumero) + ">"+; IIF( mod(nNumero,nNum) == 0 , " é "," não é ") + ; "divisível por " + cValToChar(nNum) +chr(13)+chr(10) )
EXCEPCIONALMENTE NESSE EXEMPLO usando o recurso de quebrar linhas de código com ponto e vírgula no final, e tratando uma condição SIMPLES, com duas situações de retorno PEQUENO, escrever dessa forma não ficou tão “feio”. Usamos a função IIF() — ou “if em linha” — cujo comportamento é avaliar e retornar o segundo parâmetro informado caso a condição informada no primeiro argumento seja verdadeira, ou processar e retornar o terceiro parâmetro caso a condição informada seja falsa.
De qualquer modo, eu recomendo fortemente que esse tipo de redução seja usada com muita cautela, escrever um código todo rebuscado cheio de operadores e expressões “MegaZórdicas” não quer dizer que você é um bom programador. Escrever o IF — ELSE — ENDIF tradicional e colocar exatamente o que você quer fazer, mesmo que em alguns casos possa repetir um pouco de código, é preferível para dar LEGIBILIDADE ao código.
“Codifique sempre como se o cara que for dar manutenção seja um psicopata violento que sabe onde você mora.” – Martin Golding
Desempenho
Para realizar um teste de desempenho, eu removi as chamadas de MsgAlert(), e inseri uma contagem de tempo, declarando uma variável nTempo como “Local”, atribuindo nesta variável imediatamente antes da instrução “For” o retorno da função seconds(), e depois na instrução “Next”, eu mostro na tela uma MsgAlert() com o tempo que a função demorou para executar o loop de geração de números e de gravação em disco, com precisão de 3 casas decimais. O fonte após a revisão ficou desta forma:
#INCLUDE "PROTHEUS.CH" USER Function TESTE() Local nArquivo , nNumero Local nDiv := 7 Local i, nTempo nArquivo := fcreate("c:\Temp\arquivo.txt") If ferror() != 0 MsgAlert("ERRO GRAVANDO ARQUIVO, ERRO: " + cValToChar(ferror())) return EndIf nTempo := seconds() For i:=1 to 5000 nNumero = randomize(100000,999999) If (mod(nNumero,nDiv)) == 0 fWrite(nArquivo, "O numero < " + cValToChar(nNumero) + " > é divisível por " + cValToChar(nDiv) + chr(13) + chr(10)) Else fwrite(nArquivo, "O numero <" + cValToChar(nNumero) + "> não é divisível " + cValToChar(nDiv) + chr(13) + chr(10)) EndIf Next MsgAlert("Tempo Total = "+str(seconds()-nTempo,12,3)+' s.') fclose(nArquivo) Return
Para uma métrica de tempo mais apurada, 500 iterações era muito pouco. Aumentei para 5000 iterações, e no meu equipamento a execução deste código demora entre 500 e 600 milissegundos (0.5 a 0.6 segundos).
Pontos de Atenção
Após olhar o arquivo com as informações geradas, reparei em um comportamento interessante. Os números gerados randomicamente começavam em 100000 , mas nunca eram maiores que 132000. Após consultar a documentação da função Randomize() na TDN, reparei que ela têm uma observação sobre seu funcionamento.
A função Randomize(), trabalha com um intervalo interno de 32767 números, a partir do número inicial informado, inclusive se o número inicial for negativo.
Logo, embora eu possa especificar um número inicial e final, se a diferença entre ambos for maior que 32767, ela considera o limite final igual ao limite inicial + 32767 menos uma unidade, e não o limite informado. Neste caso, precisamos usar um outro algoritmo para chegar a um número tão grande — veremos como contornar isto mais para frente, em um próximo post.
Pontos de Melhoria
Sempre têm alguma coisinha que podemos fazer para melhorar o código, direta ou indiretamente. No caso do programa de exemplo acima, por uma mera questão circunstancial o arquivo a ser criado e que receberá os dados a serem gravados foi passado para a função contendo o que chamados de “path completo”, desde a unidade de disco, diretório e nome do arquivo com extensão. A maioria das funções de baixo nível de arquivo do AdvPL entende que, se um arquivo possui a letra da unidade de disco informada, este arquivo deve ser criado e manipulado pelo SmartClient.
Isto significa que, quando o Servidor roda a instrução fCreate(), ele envia uma mensagem ao SmartClient para que ele crie o arquivo, não importa se o SmartClient e o Protheus Server estão rodando na mesma máquina. E, cada instrução fWrite() executada faz o Protheus Server enviar uma linha com as informações para o SmartClient acrescentar no arquivo.
Os tempos de execução deste programa no meu ambiente, inserindo 5 mil linhas, estava em torno de 0.5 a 0.6 segundos. Vamos criar dentro da pasta do ambiente atual em uso (RootPath especificado na configuração de ambiente do appserver.ini) uma pasta chamada “Temp”, e no fonte AdvPL, vamos passar a criar este arquivo a partir do Servidor, mais precisamente dentro da pasta “Temp”.
nArquivo := fcreate("c:\Temp\arquivo.txt")
Deve ser trocado para
nArquivo := fcreate("\Temp\arquivo.txt")
Agora, executamos o programa novamente, e … surpresa, o tempo de execução CAIU para 0,170 a 0,200 segundos. Isso é quase três vezes mais rápido, e olhe que no meu ambiente o SmartClient e o Application Server estão no mesmo equipamento. Logo, este tempo a mais — que chamamos de “overhead” — envolve a aplicação montar a mensagem de gravação para o SmartClient, enviar pela rede, o SmartClient receber a mensagem, decodificar a mensagem, processar a mensagem e informar que a operação foi executada com sucesso. Esta diferença de desempenho é mais acentuada ainda quando o SmartClient e o Application Server estão realmente sendo executados em duas máquinas diferentes, com uma rede TCP entre elas.
É possível melhorar esse tempo no SmartClient ?
Sim, é possível sim, melhorar significativamente. Repare que cada iteração no fonte gera uma linha de aproximadamente 41 bytes quando o número não é divisível por 7, e 37 bytes quando é divisível. Um pacote único de rede TCP normalmente suporta até 1500 bytes. Logo, se ao invés de escrever 36 linhas de 41 bytes, uma para cada fWrite(), a gente acumulasse na memória essas 36 linhas em uma variável caractere, gerando uma string de aproximadamente 1476 bytes, e enviasse essa string na função fWrite(), podemos reduzir bem o tempo de transmissão de dados na rede, pois é mais rápido transmitir um pacote de 1476 bytes do que transmitir 36 pacotes de 41 bytes. Neste caso, o fonte ficaria assim:
USER Function TESTE() Local nArquivo , nNumero Local nNum := 7 Local i, nTempo Local nX Local cTemp := '' nArquivo := fcreate("c:\Temp\arquivo.txt") If ferror() != 0 msgalert ("ERRO GRAVANDO ARQUIVO, ERRO: " + str(ferror())) return EndIf nTempo := seconds() For i:=1 to 5000 nNumero = Randomize(100000,999999) If ( nNumero % nNum ) == 0 cTemp += "O numero <" + cValToChar(nNumero) + "> é divisível por " + cValToChar(nNum) + chr(13) + chr(10) Else cTemp += "O numero <" + cValToChar(nNumero) + "> não é divisível por " + cValToChar(nNum) + chr(13) + chr(10) EndIf IF i % 36 == 0 fWrite(nArquivo,cTemp) cTemp := '' Endif Next if !empty(cTemp) fWrite(nArquivo,cTemp) Endif conout("Tempo Total = "+str(seconds()-nTempo,12,3)+' s.') fclose(nArquivo) return
Conclusão
Para meu espanto, na máquina local (APPServer e SmartClient no mesmo equipamento), os tempos ficaram praticamente iguais, gravando o arquivo no path “c:\Temp” através do SmartClient, ou gravando dentro da pasta “\temp\” a partir do RootPath do ambiente no APPServer.
Espero que este post agregue conhecimento para quem está começando no AdvPL, e para quem já têm alguma experiência com a linguagem e com o ambiente. Afinal, conhecimento nunca é demais. Desejo a todos TERABYTES DE SUCESSO.
Agradecimentos
Agradeço o colaborador Hussyvel Ribeiro, que me concedeu um fonte de testes para usar neste post. Hussyvel, seja bem vindo ao AdvPL !!! 😀
Referências
Boa noite.
Muito legal o seu blog. Parabéns pelo conteúdo.
Sobre as boas práticas, uma coisa que eu particularmente não gosto é “return” no meio da função.
Isso dificulta o tratamento que se queira fazer ao sair da função (RestArea por exemplo), tendo que repetir esse tratamento em todo lugar que tiver return.
Isso se agrava se a função tiver controle de transação. Já vi fontes com “return” no meio da transação, fazendo com que a rotina saísse da função sem passar pelo “End Transaction”. Imagine a bagunça.
E outra coisa que acho que dificulta um pouco a manutenção de fonte é a criação de variáveis com uma letra só. Fazer a pesquisa dessa variável no fonte é bem difícil.
CurtirCurtido por 1 pessoa
Return no meio de código, quando o código é curto e específico, até ajuda. Mas em rotinas maiores, é bom evitar …rs… Normalmente usamos variáveis com uma letra, normalmente i ou n , em iterações for .. next , onde a variável é somente um iterador de contagem, sem maior significado para o prograna em si. Porém, quando usamos o contador em processamentos posicionais ou em sub-rotinas, fica bem mais legível usar um nome mais completo, relacionado ao uso e objetivo da variável. Em breve sai mais um “Boas práticas” e eu vou abordar os pontos que você sugeriu 😀
Abraços
CurtirCurtir
[…] post Boas Práticas de Programação – Código Simples, Resultados Rápidos, eu mencionei um limite operacional da função Randomize() do AdvPL. Caso a diferença entre o […]
CurtirCurtir
Júlio, Parabéns pelo conteúdo.
CurtirCurtido por 1 pessoa
Obrigado campeão !!!! 😀
CurtirCurtir
Trabalho na TOTVS e estou aprendendo a programar e algumas pessoas que conheço tem o Júlio como referência, realmente este foi um belo post, parabéns e obrigado por compartilhar o teu conhecimento conosco!
CurtirCurtido por 1 pessoa
😉 Tks man !!!!
CurtirCurtir