Acelerando o AdvPL – Parte 01

Introdução

Como já foi visto e frisado nos tópicos anteriores sobre escalabilidade e desempenho, a premissa de “fazer mais com menos” é uma boa pratica em qualquer linguagem de programação. Existem várias formas de escrever um algoritmo, e todas serão corretas se chegarem ao resultado esperado. Porém, para cada caso existe a maneira “ótima” de escrever o algoritmo, usando a lógica e as funções da linguagem AdvPL da forma adequada para chegarmos ao mesmo resultado usando uma quantidade menor de recursos, o que normalmente pode ser traduzido em economia de tempo.

Introdução aos Estudos de Caso

Cada questão requer um estudo de caso, e normalmente alguns testes. Normalmente uma aplicação AdvPL possui um código que engloba I/O (leitura ou gravação de dados do disco ou do Banco de Dados), e o processamento destes dados pelo programa. Em determinados momentos existe um uso maior de disco, em outros um uso maior de CPU, ou de rede, ou do Banco de Dados.

A abordagem acertiva acerca do desempenho de uma rotina é identificar quais são as operações que correspondem ao consumo de tempo de pelo menos 70 % da rotina (os maiores tempos), e verificar se estas etapas de processo podem ser melhoradas. Devemos sempre ter em mente a lei de Amdahl: “O ganho de desempenho que pode ser obtido melhorando uma determinada parte do sistema é limitado pela fração de tempo que essa parte é utilizada pelo sistema durante a sua operação – Gene Amdahl”. Logo, quanto maior forem as optimizações nas partes do código relevantes no processamento, mais serão percebidos os ganhos da otimização.

Estudo de caso – Processamento de arquivo TXT

Recentemente analizei um caso de processamento, onde um programa AdvPL precisava fazer a leitura de um arquivo TXT para uma integração, onde o arquivo poderia conter linhas maiores que 1 KB. Normalmente utilizamos as funções ft_fuse() e ft_freadln() para ler linhas de um arquivo TXT, porém estas funções são limitadas a ler linhas de tamanho máximo de 1 KB. E, para cada linha lida, também era necessário identificar as colunas de dados, que eram separadas por vírgula (formato CSV).

A parte de leitura de dados foi resolvida de forma simples, usando fOpen() com fREad() ou fReadStr(), lendo um pedaço do arquivo maior para a memória, e verificando neste pedaço lido se havia uma quebra de linha (CRLF / sequência de caracteres chr(13) + Chr(10) ).

Uma vez lida a linha, ela era submetida para uma função que separava os valores das colunas encontradas considerando a virgula como separador, usando uma função mais ou menos assim:

STATIC Function MYStrTok1( cBuffer, cDelimit )
Local cTexto := ''
Local nX := 0
Local aLinha := {}
If Len(cBuffer) > 0
  cTexto := ""
  For nX := 1 to Len(cBuffer)
    If Substr(cBuffer,nX,1) == cDelimit
      aAdd(aLinha, cTexto )
      cTexto := "" 
    Else
      cTexto += Substr(cBuffer,nX,1)
    Endif
  Next nX
  If !Empty(cTexto)
    aAdd(aLinha, cTexto )
  Else
    aAdd(aLinha, " " )
  Endif
Endif
Return( aLinha )

A função é relativamente simples. Criamos uma variável de uso temporária chamada cTexto, inicializada com uma string vazia (“”), e verificamos cada posição da string informada como parâmetro. Cada valor diferente do separador é inserido em cTexto, e no momento que o separador é encontrado, o valor de cTexto é inserido no array de retorno, e cTexto é inicializado novamente para uma string vazia.

Ao fazer um teste com um arquivo texto de 200 MB, contendo 50 mil linhas de texto, onde cada linha possuía 11 palavras de tamanho variável (entre 150 e 250 caracteres) separadas por vírgula. Considerando apenas o tempo de identificação dos dados e separação das colunas, a rotina demorava aproximadamente 55 segundos para processar o arquivo inteiro, com 550 mil palavras. A lógica da rotina não está errada, ela chega ao resultado esperado em 55 segundos.

Se dividirmos a quantidade de palavras processadas pelo tempo de processamento ( 550000 / 55 ), chegamos ao número estrelar de 10 mil palavras por segundo. Isso e bem rápido, certo ?

Analisando com uma lupa

