28 de abril de 2014

Loops encadeados: a maldição

Fala pessoas!

Olha, teve 1 acesso desde o último post. Será que alguém errou algum endereço e caiu aqui?

Bom, hoje vou falar de uma coisa simples e besta, mas que tenho visto muitas vezes essa coisa simples e besta se repetindo da forma mais dantesca o possível: Loop encadeado (ou vulgo loop dentro de loop).

Por conceito, o loop é um processo que navega por todos os registros de uma tabela interna. Simples não? Conceitualmente sim e praticamente também. Então qual o problema? O problema está na necessidade de analisar os dados que serão processados antes da construção do loop.

Vejam esses dois exemplos:

  1. um loop encadeado com 10 registros na primeira tabela e 20 na segunda tabela. A execução deste loop encadeado resultaria em 200 processos (ou 125 na base 13, certo?).

  2. um loop encadeado de gente grande (mas não tão), com 96.701 registros na primeira tabela e 1.504.265 na segunda. A execução deste belo loop encadeado resultaria em simples 145.463.929.765 processos. Imagine que legal fazer um loop encadeado da forma mais hedionda o possível e se deparar com uma quantidade de registros dessas. Será muito legal justificar que seu report vai demorar só 2 anos, 7 meses, 19 dias, 21 horas e 42 minutos para terminar.
Mas com certeza, tem alguém pensando que isso é puro exagero e que mesmo um loop encadeado mais tosco o possível não pode demorar tanto assim, que ele sempre fez assim e sempre funcionou. No exemplo abaixo eu fiz um teste de tempo de execução para o exemplo 2 (olha que legal, o primeiro tempo apresentado é o do loop tosco):



Então, vamos ver os tipos mais comuns de loops encadeados, do pior para o melhor:

1 - Loop preguiça - para pouco, realmente poucos dados
 Esse é o tipo de loop mais simples e que tem a pior performance possível. É somente um loop com outro interno usando um IF para comparação dos dados. Esse caso representa o primeiro item da imagem de medição de tempo.

UNASSIGN: <gf_ekko>.
LOOP AT gt_ekko
ASSIGNING <gf_ekko>.
    UNASSIGN: <gf_ekpo>.
    LOOP AT gt_ekpo
    ASSIGNING <gf_ekpo>.
        IF <gf_ekko>-ebeln = <gf_ekpo>-ebeln.
            gs_lista-ebeln = <gf_ekko>-ebeln.
            gs_lista-qtd = 1.
            COLLECT gs_lista INTO gt_lista.
        ENDIF.
    ENDLOOP.
ENDLOOP.

2 - Loop segundo grau - para poucos ou um pouco mais de poucos registros. Nesse caso, o segundo loop contém um WHERE, o que permite uma localização rápida do registro inicial que contém os dados necessários. Esse caso representa o segundo item da imagem de medição de tempo.

UNASSIGN: <gf_ekko>.
LOOP AT gt_ekko
ASSIGNING <gf_ekko>.
    UNASSIGN: <gf_ekpo>.
    LOOP AT gt_ekpo
    ASSIGNING <gf_ekpo>
      WHERE ebeln = <gf_ekko>-ebeln.
        IF <gf_ekko>-ebeln = <gf_ekpo>-ebeln.
            gs_lista-ebeln = <gf_ekko>-ebeln.
            gs_lista-qtd = 1.
            COLLECT gs_lista INTO gt_lista.
        ENDIF.
    ENDLOOP.
ENDLOOP.

3 - Loop plus plus - para poucos ou muitos dados. Este processo usa no início do segundo loop um read table com a opção transporting no fields, o que permitem um posicionamento inicial do dados desejado e transporta essa posição para o segundo loop usando a opção from sy-tabix. Esse processo se torna mais rápido porque usa o posicionamento do índice dos registros para localizar as informações e não propriamente os dados dos registros. Esse caso representa o terceiro item da imagem de medição de tempo.

UNASSIGN: <gf_ekko>.
LOOP AT gt_ekko
ASSIGNING <gf_ekko>.

    READ TABLE gt_ekpo
    TRANSPORTING NO FIELDS
      WITH KEY ebeln = <gf_ekko>-ebeln
        BINARY SEARCH.

    IF sy-subrc EQ 0.
        UNASSIGN: <gf_ekpo>.
        LOOP AT gt_ekpo
        ASSIGNING <gf_ekpo>
          FROM sy-tabix.
            IF <gf_ekko>-ebeln = <gf_ekpo>-ebeln.
                gs_lista-ebeln = <gf_ekko>-ebeln.
                gs_lista-qtd = 1.
                COLLECT gs_lista INTO gt_lista.
            ELSE.
                EXIT.
            ENDIF.
        ENDLOOP.
    ENDIF.
ENDLOOP.

Como pode ser notado, o loop mais elaborado, que permite uma melhor performance em qualquer dos casos, não é tão mais complexo ou trabalhoso para ser criado. Agora a escolha de utilização fica a gosto de cada um.

Seguem mais algumas dicas para sempre serem usadas para se trabalhar com grandes quantidade de dados:
  1. SORT: sempre ordene os seus dados para permitir uma melhor performance

  2. BINARY SEARCH: sempre use o binary search no read table. Ele permite uma melhor performance para obter a informação desejada. Obs: sempre ordene os dados baseado nos campos que serão usados como chave no read table, pois caso os dados estejam usando outra ordem e o comando read table esteja usando o binary search, o retorno poderá ser informado como não encontrado, por mais que a informação exista. Vejam o conceito da busca binária aqui (é bem interessante e vale a pena o conhecimento e entendimento).

