 |
| Bellacosa Mainframe apresenta cobol recursivo parte II sec c |
☕ Um Café no Bellacosa Mainframe
COBOL Recursivo — Muito Além do Fatorial (Parte 2C)
Performance, Tail Recursion, Debugging, Language Environment e os Segredos que Todo Programador Mainframe Deveria Conhecer
"A pergunta que todo Programador COBOL Sênior faz não é 'a recursão funciona?', mas sim 'quanto ela custa, quando vale a pena e como o Enterprise COBOL administra tudo isso por baixo dos panos?'"
Introdução
Chegamos à última parte da nossa jornada.
Na Parte 1 entendemos o conceito.
Na Parte 2A acompanhamos a pilha crescendo e diminuindo.
Na Parte 2B vimos onde a recursividade realmente faz sentido.
Agora vamos responder às perguntas que normalmente aparecem em entrevistas técnicas e discussões entre Programadores Sêniores.
A recursão é lenta?
Quanto de memória ela consome?
O compilador otimiza chamadas recursivas?
Existe Tail Recursion no Enterprise COBOL?
Como depurar uma rotina recursiva?
O que acontece em um dump?
Como o Language Environment administra tudo isso?
Prepare mais um café.
Agora vamos olhar por dentro do motor do Enterprise COBOL.
O verdadeiro custo de uma chamada recursiva
Imagine uma rotina extremamente simples.
ROTINA A
↓
ROTINA A
↓
ROTINA A
↓
ROTINA A
Muitos imaginam que apenas uma instrução CALL é executada.
Na realidade ocorre muito mais.
Cada chamada exige que o sistema preserve o contexto da execução atual antes de iniciar a próxima.
Normalmente isso envolve:
salvar registradores utilizados;
armazenar o endereço de retorno;
reservar espaço para variáveis locais;
preparar parâmetros;
criar um novo frame de execução;
transferir o controle para a nova ativação.
Quando essa rotina retorna, todo esse processo acontece novamente, porém na ordem inversa.
Quanto custa isso?
Cada chamada possui um custo fixo.
Imagine uma recursão com profundidade 500.
Teremos aproximadamente:
Enquanto isso um simples:
PERFORM VARYING
reutiliza praticamente o mesmo ambiente durante toda a execução.
É por isso que loops costumam ser mais rápidos.
O PERFORM continua sendo o campeão
Imagine um processamento de um arquivo VSAM.
100 milhões de registros
Qual solução utilizar?
Recursão?
Jamais.
O correto continua sendo.
PERFORM UNTIL EOF
Por quê?
Porque o problema é linear.
Cada registro é independente.
Não existe árvore.
Não existe hierarquia.
Não existe motivo para criar milhares de novos contextos de execução.
Quando a recursão vence
Agora imagine.
Empresa
↓
Departamento
↓
Subdepartamento
↓
Equipe
↓
Funcionário
Aqui o problema possui profundidade variável.
Não sabemos quantos níveis existirão.
A estrutura muda constantemente.
Nesse cenário a recursividade pode produzir um código muito menor, mais legível e muito mais fácil de manter.
Complexidade não é desempenho
Existe uma confusão bastante comum.
Alguns desenvolvedores acreditam que:
"Se um algoritmo é recursivo então ele é lento."
Não.
O que determina o desempenho é o algoritmo.
QuickSort continua sendo extremamente eficiente.
MergeSort também.
DFS também.
A recursão é apenas uma técnica utilizada para implementar esses algoritmos.
A profundidade da pilha
Imagine.
Nível 1
↓
Nível 2
↓
Nível 3
↓
...
↓
Nível 500
Cada nível ocupa memória.
Quanto maior a profundidade.
Maior o consumo.
Por isso uma pergunta importante é:
Qual a profundidade máxima esperada?
Stack Overflow
Toda pilha possui limite.
Se o algoritmo continuar chamando a si próprio indefinidamente.
Chegará um momento em que não haverá espaço suficiente.
Resultado.
Stack Overflow.
Em ambientes Enterprise COBOL isso normalmente se manifesta como falha de execução provocada pelo esgotamento da pilha ou da região disponível para o processo.
Como evitar?
Existem algumas regras simples.
Sempre possuir:
caso base;
redução do problema;
validação da entrada;
profundidade conhecida.
Nunca confiar que os dados "sempre estarão corretos".
Um pequeno erro pode ser catastrófico
Imagine.
PROCESSA(100)
↓
PROCESSA(100)
↓
PROCESSA(100)
Percebe o problema?
O valor nunca muda.
O caso base jamais será alcançado.
O algoritmo continuará chamando a si próprio até consumir toda a pilha.
Esse é um dos bugs mais perigosos em programas recursivos.
Tail Recursion
Agora chegamos a um assunto que raramente aparece em livros de COBOL.
Considere.
ROTINA
↓
chama novamente
↓
retorna imediatamente
Não existe mais nada para fazer após o retorno.
Esse padrão recebe o nome de Tail Recursion.
Por que ela é especial?
Alguns compiladores conseguem transformar automaticamente esse tipo de recursão em um simples loop.
Resultado.
A pilha praticamente deixa de crescer.
Essa otimização é conhecida como Tail Call Optimization (TCO).
E o Enterprise COBOL?
O Enterprise COBOL não é conhecido por realizar uma otimização geral de Tail Call equivalente à encontrada em linguagens como Scheme ou algumas implementações modernas de C/C++. Em outras palavras, não é seguro assumir que uma chamada recursiva em posição de cauda será convertida automaticamente em um laço.
A recomendação prática para aplicações corporativas continua sendo:
prefira PERFORM.
Sempre que escrever uma rotina recursiva pensando em desempenho, consulte a documentação da versão específica do compilador e valide o comportamento com testes e medições.
Debugging
Aqui começa uma das maiores dificuldades.
Imagine um breakpoint.
Você observa.
LS-NUMERO = 4
Continua executando.
Agora.
LS-NUMERO = 3
Depois.
LS-NUMERO = 2
Depois.
LS-NUMERO = 1
O iniciante acredita que a variável está sendo alterada.
Na realidade.
Você está olhando ativações diferentes.
Cada uma possui sua própria Local-Storage.
Esse detalhe costuma confundir quem está depurando um programa recursivo pela primeira vez.
Como depurar corretamente
A primeira dica.
Sempre descubra:
"Em qual nível da pilha estou?"
Depois.
Observe:
parâmetros;
variáveis locais;
valor de retorno.
Nunca apenas o conteúdo de uma variável.
O Dump
Quando ocorre um abend.
O dump costuma revelar algo parecido.
ROTINA
↓
ROTINA
↓
ROTINA
↓
ROTINA
↓
ROTINA
Centenas de vezes.
Isso normalmente indica:
ausência de caso base;
dados inválidos;
profundidade inesperada.
Aprender a reconhecer esse padrão economiza muitas horas de investigação.
Language Environment (LE)
Poucos Programadores Júnior conhecem o LE.
Mas praticamente todo programa Enterprise COBOL moderno depende dele.
O Language Environment é responsável por diversos serviços de tempo de execução, incluindo a organização do ambiente necessário para chamadas de programas, tratamento de exceções, gerenciamento de pilha e integração entre linguagens.
Quando um programa recursivo cria novas ativações, existe uma infraestrutura por trás garantindo que cada contexto seja preservado corretamente.
Sem esse ambiente de execução seria muito mais difícil oferecer suporte consistente a recursos modernos do Enterprise COBOL.
O papel do LOCAL-STORAGE revisitado
Depois de tudo que vimos.
Fica fácil entender.
Cada frame precisa de suas próprias variáveis.
É exatamente isso que Local-Storage oferece.
Visualmente.
+-------------------+
Frame 1
LOCAL-STORAGE
+-------------------+
Frame 2
LOCAL-STORAGE
+-------------------+
Frame 3
LOCAL-STORAGE
+-------------------+
Cada chamada possui sua própria área.
Nenhuma interfere na outra.
E a Working-Storage?
Continua existindo.
Mas pertence ao programa.
Não à chamada.
Visualmente.
WORKING-STORAGE
↓
Frame 1
↓
Frame 2
↓
Frame 3
Todos enxergam a mesma área.
Por isso ela deve armazenar apenas informações realmente compartilhadas.
O impacto em aplicações CICS
Embora o CICS suporte programas escritos em Enterprise COBOL, nem todo programa é um bom candidato à recursão.
Em aplicações OLTP, normalmente buscamos:
Por isso, algoritmos profundamente recursivos raramente aparecem na lógica de transações de alta frequência.
Quando uma solução recursiva for necessária, ela deve ser cuidadosamente analisada quanto à profundidade máxima, uso de memória e comportamento sob carga.
Recursão e paralelismo
Existe outro ponto interessante.
Recursividade não significa paralelismo.
Nem concorrência.
São conceitos completamente diferentes.
É possível possuir:
algoritmo recursivo sequencial;
algoritmo iterativo paralelo;
algoritmo recursivo paralelo.
Não confunda os conceitos.
Quando um Sênior escolhe recursão?
Normalmente quando observa:
✓ estrutura hierárquica
✓ profundidade variável
✓ código muito mais simples
✓ facilidade de manutenção
✓ menor complexidade lógica
Ou seja.
A decisão raramente é baseada apenas em velocidade.
Checklist Bellacosa ☕
Antes de escrever um algoritmo recursivo, faça estas perguntas:
Existe um caso base claramente definido?
Cada chamada aproxima o problema desse caso base?
A profundidade máxima é conhecida ou razoavelmente limitada?
Uma solução iterativa seria significativamente mais simples?
As variáveis específicas de cada chamada estão em LOCAL-STORAGE?
O algoritmo foi testado com entradas extremas?
O comportamento em erro e em dumps é compreendido pela equipe?
Se alguma resposta for "não", vale a pena revisar o projeto antes de seguir.
Truques de Programador Mainframe
Algumas boas práticas que ajudam muito.
✔ Nunca misture lógica recursiva com variáveis globais desnecessárias.
✔ Documente claramente qual é o caso base.
✔ Comente qual parâmetro reduz o problema.
✔ Sempre teste entradas:
mínimas;
máximas;
inválidas.
✔ Desenhe a árvore de chamadas antes de codificar.
✔ Não escolha recursão apenas porque o código fica "bonito".
Código elegante que produz um abend continua sendo um programa ruim.
Easter Egg Bellacosa ☕
Existe uma curiosidade interessante.
Muitos Programadores Mainframe trabalham vinte ou trinta anos sem escrever um único algoritmo recursivo.
Mesmo assim.
Os melhores profissionais costumam compreender perfeitamente:
Call Stack;
Frames;
Local-Storage;
Language Environment;
Endereços de retorno;
Passagem de parâmetros.
Por quê?
Porque todos esses conceitos aparecem diariamente em dumps, depuração, integração com C, Assembler, APIs, LE e análise de problemas complexos.
Ou seja.
Você pode nunca escrever um QuickSort recursivo.
Mas provavelmente utilizará o conhecimento adquirido estudando recursão durante toda sua carreira.
Uma reflexão final
Existe um velho ditado entre arquitetos de software.
"Toda abstração tem um custo."
A recursividade é uma abstração poderosa.
Ela reduz dezenas de linhas de código para poucas chamadas elegantes.
Em troca.
Consome pilha.
Cria novos contextos.
Exige planejamento.
O Programador Júnior pergunta:
"Posso usar?"
O Programador Pleno pergunta:
"Vale a pena usar?"
O Programador Sênior pergunta:
"Qual é o impacto dessa decisão daqui a cinco anos?"
É essa mudança de perspectiva que diferencia quem apenas domina a sintaxe de quem realmente compreende engenharia de software.
Conclusão da Série
Se você acompanhou esta série desde a Parte 1, provavelmente percebeu que o objetivo nunca foi ensinar apenas um algoritmo de fatorial.
Nosso verdadeiro objetivo foi mostrar que a recursividade é uma porta de entrada para compreender a arquitetura do Enterprise COBOL.
Ao estudar esse tema, aprendemos sobre:
Call Stack;
Frames de execução;
RECURSIVE;
WORKING-STORAGE versus LOCAL-STORAGE;
passagem de parâmetros;
modelagem de problemas hierárquicos;
árvores, XML, JSON e algoritmos clássicos;
desempenho, consumo de memória e depuração.
Talvez você passe toda a carreira sem precisar implementar uma rotina recursiva em produção.
Ainda assim, entender como ela funciona tornará muito mais fácil compreender dumps, o Language Environment, integrações com C e Assembler, além do comportamento interno do Enterprise COBOL.
No fim das contas, a maior contribuição da recursividade não é ensinar o computador a chamar uma rotina novamente.
É ensinar o programador a pensar em estruturas, contexto, memória e arquitetura.
E essa forma de pensar acompanha um verdadeiro Engenheiro Mainframe por toda a vida profissional.
☕ Fim da série "COBOL Recursivo — Muito Além do Fatorial". Juntas, as Partes 1, 2A, 2B e 2C formam um material extenso e progressivo, saindo dos fundamentos até aspectos de arquitetura e engenharia de software voltados ao Enterprise COBOL no IBM Z.