Vamos olhar mais de perto a função lida com as informações: Cada letra da string é extraída usando a função substr(), atuando na linha inteira, caractere por caractere. Cada caractere é comparado com o delimitador, e caso seja igual, o conteúdo de cTexto é acrescentado no array e a variável é limpa, caso contrário o mesmo caractere é extraído novamente, e acrescentado na variável cTexto, que vai crescendo na memoria um caractere por vez.

Refatorando e otimizando

E, se ao invés de extrairmos os caracteres um a um, usarmos uma função que busca dentro da string a próxima ocorrência do delimitador (“,”), e caso seja encontrado, nós acrescentamos no array o primeiro pedaço da string até a posição encontrada, e removemos este pedaço do texto já identificado, inclusive com a vírgula, da variável cBuffer… O fonte ficaria assim:

STATIC Function MYStrTok2( cBuffer, cDelimit )
Local nX := 0
Local aLinha := {}
If Len(cBuffer) > 0
  while ( ( nX := At(cDelimit,cBuffer) ) > 0 )
    aAdd(aLinha, left(cBuffer,nX-1) )
    cBuffer := substr(cBuffer,nX+1)
  Enddo
  If !Empty(cBuffer)
    aAdd(aLinha, cBuffer )
  Endif
Endif
Return( aLinha )

Na instrução while, usamos a variável nX, fazendo uma atribuição em linha do resultado da função AT(), que retorna a primeira posição de cBuffer que contém o delimitador cDelimit, ou 0 caso o delimitador não seja encontrado. Enquanto houver um delimitador encontrado, acrescentamos no array aLinha a parte esquerda da string, da primeira posição até a posição do delimitador – 1 ( desconsiderando o delimitador ), e na linha de baixo reatribuimos o valor de cBuffer, usando a função Substr(), considerando como inicio do novo buffer o próximo caractere após o delimitador até o final da String. No final do loop, caso o buffer não esteja vazio, a linha não terminou com um delimitador, então o que sobrou no buffer é acrescentado no array de retorno.

Testando a velocidade

Repetindo o mesmo teste, com o mesmo arquivo, mas usando a nova função, o tempo cair de 55 segundos, para 4,5 s. (quatro segundos e meio). Com a função original, 10 mil palavras eram identificadas por segundo. Agora, com a nova função, aproximadamente 122 mil palavras são identificadas por segundo 😀

Isso apresentou um ganho de aproximadamente 12x de desempenho.

Em se tratando de uma rotina de transformação de dados, onde a entrada é um TXT, e a saída é um XML, por exemplo, considerando que o tempo de escrita dos dados identificados em um arquivo no disco gaste uns 20 segundos, a rotina original demorava 55 + 20 = 75 segundos, pouco mais de um minuto. Ao otimizar a identificação de palavras, a nova rotina passa a demorar 5 + 20 = 25 segundos, pouco menos que meio minuto.

De 75 segundos, esta etapa de processo foi reduzida para 25 segundos, apresentando um ganho de 3x, ou executando em apenas 1/3 do tempo da rotina original. Isto sim reflete em um ganho de tempo sensível e perceptível na operação.

A relatividade de Amdahl

Considerando apenas a etapa de processo de ler um TXT e gravar um XML, onde ler e identificar as palavras levava 55 segundos, o ganho nesta etapa foi de 12 vezes, ou seja, passou a ser executada em uma fração de 1/12 do tempo original. Quando consideramos o processo inteiro, inclusive a gravação — onde não houve nenhuma otimização — o ganho de desempenho foi de 3 vezes. Se este processamento tivesse mais etapas, que demoram mais tempo, o ganho continua existindo, mas ele não vai ser facilmente percebido, pois se as etapas posteriores representam mais 20 minutos de processamento, e você otimizou apenas um pedaço que consumia 1 minuto, no final das contas o tempo total do processo caiu de 20 para 19 minutos. A otimização realizada agiu sobre uma fatia de tempo muito curta em relação ao processo inteiro (apenas 5% do tempo total do processo foi otimizado).

Agradecimentos

Meus agradecimentos ao nobre leitor e desenvolvedor Eurai Rapelli, que me procurou para tirar uma dúvida, e contribuiu com um caso de uso interessante e sob medida para esta publicação 😀

Conclusão