Antes que eu esqueça, vocês podem reparar que eu usei FIELD-SYMBOLS em todos os exemplo, ao invés de WORKAREAS ou HEADERS. Isso por um simples motivos: porque workareas e headers sempre (sempre) apresentam performance menor, para qualquer situação. Depois monto um post explicando o porque usar field-symbols.

Caso se interessem, segue abaixo os arquivos dos reports utilizados para medição: fontes.

Até mais e obrigado pelos peixes.

3 de abril de 2014

FOR ALL ENTRIES: pode até ser seu colega, mas cuidado com ele

Fala pessoas (olha, um arbusto seco rolando ao vento...)

Vamos iniciar nossa série sobre performance, que terá um total de partes equivalente ao quadrado do grau de inclinação do rabo do hipopótamo.

Não vou abordar os conceitos iniciais de performance, como selecionar somente os campos necessários no select, não usar tabela com cabeçalho, não contratar aborígenes de Papua para ficar imputando NFe e outros. Mas caso achem necessário, deixe comentários no post solicitando os conceitos básicos de performance (ou seja, como não tem ninguém lendo, não vou ter que fazer).

Inicialmente, vamos falar de FOR ALL ENTRIES.
Esse prezado e dileto comando pode até ser nosso colega, mas não devemos confiar nele para cuidar da nossa coleção de 151 pokémons.

Tenho dois casos para demonstrar que nem sempre a utilização do FOR ALL ENTRIES (trataremos de FAE daqui em diante para economizar letras, pois meu estoque de "L" está baixo) é a mais indicada. Para ambos os exemplos eu fiz duas comparações de tempos, uma com a utilização de FAE e outra sem, sempre usando os mesmos dados.

No primeiro caso, temos a utilização de FAE para grande quantidade de dados. Quando há uma quantidade de dados baixa, o processo de FAE é um processo benéfico que garante acesso único a base de dados, mas quando há uma grande quantidade de dados, a execução do FAE se torna mais lenta que o processo de select individual, pois há mais informação para ser carregada para memória (e quanto maior a carga de dados para a memória, maior a alocação física e menor a performance, progressivamente):


Nesse caso, para o período de 2014, como há poucos registros, a obtenção de dados com FAE é realizada mais rápida, porém, no período de 2011 a obtenção com FAE é mais lenta que um select dentro de um loop. Ok, eu sei que não se usa select dentro de loop, mas quando não há como reduzir a massa de dados ou segmentar o processo, é preferível isso à um DUMP.

No segundo, temos a utilização de FAE derivado de um select sem a utilização de campos chaves (sejam PK, FK ou IDX). Nesse caso a utilização do FAE se torna um pouco maior do que sem o mesmo:


Bom, vale lembrar que para usar o FAE, SEMPRE, repito SEMPRE, deve ser verificado se a tabela de referencia possui conteúdo, pois caso a tabela esteja vazia isso forçará o select a trazer absolutamente todos os registros da tabela acessada..

Caso se interessem, segue abaixo os arquivos dos reports utilizados para medição: fontes.

Até mais e obrigado pelos peixes.

1 de abril de 2014

Antes de falarmos de performance

Fala pessoal (cri cri cri)...

Estive pensando (sim, as vezes faço isso), e acho interessante abordarmos outro assunto antes de falarmos de performance. Vamos falar de padronização!

Quantas vezes nós (abaps amaldiçoados) nos deparamos com as mais diversas, e diga-se criativa às vezes, formas de declaração de itens no código fonte. Entendo que você pode justificar que como o desenvolvimento é seu você desenvolver como quer, mas imagine que um outro abap irá realizar uma manutenção no seu código, ele pode ficar com dúvida sobre qual a finalidade de tabela "tb_table" ou ainda não saber direito para que server a variável "baleia".

Além do mais, é importante se acostumar a desenvolver baseado em padrões de nomenclaturas, pois muitas empresas possui um workbook interno (documento empresarial contendo a descrição, normalização e metodologia de todos os padrões de programação que você deve utilizar). Então porque não ter um workbook próprio e usa-lo no dia a dia?

Inicialmente você não precisa criar todos os padrões possíveis a imagináveis para começar a utilizar seu workbook. Comece pelos itens mais simples e do cotidiano e vá evoluindo aos poucos. Para criar uma padronização, pense em duas coisas: clareza e simplicidade (lembre-se que outras pessoas precisam identificar de forma rápida para qual objetivo existe o objeto)! Veja alguns exemplos de padronização que eu uso:

gt_notas
G = declaração global - T = Tabela interna - NOTAS = conteúdo existente

ls_material
L = declaração local - S = estrutura interna (workarea) - MATERIAL = conteúdo existente

lv_tabix
L = declaração local - V = variável - TABIX =  número do loop atual

Esses são apenas exemplos simples que eu uso no cotidiano. A padronização pode ser criada do modo que desejado, desde que seja interpretável de forma simples por outras pessoas (não usem runas Angerthas ou klingon para isso).

No início você irá se atrapalhar um pouco para lembrar o seu novo método de padronização, mas depois de pouco tempo isso se tornará nativo.

Até mais e obrigado pelos peixes.