Quanto mais etapas de processo você conseguir otimizar, mais perceptível será o ganho, ou em tempo, ou em carga do equipamento. E é preciso tomar cuidado com o excesso de otimizações, pois elas tendem a especializar as rotinas, e normalmente elas ficam mais complexas, e isso pode aumentar o custo de manutenção. Por isso, o foco em desempenho e performance deve ser direcionado para os casos onde realmente ele é necessário.

Espero que vocês gostem e tirem bom proveito deste post, comentem, tirem suas dúvidas, e para sugerir um assunto a ser abordado no Blog, é só mandar um e-mail para “siga0984@gmail.com”, com o assunto “Sugestão para o Blog” 😀

Até o próximo post, pessoal !!!

11 respostas em “Acelerando o AdvPL – Parte 01

  1. Mto Obrigadoo Julio..
    Realmente essa foi ótima.
    Sempre me preocupo com desempenho/velocidade das funções e Legibilidade da mesma. Procurando otimizar o máximo possível.
    Essas informações/testes trocadas nos últimos dias me agregou mto conhecimento.

    Tive uma dúvida aqui, não lembro se já li.

    Tem diferença de tempo entre chamar um método ou uma função ?

    Curtido por 1 pessoa

    • Opa, magina ..rs.. Quanto a diferença de tempo de chamada de funções e métodos, fiz este teste a algum tempo, e a diferença é insignificante. Voce precisa fazer centenas de milhões de chamadas para medir um décimo de segundo de diferença. 😀

      Curtir

  2. Muito interessante. Adorei a abordagem. Ao ler o primeiro exemplo, imaginei uma otimização usando At() também, o que se revelou verdadeira hehehe…

    Parecia minhas aulas de “Complexidade de algoritmos” na faculdade 😉

    Curtido por 1 pessoa

  3. Interessante o artigo, Julio…

    Confesso que estou mais focado na solução do problema e raramente me preocupo com o desempenho.

    O artigo me fez pensar sobre isso…

    Bom… Fiquei com uma dúvida: as funções “separa” e “StrTokArr” também estão otimizadas ?

    E muito obrigado pelo blog.
    []´s
    Marcelo Vicente

    Curtido por 1 pessoa

    • Olá Marcelo, boa tarde,

      A função Separa() é escrita em AdvPL, é uma função do FrameWork. Ela foi escrita a muito tempo, antes de usarmos o Protheus como plataforma de desenvolvimento. A função StrTokArr foi escrita em C++ dentro do servidor justamente para substituir a função Separa(), porém ela não contemplou o comportamento original da função Separa(), pois a StrTokArr() desconsidera elementos vazios entre os separadores, e somente considera o primeiro caractere do Token para fazer a separação. A funçao StrTokarr2() foi criada recentemente em C++ dentro do TOTVS Appication Server para ser mais rápida que a StrTokArr(), e contemplar os comportamentos da função Separa(). A função mais rápida de separação de strings por Token e que abrange os comportamentos desejáveis é a StrTokArr2(), documentada na TDN no link http://tdn.totvs.com/display/tec/strtokarr2 , porem disponível apenas a partir do Protheus 11, usando a Build 7.00.131227A com data de geração superior a 08/09/2014.

      Abraços

      Júlio Wittwer
      siga0984@gmail.com
      https://siga0984.wordpress.com/
      https://www.facebook.com/siga0984

      Curtir

  4. Olá Julio, achei seu site agora e foi uma descoberta muito feliz.
    Estou iniciando nesse mundo do ADVPL mas sou programador em outras linguegens já a algum tempo, me preocupando muito com um código rápido.
    Serei seu leitor assíduo.

    Curtido por 1 pessoa

  5. Pingback: Acelerando o AdvPL – Parte 02 | Tudo em ADVPl

  6. Boa tarde Julio,

    Para esses caso de leitura de TXT, quando entrei na TOTVS em 2000, me deparei com esse problema de leitura byte a byte do buffer lido do arquivo até encontrar o CR+LF. Então desenvolvi na época a função fReadLn, que faz a leitura de uma linha que possua como separadores CR+LF e o tamanho máximo de uma linha é definido por parâmetro e ela retorna .F. quando chegar no final de arquivo (CHR(26)). Está no RPO padrão. O restante continua igual, fOpen e fClose.

    Abraço.

    Curtido por 1 pessoa

    • Perfeita 😀 Rapaz, o que de gente que precisa saber que algo assim existe …rs… vou ver a quem de direito deve ser solicitada a documentação desta função 😉

      Parabéns cara, e obrigado !!

      Curtir

Deixe um comentário