Mostrando postagens com marcador ciência. Mostrar todas as postagens
Mostrando postagens com marcador ciência. Mostrar todas as postagens

Apple apresenta M6 de 2 nm e M5 Ultra com quatro dies: o Mac entra de vez na disputa pela IA local

A Apple apresentou dois processadores que ocupam extremos diferentes de sua linha de computadores. O M6 estreia no Mac mini com uma arquitetura de 2 nanômetros voltada ao uso cotidiano, enquanto o M5 Ultra chega ao Mac Studio tentando transformar uma estação compacta em uma máquina capaz de executar modelos massivos de inteligência artificial localmente.

Representação dos processadores Apple M6 e M5 Ultra utilizados no Mac mini e no Mac Studio
O M6 inaugura a fabricação de 2 nanômetros nos chips da Apple, enquanto o M5 Ultra reúne quatro dies e pode trabalhar com até 512 GB de memória unificada.

Os dois lançamentos têm propostas muito diferentes, mas revelam a mesma direção. A Apple não está mais tratando o processamento de inteligência artificial como um recurso complementar. CPU, GPU, Neural Engine, memória e ferramentas de desenvolvimento estão sendo reorganizados para que aplicações de IA sejam executadas diretamente no computador, sem depender o tempo todo de servidores externos.

O M6 representa uma evolução mais ampla, destinada a estudantes, desenvolvedores, usuários domésticos e profissionais que precisam de um desktop pequeno e eficiente. O M5 Ultra pertence a outra categoria. Ele foi projetado para renderização 3D, edição de vídeos pesados, simulações científicas, geração de imagens, desenvolvimento de modelos e execução de grandes modelos de linguagem.

A diferença entre eles não está apenas na quantidade de núcleos. Está na maneira como a Apple pretende dividir o futuro do Mac: computadores menores ganham aceleração neural suficiente para tarefas locais, enquanto o Mac Studio assume o papel de estação de IA, vídeo e computação intensiva.

M6 é o primeiro chip de 2 nanômetros da Apple

A mudança mais visível do M6 está no processo de fabricação. Segundo a empresa, este é o primeiro processador da Apple construído com tecnologia de 2 nanômetros.

O número não deve ser interpretado como uma medida literal de cada transistor. Nas gerações atuais de semicondutores, expressões como 5 nm, 3 nm e 2 nm representam famílias de processos de fabricação, com diferentes níveis de densidade, eficiência e características elétricas.

Na prática, uma tecnologia mais avançada permite colocar mais transistores dentro de uma área limitada. Os engenheiros podem usar esse ganho para aumentar o desempenho, reduzir o consumo, acrescentar novos blocos de processamento ou equilibrar essas três possibilidades.

A Apple afirma que o processo de 2 nm proporciona ao M6 um avanço simultâneo em desempenho e eficiência energética. É uma afirmação plausível considerando a evolução da fabricação, mas a dimensão real desse ganho dependerá de medições independentes de consumo, temperatura e desempenho sustentado.

Um chip pode ser muito rápido durante alguns segundos e reduzir sua frequência quando aquece. Também pode apresentar excelente eficiência em determinada carga e comportamento diferente em outra. Esses detalhes só ficam claros quando os computadores chegam aos usuários e passam por testes fora do ambiente controlado da fabricante.

Mesmo assim, a estreia dos 2 nm é significativa. Ela dá à Apple mais espaço para ampliar partes importantes do sistema sem transformar o Mac mini em uma máquina maior ou mais difícil de resfriar.

Uma CPU de 12 núcleos com nova divisão de tarefas

O M6 traz uma CPU com 12 núcleos, dois a mais que a geração anterior. A organização interna chama atenção: são dois “super cores”, quatro núcleos de desempenho e seis núcleos de eficiência.

Os núcleos de eficiência cuidam de operações cotidianas e tarefas em segundo plano consumindo menos energia. Os núcleos de desempenho entram quando o trabalho exige mais capacidade de processamento. Os dois super cores assumem as tarefas mais sensíveis à velocidade de execução individual.

Essa estrutura representa uma evolução da arquitetura híbrida que a Apple já utiliza há várias gerações. Em vez de tratar todos os núcleos da mesma forma, o sistema distribui o trabalho conforme a necessidade. Não faz sentido ativar uma unidade muito poderosa para verificar notificações ou sincronizar pequenos arquivos. Da mesma maneira, compilar um projeto grande ou processar uma imagem complexa exige recursos que os núcleos econômicos não entregariam na mesma velocidade.

Segundo os números divulgados no comunicado oficial do M6 e do M5 Ultra, o novo processador oferece desempenho multithread de até 1,2 vez o M5 e até 2,4 vezes o M1. A empresa também afirma possuir o núcleo de CPU mais rápido do mundo em desempenho single-thread.

Essa última frase precisa ser lida como uma alegação de fabricante. A Apple informa que realizou seus testes em agosto de 2026, utilizando sistemas comerciais concorrentes e benchmarks selecionados. Ainda não há, no momento do anúncio, uma bateria ampla de avaliações independentes que permita comparar o M6 com todas as arquiteturas atuais em diferentes sistemas, aplicações e limites de energia.

O desempenho single-thread continua importante porque muitos processos não conseguem distribuir perfeitamente o trabalho entre dezenas de núcleos. Partes de compiladores, navegadores, ferramentas de edição e interfaces dependem da velocidade de uma sequência principal de instruções. É isso que costuma produzir a sensação de resposta imediata ao abrir programas, manipular projetos ou realizar operações curtas.

Já o ganho multithread aparece com mais clareza na compilação de código, exportação de arquivos, indexação, renderização e outras tarefas capazes de utilizar vários núcleos ao mesmo tempo.

Dual Neural Engine: dois blocos dedicados à IA

O M6 também estreia o que a Apple chama de Dual 16-core Neural Engine. Em termos simples, o chip possui dois motores neurais com 16 núcleos cada, voltados à execução de operações comuns em redes neurais.

O Neural Engine surgiu nos chips da Apple inicialmente ligado a recursos como reconhecimento de imagens, fotografia computacional, processamento de voz e aprendizado de máquina. Com a expansão dos modelos generativos, esse bloco ganhou outra importância.

Agora ele precisa lidar com tarefas como:

  • execução de modelos locais;
  • análise e classificação de conteúdo;
  • transcrição;
  • geração e transformação de imagens;
  • compreensão de linguagem;
  • recursos da Apple Intelligence;
  • operações realizadas por agentes de IA;
  • inferência de redes neurais incorporadas aos aplicativos.

A Apple afirma que o novo conjunto pode entregar até duas vezes o pico de capacidade computacional das gerações anteriores. Os frameworks do sistema conseguem utilizar os dois motores ao mesmo tempo, desde que o aplicativo e a carga tenham sido preparados para isso.

Esse detalhe importa. Dobrar um bloco de hardware não significa que todo aplicativo ficará duas vezes mais rápido automaticamente. O modelo precisa utilizar operações compatíveis, a tarefa precisa ser distribuída de maneira eficiente e o software deve conseguir alimentar os dois motores sem criar gargalos em outras partes do sistema.

Em alguns trabalhos, a limitação estará na GPU. Em outros, na largura de banda da memória. Também existem modelos que executam determinadas operações na CPU porque elas não se adaptam bem ao Neural Engine.

A arquitetura de IA da Apple está se tornando heterogênea. Isso significa que uma mesma carga pode ser dividida entre CPU, GPU e Neural Engine. O macOS e os frameworks da empresa tentam decidir qual parte do processador é mais adequada para cada operação.

Para o desenvolvedor, o ganho depende menos de “programar diretamente os 32 núcleos neurais” e mais de utilizar ferramentas que saibam aproveitar a arquitetura.

A GPU do M6 também ganhou aceleradores neurais

O M6 possui uma GPU de 12 núcleos, novamente dois a mais que o M5. Cada núcleo gráfico inclui um Neural Accelerator dedicado.

Essa aproximação entre computação gráfica e inteligência artificial acompanha uma transformação vista em toda a indústria. GPUs sempre foram boas em executar grandes quantidades de operações paralelas. As redes neurais se beneficiam justamente desse tipo de cálculo, especialmente multiplicações de matrizes realizadas repetidamente.

A presença de aceleradores especializados dentro dos núcleos da GPU busca melhorar esse trabalho sem depender exclusivamente do Neural Engine. Modelos generativos, ferramentas de imagem, aplicações de vídeo e grandes modelos de linguagem podem dividir suas operações entre diferentes unidades.

Segundo a Apple, o M6 apresenta um aumento próximo de 30% no pico de computação de IA pela GPU em comparação com o M5 e supera em mais de oito vezes a capacidade do M1 nessa métrica específica.

“Pico de computação” não é sinônimo de desempenho final do aplicativo. Trata-se de uma medida da capacidade teórica ou máxima de determinado bloco. O resultado percebido pelo usuário depende do modelo, da precisão numérica utilizada, do tamanho do contexto, do formato dos pesos, da memória disponível e da otimização do software.

Essa distinção ficou ainda mais necessária com a explosão das comparações de IA. Um computador pode ter excelente capacidade matemática e, mesmo assim, apresentar resultados medianos em um modelo mal otimizado. Outro pode ter números teóricos menores, mas contar com bibliotecas mais maduras e executar a mesma tarefa com maior eficiência.

O anúncio fornece uma direção. Os testes práticos dirão o tamanho do avanço.

Gráficos, jogos e ray tracing

O foco em IA não eliminou a função tradicional da GPU. O M6 recebe uma arquitetura atualizada de shaders, melhorias no Dynamic Caching e aceleração de ray tracing por hardware.

O Dynamic Caching é uma tecnologia utilizada para ajustar dinamicamente a quantidade de memória local destinada às tarefas da GPU. Em arquiteturas mais tradicionais, uma parte dos recursos pode acabar reservada sem ser totalmente utilizada. A proposta é realizar essa alocação de forma mais precisa, reduzindo desperdícios e aumentando o aproveitamento do chip.

A Apple também declara um aumento de 50% na taxa de processamento de geometria. Esse ganho pode ajudar em cenas tridimensionais complexas, jogos, modelagem 3D, visualização arquitetônica e aplicações que trabalham com muitos objetos.

O ray tracing simula o comportamento da luz para produzir reflexos, sombras e iluminação mais realistas. É uma técnica computacionalmente pesada, por isso a aceleração em hardware faz tanta diferença.

Ainda existe uma questão que a especificação do processador não resolve: disponibilidade de jogos. A Apple vem fortalecendo o Metal e aproximando o Mac de títulos mais exigentes, mas desempenho de GPU, por si só, não cria um catálogo. Desenvolvedores precisam portar, testar e manter seus jogos no macOS.

Para criação de conteúdo, o cenário costuma ser mais favorável. Aplicações de edição, renderização e produção audiovisual já aproveitam amplamente os recursos do Apple silicon.

Memória unificada mais rápida, mas limitada a 32 GB

O M6 alcança até 170 GB/s de largura de banda de memória, aproximadamente 10% acima do M5 e 2,5 vezes a largura de banda oferecida pelo M1, segundo a Apple. A capacidade máxima, porém, permanece em 32 GB no Mac mini equipado com esse chip.

Na arquitetura de memória unificada, CPU, GPU e Neural Engine acessam o mesmo conjunto de memória. Isso reduz a necessidade de copiar dados entre a memória principal e uma memória gráfica separada.

A ideia é especialmente útil em inteligência artificial. Os pesos de um modelo podem ser acessados pelos diferentes componentes do chip sem tantas movimentações intermediárias. O resultado potencial é menor latência, melhor eficiência e aproveitamento mais flexível da capacidade instalada.

Só que 32 GB ainda impõem limites claros.

Essa quantidade é confortável para atividades domésticas, desenvolvimento de software, edição de imagens, alguns projetos de vídeo e modelos locais menores. Já modelos de linguagem maiores, contextos muito extensos e geração avançada podem consumir rapidamente toda a memória disponível.

O M6 não tenta resolver o segmento extremo. Ele oferece uma base eficiente para IA local de uso cotidiano. Quem precisa carregar centenas de gigabytes de pesos entra no território do M5 Ultra.

M5 Ultra estreia uma arquitetura com quatro dies

O M5 Ultra é descrito pela Apple como o processador mais poderoso já criado pela empresa. Seu principal diferencial é a arquitetura de quatro dies, a primeira desse tipo em um chip da série M.

Um die é a parte física de silício que contém os componentes do processador. Fabricar um único chip gigantesco pode ser caro e difícil, pois qualquer defeito na produção compromete uma área maior. Uma alternativa é combinar vários dies menores por meio de conexões de alta velocidade.

No M5 Ultra, a Apple utiliza a tecnologia UltraFusion para conectar dois conjuntos M5 Max, cada um já formado por dois dies. O resultado é um sistema com quatro partes físicas trabalhando como um único processador lógico.

A comunicação entre os dies é decisiva. Se a conexão for lenta, o processador pode perder desempenho enquanto uma parte espera dados da outra. A Apple afirma ter elevado a largura de banda entre os dies para mais de 4,4 TB/s e aumentado a densidade das conexões em mais de seis vezes.

Esses números buscam permitir que os quatro dies compartilhem informações com baixa latência. Para o software, a intenção é que a estrutura se comporte como um grande sistema integrado, sem exigir que cada aplicativo seja refeito para gerenciar manualmente quatro chips separados.

É uma solução tecnicamente ambiciosa. Também será uma das partes mais interessantes dos futuros testes do Mac Studio: cargas reais mostrarão até que ponto o sistema consegue escalar entre todos os dies sem sofrer com sincronização, temperatura ou distribuição desigual do trabalho.

Até 36 núcleos de CPU no Mac Studio

O M5 Ultra pode ser configurado com uma CPU de até 36 núcleos, divididos entre 12 super cores e 24 núcleos de desempenho. Diferentemente do M6, não há núcleos de eficiência nessa configuração máxima divulgada.

Isso deixa evidente a prioridade. O M5 Ultra não foi construído para economizar cada pequena parcela de energia durante tarefas leves. Ele foi pensado para permanecer sob carga intensa, lidando com projetos nos quais terminar o trabalho mais cedo pode ser mais importante do que reduzir o consumo instantâneo.

A Apple aponta desempenho single-thread até 1,25 vez superior ao M3 Ultra e desempenho multithread até 1,3 vez maior. As comparações foram realizadas pela própria empresa com unidades de pré-produção do novo Mac Studio.

Um avanço de 30% em cargas multithread pode parecer menos dramático do que os números de IA apresentados no mesmo anúncio. A razão é que o foco desta geração está espalhado por várias áreas: interconexão dos dies, GPU, aceleradores neurais, memória, mecanismos de vídeo e execução local de modelos.

O M5 Ultra não é apenas uma CPU maior. Ele é uma plataforma de computação heterogênea.

GPU de até 80 núcleos e aceleração neural

A GPU do M5 Ultra chega a 80 núcleos, cada um equipado com um Neural Accelerator. Segundo o comunicado, a capacidade máxima de computação da GPU para IA pode ser até 4,5 vezes a do M3 Ultra e mais de seis vezes a do M1 Ultra.

Em uma comparação distinta de desempenho no Mac Studio, a empresa fala em ganhos de até 4,3 vezes sobre o M3 Ultra em determinados trabalhos de IA. A diferença entre os números ocorre porque “pico de computação” e “desempenho medido em uma aplicação” não são a mesma coisa.

A GPU incorpora shaders atualizados, Dynamic Caching de segunda geração, mesh shading acelerado e ray tracing de terceira geração. A Apple promete gráficos até 40% mais rápidos que os do M3 Ultra em sua comparação de arquitetura, enquanto a apresentação do novo Mac Studio menciona ganhos de até 1,8 vez em cargas selecionadas.

É um processador voltado a trabalhos bastante específicos:

  • renderização tridimensional;
  • efeitos visuais;
  • geração de imagens e vídeos;
  • treinamento ou ajuste de modelos;
  • simulações científicas;
  • visualização de grandes conjuntos de dados;
  • inferência de modelos generativos;
  • desenvolvimento de jogos;
  • edição de vídeo em alta resolução.

Para um usuário que navega na internet, edita documentos e ocasionalmente trabalha com imagens, grande parte dessa capacidade ficaria parada. O M5 Ultra faz sentido quando cada minuto de renderização, compilação ou processamento tem valor econômico.

Os 512 GB de memória são uma parte central do projeto

O componente mais impressionante do M5 Ultra talvez não seja a CPU nem a GPU. É a possibilidade de configurar o Mac Studio com até 512 GB de memória unificada e acessar essa memória com largura de banda de 1,2 TB/s.

A largura de banda é 50% superior à do M3 Ultra, de acordo com a Apple. Ela representa a quantidade de dados que o sistema consegue movimentar por segundo entre a memória e as unidades de processamento.

Grandes modelos de linguagem são particularmente sensíveis a esse fator. Durante a inferência, o computador precisa acessar repetidamente os pesos do modelo. Quando a largura de banda não acompanha a capacidade da GPU, os núcleos ficam esperando pelos dados.

Os 512 GB também permitem manter modelos muito grandes inteiramente na memória. A Apple afirma que o sistema pode executar localmente LLMs com centenas de bilhões de parâmetros, desde que sejam utilizados formatos e níveis de quantização compatíveis com a capacidade disponível.

Isso não significa que qualquer modelo desse tamanho será executado em sua precisão original ou terá o mesmo desempenho de um grande data center. Modelos podem exigir quantização para reduzir o consumo de memória. O tamanho do contexto também ocupa espaço, assim como o próprio sistema operacional e os aplicativos.

Mesmo com essas ressalvas, trata-se de uma capacidade incomum em um computador de mesa compacto. Muitas GPUs dedicadas de alto desempenho oferecem memória muito mais limitada. Servidores conseguem superar essa quantidade combinando várias placas, mas o custo, o consumo e a complexidade aumentam rapidamente.

A memória unificada cria uma característica interessante: os 512 GB não estão presos exclusivamente à CPU ou à GPU. O sistema pode distribuir seu uso conforme a carga. Para pesquisa local, prototipagem e trabalho com modelos privados, isso pode ser mais valioso do que uma simples comparação de núcleos.

Inteligência artificial local não é apenas uma questão de velocidade

A Apple associa os novos processadores à possibilidade de executar modelos de IA sem enviar dados continuamente para a nuvem.

Essa abordagem oferece algumas vantagens. Informações sensíveis podem permanecer no computador. A aplicação continua funcionando mesmo com conectividade limitada. O usuário não precisa pagar por cada token processado por um serviço remoto. A latência também pode diminuir, especialmente em tarefas interativas.

Existem limitações.

Modelos em nuvem podem utilizar grandes agrupamentos de GPUs, receber atualizações frequentes e atender cargas que seriam inviáveis em um computador pessoal. Executar tudo localmente exige armazenamento, memória, energia e capacidade de manutenção. Alguns modelos sequer possuem pesos abertos para instalação.

A tendência mais provável é híbrida. Tarefas privadas, rápidas ou repetitivas serão processadas no próprio Mac. Operações muito grandes ou dependentes de modelos fechados continuarão utilizando servidores.

O M6 e o M5 Ultra cobrem dois pontos dessa arquitetura. O M6 oferece IA local integrada ao computador cotidiano. O M5 Ultra tenta colocar uma parcela da computação antes restrita a servidores dentro de um desktop profissional.

Um mecanismo de mídia construído para produção audiovisual

O M5 Ultra também recebe um Media Engine mais poderoso, com aceleração dedicada para H.264, HEVC, ProRes, ProRes RAW e decodificação AV1.

Na configuração divulgada pela Apple, o chip possui dois mecanismos de decodificação de vídeo, quatro mecanismos de codificação e quatro unidades ProRes de codificação e decodificação. A empresa diz que o Mac Studio consegue reproduzir simultaneamente até 33 fluxos de vídeo ProRes 422 em resolução 8K e 30 quadros por segundo.

Esse tipo de aceleração é diferente de executar toda a operação na CPU. Blocos especializados conseguem processar codecs com mais eficiência, liberando CPU e GPU para efeitos, composição, correção de cor e outras partes do projeto.

Para editores de vídeo, estúdios e profissionais de efeitos visuais, o Media Engine pode ter impacto mais concreto do que o discurso sobre IA. Um projeto com várias câmeras, arquivos de alta resolução e camadas de efeitos coloca pressão sobre memória, armazenamento, decodificação e renderização ao mesmo tempo.

A página de especificações do Mac Studio confirma opções com CPU de 30 ou 36 núcleos, GPU de 64 ou 80 núcleos, Neural Engine de 32 núcleos e configurações de memória que chegam a 512 GB.

Desenvolvedores terão que aproveitar CPU, GPU e Neural Engine

Hardware especializado só apresenta bons resultados quando existem ferramentas capazes de utilizá-lo.

A Apple está concentrando esse trabalho em tecnologias como Core AI, Core ML, Metal, MLX e Xcode. Cada uma atua em uma parte diferente do processo de criação e execução de aplicações inteligentes.

O Core ML permite integrar modelos de aprendizado de máquina aos aplicativos. O Metal oferece acesso à computação da GPU. O MLX é um framework aberto e otimizado para Apple silicon, usado em experimentação, treinamento e inferência. Já o novo Core AI procura organizar a execução de modelos entre memória unificada, CPU, GPU e Neural Engine.

A proposta é evitar que o desenvolvedor precise administrar manualmente cada unidade de processamento. Os frameworks analisam as operações e tentam utilizar o componente mais adequado.

A Apple também permite que aplicativos acessem seus Foundation Models e recursos da Apple Intelligence por meio de ferramentas como App Intents. Desenvolvedores continuam podendo incorporar modelos próprios, o que é indispensável para empresas que trabalham com informações privadas ou sistemas especializados.

O benefício será maior nos aplicativos que adotarem essas APIs. Softwares antigos ou desenvolvidos sem otimização específica podem utilizar apenas uma pequena parte do novo hardware.

E existe a questão da compatibilidade. Nem todo modelo criado para CUDA ou preparado para GPUs de outros fabricantes será transportado imediatamente para o ecossistema da Apple. Ferramentas como MLX reduzem essa barreira, mas o ecossistema de software ainda pesa tanto quanto o silício.

Mac mini e Mac Studio deixam de disputar exatamente o mesmo usuário

O novo Mac mini com M6 procura oferecer desempenho elevado em uma máquina compacta, com 16 GB de memória na configuração inicial e possibilidade de chegar a 32 GB. Ele atende programação, produtividade, edição, desenvolvimento e experimentação com IA local.

A Apple também o posiciona como um dispositivo que pode permanecer ligado executando agentes de inteligência artificial. Esse é um uso relativamente novo para um computador doméstico: uma pequena máquina dedicada a automações, análise de documentos, geração de conteúdo, monitoramento ou serviços locais.

Já o Mac Studio com M5 Ultra funciona como estação profissional. Seu público não está apenas procurando um computador rápido. Está procurando capacidade para manter conjuntos enormes de dados na memória, processar vídeo de alta resolução ou executar modelos que não cabem em equipamentos convencionais.

A diferença aparece até na escala da conectividade. O novo Mac Studio adota Thunderbolt 5 e pode utilizar RDMA para formar grupos de várias máquinas. A Apple afirma que um cluster de quatro Mac Studio pode alcançar até três vezes o desempenho de inferência de uma única unidade em cargas selecionadas.

Isso não transforma o Mac Studio em substituto universal de um data center. Cria, porém, uma opção intermediária interessante entre um computador pessoal e uma infraestrutura especializada de servidores.

Os números são grandes, mas ainda são números da Apple

Quase todas as comparações apresentadas até agora foram produzidas pela própria fabricante em computadores de pré-produção, utilizando sistemas e cargas selecionadas.

Isso não invalida os resultados. A Apple informa a configuração das máquinas comparadas e descreve que utilizou benchmarks e aplicações específicas. O cuidado necessário está na interpretação.

“Até 4,5 vezes mais computação de IA” não significa que qualquer modelo ficará 4,5 vezes mais rápido. “Até 40% mais desempenho gráfico” não garante a mesma diferença em todos os jogos. “O núcleo mais rápido do mundo” depende do conjunto de sistemas, do benchmark e dos limites energéticos usados na comparação.

Também será necessário observar:

  • desempenho após vários minutos de carga;
  • temperatura e ruído;
  • consumo na tomada;
  • escalabilidade entre os quatro dies;
  • eficiência do Dual Neural Engine;
  • compatibilidade dos modelos;
  • desempenho com diferentes quantizações;
  • velocidade real de geração de tokens;
  • comportamento com toda a memória ocupada;
  • suporte dos aplicativos profissionais.

Os lançamentos foram anunciados em 25 de agosto de 2026, e a disponibilidade dos novos Mac mini e Mac Studio está prevista para 22 de setembro. Até que as unidades comerciais sejam analisadas por veículos e laboratórios independentes, o material oficial deve ser entendido como uma apresentação técnica acompanhada de marketing.

A mudança mais importante não está na litografia

Os 2 nanômetros do M6 são importantes. A arquitetura com quatro dies do M5 Ultra também. Mesmo assim, o aspecto mais revelador do anúncio é a distribuição do processamento neural por todo o chip.

A Apple não depende apenas de um Neural Engine maior. Ela adiciona Neural Accelerators aos núcleos da GPU, amplia a memória, aumenta a largura de banda, desenvolve novos frameworks e prepara o sistema para movimentar trabalhos entre diferentes unidades.

O computador deixa de ter “um componente de IA” isolado. A inteligência artificial passa a influenciar o desenho completo do processador.

Esse movimento também aparece na quantidade de memória do M5 Ultra. Durante anos, a discussão sobre computadores pessoais ficou concentrada em CPU e GPU. Modelos generativos trouxeram a memória novamente para o centro do projeto, pois não adianta possuir dezenas de núcleos se os pesos do modelo não cabem no sistema ou chegam lentamente às unidades de cálculo.

O M6 procura tornar a IA local uma função normal de um Mac compacto. O M5 Ultra leva a mesma ideia ao limite, com quatro dies, GPU de 80 núcleos, 512 GB de memória e largura de banda de 1,2 TB/s.

Ainda não sabemos se a geração cumprirá todas as promessas de desempenho. Isso depende de testes, aplicativos e de como os desenvolvedores responderão às novas ferramentas. O rumo, porém, ficou claro: para a Apple, o próximo ciclo dos computadores não será definido apenas por máquinas mais rápidas, mas por quanto processamento inteligente elas conseguem realizar sem sair da mesa do usuário.

COBOL: a linguagem de 1959 que ainda movimenta bancos, governos e grandes empresas

Quando uma transferência bancária é processada, uma folha de pagamento é calculada ou uma apólice de seguro é atualizada, existe uma possibilidade real de que alguma etapa dessa operação passe por um programa escrito em COBOL. A linguagem nasceu antes da internet, dos computadores pessoais e dos bancos de dados relacionais, mas continua presente em sistemas que não podem simplesmente parar para serem reconstruídos.

Representação de um sistema COBOL conectado a mainframe, banco de dados, aplicações web, APIs e serviços em nuvem
O COBOL raramente trabalha sozinho: ele faz parte de um ecossistema que reúne mainframes, bancos de dados, processamento em lote, mensageria, APIs e aplicações modernas.

Essa longevidade costuma produzir duas interpretações opostas. Para alguns, o COBOL é uma relíquia que deveria ter desaparecido há décadas. Para outros, é uma tecnologia tão confiável que não existe motivo para substituí-la. As duas visões simplificam um problema muito mais interessante.

O COBOL não continua em uso apenas porque empresas resistem à mudança. Ele permanece porque décadas de regras comerciais, cálculos financeiros, exceções operacionais e exigências regulatórias foram incorporadas aos programas. Em muitos casos, o código representa uma parte da memória institucional da organização.

O que é COBOL?

COBOL é a sigla para Common Business-Oriented Language, ou Linguagem Comum Orientada a Negócios. Trata-se de uma linguagem compilada de alto nível, criada especificamente para aplicações administrativas e processamento de dados empresariais.

Seu objetivo nunca foi controlar foguetes, desenvolver jogos ou realizar cálculos científicos avançados. O COBOL foi pensado para tarefas como:

  • processar folhas de pagamento;
  • calcular juros e parcelas;
  • atualizar saldos;
  • emitir faturas;
  • consolidar movimentações financeiras;
  • administrar apólices de seguro;
  • controlar estoques;
  • gerar relatórios;
  • processar grandes arquivos de registros;
  • validar operações segundo regras de negócio.

Uma de suas características mais conhecidas é a sintaxe relativamente próxima do inglês. Um programa simples pode conter instruções como MOVE, ADD, SUBTRACT, COMPUTE, READ, WRITE, PERFORM e DISPLAY.

Um exemplo básico seria:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. CALCULA-SALDO.

       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01 SALDO-ATUAL    PIC S9(9)V99 COMP-3.
       01 VALOR-DEPOSITO PIC S9(9)V99 COMP-3.

       PROCEDURE DIVISION.
           ADD VALOR-DEPOSITO TO SALDO-ATUAL
           DISPLAY "NOVO SALDO: " SALDO-ATUAL
           STOP RUN.

A aparência pode parecer incomum para quem começou a programar em Python, JavaScript ou Java. Ainda assim, o propósito do código é razoavelmente legível: adicionar o valor de um depósito ao saldo atual.

A linguagem também se destaca pelo tratamento de números decimais de ponto fixo. Isso é importante em sistemas financeiros, nos quais erros de arredondamento podem produzir diferenças contábeis. A IBM descreve o COBOL como especialmente adequado ao processamento confiável e volumoso de dados comerciais, incluindo arquivos sequenciais, indexados e relativos.

Uma linguagem criada para resolver a fragmentação

No final da década de 1950, os computadores eram equipamentos grandes, caros e profundamente incompatíveis entre si. Cada fabricante possuía arquiteturas, sistemas e linguagens próprias. Um programa desenvolvido para uma máquina geralmente precisava ser reescrito para funcionar em outra.

Essa dependência preocupava governos e grandes empresas. Trocar de fornecedor não significava apenas comprar um computador novo: podia exigir a reconstrução de todo o conjunto de programas administrativos.

Em 1959, representantes do governo dos Estados Unidos, fabricantes de computadores e grandes organizações formaram o Conference on Data Systems Languages, conhecido como CODASYL. O grupo buscava criar uma linguagem comum, voltada ao processamento de dados comerciais e menos dependente de um fabricante específico.

Grace Hopper teve influência decisiva nesse processo por meio de seu trabalho com compiladores e com o FLOW-MATIC, uma linguagem que permitia descrever operações comerciais com instruções semelhantes ao inglês. É impreciso, porém, afirmar que Hopper criou o COBOL sozinha. A linguagem foi resultado do trabalho coletivo de comitês técnicos, instituições públicas e empresas.

A primeira especificação foi apresentada em 1960. O COBOL recebeu revisões sucessivas e foi padronizado pelo ANSI em 1968. Vieram depois versões como COBOL-74, COBOL-85, COBOL 2002, COBOL 2014 e COBOL 2023. A atual norma publicada é a ISO/IEC 1989:2023, que define a sintaxe e a semântica da linguagem e busca preservar seu grau de independência entre diferentes sistemas de processamento.

O fato de existir uma especificação publicada em 2023 — e de uma nova revisão estar em desenvolvimento — mostra que o COBOL não é apenas um artefato histórico congelado em 1959.

A importância dos mainframes

Embora possa ser executado em outros ambientes, o COBOL tornou-se fortemente associado aos mainframes. Esses computadores foram construídos para processar grandes volumes de dados com disponibilidade, segurança, controle de acesso e previsibilidade operacional.

A família IBM System/360, apresentada em 1964, teve papel importante nessa expansão. A proposta de uma arquitetura compatível entre diferentes modelos permitia que empresas ampliassem sua capacidade de processamento sem abandonar todo o software já desenvolvido.

O mainframe transformou-se na base de sistemas bancários, seguradoras, empresas aéreas, governos, indústrias, redes varejistas e grandes processadores de pagamentos. O COBOL encontrou nesse ambiente um espaço ideal: processamento de arquivos extensos, cálculos comerciais, tarefas repetitivas e milhões de transações que exigiam consistência.

Ainda hoje, organizações brasileiras mantêm operações importantes em mainframes. Um caso documentado é o do Banco do Brasil, que utiliza tecnologias Db2 for z/OS e IBM Z para disponibilizar dados corporativos e sustentar consultas realizadas por milhares de painéis internos. O Banco de Brasília também mantém uma infraestrutura baseada em IBM Z, z/OS, CICS e Db2.

Esses exemplos não significam que todo o sistema esteja escrito em COBOL. Eles demonstram que a plataforma na qual muitas aplicações COBOL tradicionalmente operam continua presente em instituições brasileiras relevantes.

COBOL nunca foi uma tecnologia isolada

É comum imaginar um programador escrevendo COBOL em uma tela preta e controlando sozinho todo o sistema. Na prática, aplicações corporativas são formadas por várias tecnologias interdependentes.

Aprender a sintaxe do COBOL é apenas a primeira parte. Para compreender uma aplicação real, o profissional normalmente precisa conhecer componentes de execução, armazenamento, segurança, comunicação e automação.

JCL e o processamento em lote

O Job Control Language, ou JCL, é usado no ambiente z/OS para descrever trabalhos que serão executados pelo computador.

O JCL não substitui o COBOL. Sua função é informar ao sistema qual programa executar, quais arquivos serão utilizados, onde os resultados deverão ser gravados, quais recursos serão necessários e o que fazer em determinadas condições.

Imagine o fechamento noturno de um banco. Um conjunto de tarefas pode precisar:

  1. receber os movimentos realizados durante o dia;
  2. validar os registros;
  3. calcular juros;
  4. atualizar saldos;
  5. gerar arquivos contábeis;
  6. produzir relatórios;
  7. arquivar os resultados;
  8. liberar o próximo ciclo de processamento.

Os programas responsáveis pelos cálculos podem estar em COBOL. O JCL organiza e inicia sua execução.

Esse processamento em lote continua relevante porque permite tratar enormes quantidades de dados de maneira planejada e controlada. Para quem está acostumado apenas a APIs e aplicações web, entender uma janela de processamento noturno exige uma mudança de perspectiva.

VSAM e os arquivos corporativos

Antes de bancos de dados relacionais se tornarem comuns, muitas aplicações armazenavam informações em arquivos. No ecossistema IBM, o VSAM tornou-se uma tecnologia importante para organizar e acessar conjuntos de dados sequenciais ou indexados.

Sistemas COBOL podem ler, alterar e gravar registros VSAM. É possível localizar uma conta por uma chave, percorrer registros em sequência ou produzir um novo arquivo a partir do processamento de outro.

Mesmo quando uma organização adota bancos de dados modernos, arquivos VSAM podem continuar presentes em partes da operação. A migração exige entender formatos de registros, chaves, dependências, programas consumidores e procedimentos de recuperação.

CICS e as transações on-line

O COBOL começou muito ligado ao processamento em lote, mas as empresas também precisavam executar operações interativas. Surgiu então a necessidade de controlar milhares de transações simultâneas realizadas por terminais, caixas eletrônicos e sistemas corporativos.

O CICS tornou-se uma das principais soluções para isso. Ele funciona como um servidor de aplicações transacionais, administrando execução, segurança, comunicação e acesso a recursos.

Um programa COBOL executado sob CICS pode consultar um cliente, registrar um pagamento, autorizar uma operação ou atualizar um cadastro. O usuário não precisa saber que existe COBOL atrás da interface.

O CICS Transaction Server atual não está restrito a terminais antigos. Ele oferece integração com aplicações de diferentes linguagens, observabilidade, APIs, automação e ferramentas de desenvolvimento modernas.

IMS: banco de dados e gerenciador de transações

O IMS, sigla de Information Management System, é outra tecnologia histórica encontrada em grandes ambientes corporativos. Ele reúne um banco de dados hierárquico e um gerenciador de transações.

Aplicações COBOL podem utilizar o IMS para consultar e atualizar estruturas de dados extremamente importantes. Em algumas organizações, essas bases carregam décadas de informações e continuam atendendo operações críticas.

O IMS permanece em evolução, com integração a Java, APIs REST, automação por Ansible e ferramentas voltadas à nuvem híbrida. A tecnologia é antiga, mas seu ambiente atual não corresponde necessariamente ao que existia nas décadas de 1970 ou 1980.

Db2 e o SQL embutido

Com a expansão dos bancos de dados relacionais, o Db2 passou a complementar muitas soluções COBOL. O programa pode conter comandos SQL embutidos para consultar, incluir, alterar ou excluir registros.

Isso aproximou o COBOL de um modelo de desenvolvimento ainda familiar: a aplicação executa regras de negócio enquanto o sistema de banco de dados organiza os dados, os índices, os bloqueios e as transações.

Conhecer SQL é, assim, uma habilidade valiosa para quem deseja trabalhar com COBOL corporativo. Em determinados projetos, entender o plano de acesso de uma consulta ou o impacto de um índice pode ser mais importante do que memorizar comandos da linguagem.

Mensageria, integração e sistemas distribuídos

À medida que novas plataformas foram surgindo, as aplicações COBOL precisaram conversar com sistemas Unix, Windows, Java, aplicações móveis, sites e serviços hospedados em nuvem.

Tecnologias de mensageria, como IBM MQ, passaram a transportar eventos e solicitações entre ambientes. Uma aplicação pode publicar uma mensagem que será processada por outro sistema, sem exigir que ambos estejam disponíveis exatamente no mesmo instante.

Gateways e plataformas de integração permitem que programas existentes sejam acessados por aplicações Java, .NET e outras linguagens. O CICS Transaction Gateway, por exemplo, conecta aplicações de diferentes plataformas aos serviços executados no CICS.

APIs REST e ferramentas como z/OS Connect levaram essa integração adiante. Uma aplicação de celular pode chamar uma API moderna que, internamente, aciona uma transação CICS escrita em COBOL. Para o usuário, tudo parece parte de uma única experiência digital.

O que realmente significa “tecnologia legada”?

O termo “legado” é frequentemente empregado como sinônimo de velho, ruim ou ultrapassado. Essa associação nem sempre é justa.

Um sistema legado é, antes de tudo, um sistema herdado. Ele foi construído em outro contexto tecnológico, mas continua necessário para o funcionamento da organização.

Existem sistemas COBOL bem estruturados, documentados e mantidos. Também existem sistemas Java recentes repletos de dependências problemáticas e decisões difíceis de corrigir. A idade da linguagem não determina, sozinha, a qualidade do software.

O problema surge quando uma aplicação acumula características como:

  • documentação incompleta;
  • regras de negócio conhecidas por poucas pessoas;
  • dependências que não foram mapeadas;
  • bibliotecas sem código-fonte;
  • compiladores muito antigos;
  • rotinas duplicadas;
  • ausência de testes automatizados;
  • processos manuais de implantação;
  • estruturas de dados difíceis de interpretar;
  • integrações sem responsáveis definidos.

Um programa com milhares de linhas pode incluir regras criadas após auditorias, mudanças tributárias, incorporações empresariais e exceções negociadas com clientes. Reescrever o código sem compreender esse histórico pode eliminar comportamentos aparentemente estranhos, mas essenciais.

Por isso, o maior valor de certas aplicações não está na sintaxe do COBOL. Está no conhecimento operacional codificado ao longo de décadas.

Por que não reescrever tudo?

A ideia de substituir uma aplicação COBOL por Java, C# ou outra linguagem parece simples em uma apresentação. A execução costuma ser bem mais delicada.

Uma reescrita integral precisa reproduzir o comportamento do sistema antigo, incluindo casos raros, arredondamentos, formatos de arquivos, rotinas de fechamento, integrações e procedimentos de recuperação.

Também existem particularidades como:

  • dados em EBCDIC;
  • campos numéricos em formato compactado;
  • copybooks compartilhados por muitos programas;
  • chamadas entre módulos;
  • processamento orientado a registros;
  • arquivos de largura fixa;
  • dependências entre tarefas em lote;
  • códigos de retorno usados para controlar cadeias de execução;
  • atualizações coordenadas entre COBOL, CICS, Db2, IMS e VSAM.

Converter a sintaxe não garante equivalência funcional. Uma tradução pode compilar corretamente e ainda produzir resultados diferentes em situações contábeis específicas.

A modernização responsável começa pelo inventário: quais programas existem, quem os chama, quais dados utilizam, quais regras executam e quais processos dependem deles. A própria IBM alerta que uma estratégia baseada somente na tradução do COBOL ignora a arquitetura de dados, a plataforma de execução e a integridade transacional do sistema.

A dificuldade de aprender COBOL no Brasil

A sintaxe básica do COBOL não é o maior obstáculo para novos profissionais. Com dedicação, alguém que já conhece lógica de programação pode aprender suas divisões, tipos de dados, estruturas condicionais, arquivos e subprogramas.

A dificuldade está em adquirir experiência semelhante à exigida por ambientes de produção.

Pouco espaço nas formações tradicionais

Cursos superiores e técnicos no Brasil costumam concentrar seus programas em Java, Python, JavaScript, desenvolvimento web, bancos de dados relacionais e computação em nuvem. Essa escolha é compreensível, pois essas tecnologias atendem um mercado amplo e são fáceis de praticar em computadores pessoais.

COBOL, JCL, CICS, IMS, RACF, VSAM e z/OS raramente aparecem juntos em uma graduação. Quando aparecem, muitas vezes são apresentados de maneira histórica, sem acesso a um ambiente real.

O estudante termina conhecendo a existência do COBOL, mas não necessariamente sabe compilar um programa no z/OS, submeter um job, analisar um abend, navegar por conjuntos de dados ou investigar uma transação CICS.

Acesso limitado ao ambiente completo

É possível instalar um compilador COBOL no Linux ou no Windows. Isso permite aprender a linguagem, criar programas e processar arquivos locais.

Reproduzir todo o ambiente corporativo é outra história. Um mainframe de produção envolve sistema operacional, segurança, catálogos, filas, bancos de dados, monitores transacionais, agendadores, ferramentas de desenvolvimento e padrões específicos de cada empresa.

O acesso a esses ambientes é controlado por razões óbvias. Eles armazenam informações financeiras, pessoais e comerciais. Dessa forma, boa parte do conhecimento mais valorizado é adquirida no emprego, em programas de formação interna ou em laboratórios especializados.

Forma-se um paradoxo: algumas vagas pedem experiência prática, mas o candidato só consegue essa experiência depois de entrar em uma organização que opere a plataforma.

O ecossistema é maior do que a linguagem

Uma pessoa pode escrever um programa COBOL correto e ainda não estar pronta para investigar um incidente em produção.

O trabalho pode exigir leitura de JCL, análise de códigos de retorno, conhecimento de Db2, controle de transações CICS, interpretação de arquivos VSAM, uso de TSO/ISPF, entendimento de copybooks, ferramentas de versionamento e regras internas de implantação.

Também é necessário conhecer o negócio. Um desenvolvedor que atua em sistemas bancários precisa compreender conceitos financeiros. Em seguros, precisa conhecer apólices, prêmios, sinistros e vigências. Em sistemas públicos, pode precisar interpretar normas e processos administrativos.

É justamente a combinação entre tecnologia e domínio de negócio que torna profissionais experientes difíceis de substituir.

Documentação e comunidades menores

Há muito menos conteúdo recente em português sobre COBOL e mainframe do que sobre desenvolvimento web. Vários materiais técnicos estão em inglês, e parte da documentação pressupõe acesso a produtos ou ambientes específicos.

As comunidades são menores e boa parte do código corporativo não pode ser publicada em repositórios públicos. Um desenvolvedor JavaScript encontra milhares de projetos completos para estudar. Já uma aplicação COBOL bancária real dificilmente estará disponível no GitHub, pois contém regras proprietárias e estruturas sensíveis.

Isso reduz a visibilidade do ecossistema e cria a impressão de que não existe trabalho. Na verdade, muitas oportunidades circulam em nichos formados por bancos, seguradoras, processadoras, empresas de serviços de tecnologia e consultorias.

Como começar a estudar COBOL hoje

Embora o acesso profissional continue sendo um desafio, o caminho de entrada melhorou consideravelmente.

Uma possibilidade é iniciar com GnuCOBOL em um computador pessoal. O compilador permite estudar estruturas de dados, arquivos, cálculos, subprogramas e diferentes dialetos sem depender imediatamente de um mainframe.

O Open Mainframe Project mantém um curso aberto de programação COBOL, com materiais que vão dos primeiros passos a tópicos avançados e testes. O projeto também apresenta ferramentas modernas de edição, integração com VS Code, Zowe, automação e pipelines de integração contínua.

Outra alternativa é o IBM Z Xplore, plataforma gratuita e baseada em desafios. Ela oferece contato prático com COBOL, JCL, VSAM, Db2, REXX, Linux e outros componentes do ecossistema IBM Z.

Uma trilha sensata pode seguir esta ordem:

  1. lógica de programação e estruturas de dados;
  2. sintaxe básica do COBOL;
  3. processamento sequencial de arquivos;
  4. tabelas, subprogramas e copybooks;
  5. JCL e execução de tarefas em lote;
  6. TSO, ISPF e organização de conjuntos de dados;
  7. VSAM;
  8. SQL e Db2;
  9. conceitos de CICS;
  10. depuração e análise de falhas;
  11. controle de versão, testes e automação;
  12. APIs e integração com aplicações modernas.

Projetos próprios ajudam a transformar o estudo em evidência prática. Um pequeno sistema de contas, uma folha de pagamento simulada ou um processador de arquivos de vendas pode demonstrar domínio da linguagem. O ideal é incluir testes, documentação, JCL de exemplo e uma descrição clara das regras implementadas.

Ainda assim, é preciso honestidade: um laboratório pessoal não equivale à experiência de sustentar um sistema crítico em produção. Ele serve como porta de entrada, não como substituto integral.

Como o COBOL ainda é usado

O COBOL permanece adequado a operações que reúnem grande volume, regras estáveis, cálculos comerciais e necessidade rigorosa de consistência.

Entre os usos mais comuns estão:

  • sistemas bancários centrais;
  • processamento de cartões;
  • liquidação de pagamentos;
  • folhas de pagamento;
  • faturamento;
  • previdência;
  • seguros;
  • cobrança;
  • sistemas tributários;
  • reservas e emissão de passagens;
  • gestão de grandes cadastros;
  • processamento de lotes financeiros;
  • relatórios regulatórios.

Isso não significa que o cliente interaja diretamente com uma “tela COBOL”. O aplicativo móvel pode ser desenvolvido em Swift ou Kotlin, o portal pode usar React e os serviços intermediários podem estar em Java, Go ou .NET. No centro da operação, uma transação COBOL pode validar e registrar a movimentação.

A arquitetura moderna frequentemente se parece com uma composição de camadas. O COBOL preserva a lógica central, enquanto APIs, mensageria e microsserviços oferecem novas formas de acesso.

Modernizar não é apenas migrar

Existem várias estratégias possíveis para uma aplicação COBOL:

  • manter o sistema e atualizar compiladores;
  • melhorar documentação e cobertura de testes;
  • otimizar programas sem alterar sua lógica;
  • expor funções existentes por meio de APIs;
  • substituir interfaces antigas;
  • integrar eventos e mensageria;
  • mover partes específicas para outra plataforma;
  • modularizar programas monolíticos;
  • converter somente serviços selecionados;
  • reescrever completamente uma aplicação;
  • aposentar sistemas que perderam sua função.

A melhor escolha depende do risco, do custo, da qualidade do código, da disponibilidade de profissionais e do valor comercial da aplicação.

Uma reescrita total pode ser adequada para um sistema pequeno e bem compreendido. Em uma aplicação central com milhões de transações e dependências acumuladas, a evolução incremental tende a oferecer maior controle.

Ambientes COBOL também estão incorporando Git, VS Code, pipelines de CI/CD, análise estática, testes automatizados, APIs e observabilidade. A existência de código antigo não obriga a equipe a utilizar processos antigos.

Inteligência artificial e compreensão de sistemas legados

Ferramentas de inteligência artificial começam a ser usadas para explicar programas, mapear dependências, produzir documentação, sugerir testes e auxiliar na refatoração.

O watsonx Code Assistant for Z, por exemplo, oferece recursos de descoberta de aplicações, explicação de código, geração de COBOL, refatoração modular e transformação assistida para Java.

Essas ferramentas podem reduzir o tempo necessário para navegar por grandes bases de código. Elas não eliminam a necessidade de especialistas.

Nomes de campos abreviados, regras não documentadas e comportamentos históricos exigem contexto organizacional. Uma inteligência artificial pode interpretar a estrutura do programa, mas não necessariamente sabe por que uma determinada exceção foi criada vinte anos atrás.

O uso seguro exige testes de regressão, validação de resultados, revisão humana e participação das áreas de negócio. Em aplicações financeiras e governamentais, “parece equivalente” não é um critério suficiente.

O futuro do COBOL

É improvável que o COBOL volte a ocupar posição central nos cursos de programação ou se transforme em uma linguagem popular entre iniciantes. Também é improvável que desapareça rapidamente.

Seu futuro deve ser marcado por uma redução gradual de alguns sistemas, combinada à modernização e manutenção de outros. Certas aplicações serão reescritas. Outras permanecerão em COBOL, mas ganharão APIs, monitoramento, automação, interfaces modernas e integração com ambientes distribuídos.

O padrão publicado em 2023, os compiladores atualizados, os projetos de código aberto e as ferramentas contemporâneas de desenvolvimento mostram que a linguagem ainda evolui. O próprio mercado de mainframe busca renovar profissionais: a IBM anunciou em 2024 um Mainframe Skills Council e programas de formação voltados a desenvolvedores, administradores e arquitetos de modernização.

Para o profissional brasileiro, COBOL provavelmente continuará sendo uma especialização. A quantidade de vagas pode ser menor do que em Java ou JavaScript, mas o conhecimento exigido também é mais raro. O perfil mais valorizado não será o de quem conhece somente a sintaxe da linguagem, e sim o de quem consegue compreender sistemas inteiros.

Esse profissional precisará transitar entre dois mundos: COBOL e APIs, lote e tempo real, ISPF e VS Code, arquivos tradicionais e bancos relacionais, mainframe e nuvem, regras escritas há décadas e novos produtos digitais.

A linguagem criada em 1959 não sobreviveu porque o tempo parou. Ela sobreviveu porque as organizações continuaram operando, acumulando regras e construindo novas camadas ao redor de sistemas que já funcionavam.

Compreender COBOL é também compreender que a história da computação não é feita apenas de substituições. Muitas vezes, ela avança por integração, adaptação e convivência entre tecnologias de épocas diferentes.

Hierarquia de memória: por que o computador precisa de registradores, cache, RAM, SSD e HDD?

Um computador não possui vários tipos de memória apenas porque a indústria criou tecnologias diferentes ao longo do tempo. Essa diversidade existe porque nenhum dispositivo consegue ser, ao mesmo tempo, extremamente rápido, barato, durável, energeticamente eficiente e capaz de armazenar grandes quantidades de dados.

Representação da hierarquia de memória de um computador, dos registradores ao armazenamento em HDD
A hierarquia de memória organiza diferentes tecnologias conforme velocidade, capacidade, custo, consumo de energia e distância física até o processador.

Quanto mais próxima uma memória está das unidades de execução do processador, mais rapidamente seus dados podem ser acessados. Só que essa proximidade tem preço. Registradores e caches ocupam uma área valiosa dentro do chip, consomem energia e são caros por byte. Discos rígidos ficam muito mais distantes do processador e trabalham em outra escala de tempo, mas conseguem guardar terabytes por um custo relativamente baixo.

A hierarquia nasce dessa incompatibilidade.

No topo estão os registradores, pequenos e extremamente rápidos. Logo abaixo aparecem os diferentes níveis de cache. Depois vem a memória principal, normalmente baseada em DRAM. Mais distante encontramos os SSDs, seguidos pelos discos rígidos e por sistemas de armazenamento ainda maiores, como bibliotecas de fitas, armazenamento distribuído e serviços de nuvem.

Não se trata apenas de uma lista organizada do componente mais rápido para o mais lento. Cada nível tenta esconder as limitações do nível seguinte.

Uma biblioteca para entender a hierarquia

Imagine uma pessoa trabalhando em uma grande biblioteca.

Em cima de sua mesa estão algumas folhas com as informações utilizadas naquele exato momento. Elas podem ser alcançadas sem levantar da cadeira. Esse espaço é pequeno, mas o acesso é quase imediato. Na analogia, seriam os registradores do processador.

Ao lado da mesa existe uma estante com os livros consultados com maior frequência. Ainda é um espaço limitado, porém guarda mais informações que a mesa. A pessoa precisa esticar o braço ou levantar rapidamente para alcançar um volume. Essa estante representa a memória cache.

Os demais livros ficam organizados na sala principal da biblioteca. Há milhares deles. Para buscar um título, a pessoa precisa caminhar até a estante correta, localizar a prateleira e retornar. A biblioteca principal corresponde à memória RAM.

Existe também um arquivo no subsolo, onde são guardados documentos que não precisam permanecer disponíveis na sala. O acesso demora mais, mas o arquivo possui espaço muito maior. Podemos associá-lo a um SSD.

Documentos antigos, grandes coleções e materiais raramente consultados talvez fiquem em outro prédio. Um funcionário precisa solicitar o item, esperar sua localização e aguardar o transporte. Esse depósito representa o HDD, os sistemas de fita ou outras camadas de armazenamento de grande capacidade.

Se a pessoa precisasse ir ao outro prédio a cada frase lida, o trabalho seria insuportavelmente lento. Por isso, materiais relevantes são antecipadamente levados para a sala, depois para a estante próxima e finalmente colocados sobre a mesa.

O computador tenta fazer algo semelhante. Dados que provavelmente serão usados em breve são movidos para níveis mais próximos do processador. Quando essa previsão funciona, a CPU continua trabalhando. Quando falha, ela precisa esperar.

E processadores atuais são rápidos demais para esperar sem que isso represente desperdício.

Velocidade não é uma única medida

Quando falamos que uma memória é rápida, podemos estar tratando de duas características diferentes: latência e largura de banda.

Latência é o tempo necessário para iniciar e concluir determinado acesso. É quanto o processador espera até receber a informação solicitada.

Largura de banda representa a quantidade de dados que pode ser transferida durante certo período. Uma memória pode demorar para iniciar uma operação e, depois disso, transferir um grande volume de dados por segundo.

Uma rodovia ajuda a visualizar a diferença. A latência é o tempo que um veículo leva para chegar ao destino. A largura de banda está relacionada ao número de veículos que a estrada consegue transportar simultaneamente.

Discos rígidos ilustram bem essa separação. A leitura aleatória de um pequeno arquivo pode ser lenta porque a cabeça precisa se deslocar e esperar o setor correto passar sob ela. Quando o disco começa a ler um arquivo grande e contínuo, consegue manter uma taxa de transferência razoável.

Em sistemas de inteligência artificial, a largura de banda ganha um peso enorme. Não basta a GPU executar trilhões de operações matemáticas se os dados necessários não chegam às unidades de processamento com velocidade suficiente.

A localidade faz a hierarquia funcionar

A hierarquia depende de um comportamento comum dos programas chamado localidade.

A localidade temporal indica que, se um dado foi usado recentemente, existe uma boa chance de ele ser usado novamente em pouco tempo. Uma variável dentro de um laço, por exemplo, pode ser lida milhões de vezes.

A localidade espacial sugere que, quando um endereço é acessado, os endereços próximos também podem ser necessários. Um programa que percorre um vetor tende a ler seus elementos em sequência.

As caches exploram os dois padrões. Em vez de buscar apenas um byte isolado, o processador normalmente transfere um bloco maior, chamado linha de cache. Se o programa continuar acessando dados próximos, eles já estarão disponíveis.

Isso também explica por que dois programas que realizam a mesma quantidade de operações podem apresentar desempenhos muito diferentes. O primeiro organiza seus dados de maneira previsível e aproveita a cache. O segundo pula entre regiões distantes da memória, provocando falhas de cache e esperas constantes.

O algoritmo continua correto. O processador continua sendo o mesmo. Só mudou a maneira como os dados atravessam a hierarquia.

HDD: bits gravados em uma superfície magnética

O disco rígido é uma mistura impressionante de mecânica de precisão, magnetismo e eletrônica.

Dentro de um HDD existem um ou mais pratos circulares revestidos por material magnético. Esses pratos giram em alta velocidade, normalmente a milhares de rotações por minuto. Pequenas cabeças de leitura e gravação se movem sobre suas superfícies.

A cabeça não encosta normalmente no prato durante a operação. Ela flutua sobre uma camada de ar extremamente fina criada pela própria rotação. Quanto menor a distância segura entre a cabeça e a superfície, maior pode ser a densidade de gravação. A Seagate explica que essas cabeças ficam suspensas sobre essa fina almofada de ar enquanto leem ou alteram regiões magnéticas.

Os dados são representados por propriedades magnéticas de áreas microscópicas do prato. Na escrita, o campo produzido pela cabeça modifica o estado magnético da região. Na leitura, alterações nesse campo são detectadas e convertidas em sinais elétricos.

A mecânica introduz atrasos inevitáveis.

Primeiro, o braço precisa posicionar a cabeça sobre a trilha adequada. Esse movimento produz o tempo de busca. Depois, é necessário esperar que o setor desejado gire até passar sob a cabeça, criando a latência rotacional.

Milissegundos parecem pouco na experiência humana. Para um processador trabalhando em nanossegundos, representam uma eternidade.

O HDD continua relevante porque oferece enorme capacidade por um custo baixo. Data centers, sistemas de backup, servidores de arquivos, vigilância, armazenamento científico e grandes repositórios ainda podem se beneficiar dessa relação entre preço e volume.

Tecnologias como gravação magnética assistida por calor, conhecida como HAMR, permitem aumentar a densidade dos pratos. Nessa técnica, um pequeno laser aquece temporariamente uma área minúscula para facilitar a alteração de sua polaridade magnética, conforme descreve a documentação da Seagate sobre HAMR.

O disco rígido é lento para acessos aleatórios, mas continua difícil de substituir quando a prioridade é guardar muitos terabytes sem transformar cada servidor em um objeto absurdamente caro.

SSD: elétrons presos em células NAND

O SSD elimina pratos, motores e cabeças móveis. Seus dados são armazenados em memória flash NAND, uma tecnologia não volátil capaz de manter informações mesmo sem fornecimento de energia.

Na escala microscópica, a célula NAND utiliza um transistor modificado para reter carga elétrica. Dependendo da arquitetura, essa carga fica armazenada em uma porta flutuante ou em uma estrutura de aprisionamento de carga.

A presença e a quantidade de elétrons modificam a tensão necessária para ativar o transistor. Durante a leitura, o controlador aplica tensões e interpreta em qual faixa o comportamento da célula se encontra.

Uma célula SLC armazena um bit, permitindo dois estados lógicos. MLC, TLC e QLC registram mais bits por célula por meio de vários níveis de tensão. Isso aumenta a capacidade e reduz o custo por gigabyte, mas exige distinguir margens elétricas menores.

Quanto mais estados uma célula precisa representar, mais delicadas ficam a programação, a leitura e a correção de erros. Também tende a existir maior desgaste e menor velocidade de gravação direta quando comparada a células que armazenam menos bits.

Nos SSDs modernos, as células não ficam apenas lado a lado sobre uma superfície plana. A 3D NAND empilha camadas verticalmente, aumentando a densidade sem depender exclusivamente da redução horizontal dos componentes. Existem produtos com centenas de camadas.

A memória NAND trabalha com páginas e blocos. Dados podem ser lidos e programados em páginas, mas o apagamento normalmente acontece em blocos maiores. Isso cria uma complicação: para atualizar determinada informação, o controlador nem sempre pode sobrescrever diretamente a mesma célula.

Ele grava a nova versão em outro local, marca a anterior como inválida e reorganiza blocos posteriormente. Esse processo está ligado à coleta de lixo, ou garbage collection.

O controlador do SSD mantém uma camada de tradução entre os endereços lógicos vistos pelo sistema operacional e as posições físicas da memória. Também executa nivelamento de desgaste, correção de erros, gerenciamento de blocos defeituosos e outras tarefas internas.

Um SSD, então, não é apenas um conjunto de chips NAND. O controlador e seu firmware têm papel decisivo no desempenho e na durabilidade.

Há ainda caches baseadas em DRAM ou em uma região NAND operando temporariamente como SLC. Por isso, certos SSDs gravam rapidamente no início de uma transferência e reduzem a velocidade quando a cache se esgota.

Mesmo com essas limitações, o SSD é muito mais rápido que o HDD em acessos aleatórios porque não precisa movimentar partes mecânicas. Isso muda completamente a inicialização do sistema, a abertura de programas, a instalação de pacotes e a manipulação de milhares de arquivos pequenos.

Armazenamento não é memória principal

SSD e HDD guardam o sistema operacional, os programas e os arquivos quando o computador está desligado. Para executar um programa, seus dados precisam ser carregados para a memória principal.

A diferença não é apenas terminológica.

O armazenamento é persistente e possui grande capacidade, mas sua latência é alta demais para alimentar diretamente o processador em operações comuns. A memória RAM é volátil, mais cara por byte e perde seus dados sem energia, porém oferece acesso muito mais rápido.

Quando abrimos um programa, partes do executável são lidas do SSD e colocadas na RAM. O sistema operacional pode carregar apenas as páginas necessárias, em vez de copiar imediatamente o programa inteiro.

Se a quantidade de memória física se torna insuficiente, o sistema pode mover páginas menos usadas para uma área de troca no armazenamento. No Linux, isso aparece na forma de partição ou arquivo de swap. O mecanismo permite continuar funcionando, mas trocar constantemente páginas entre RAM e SSD causa uma queda perceptível de desempenho.

É outra demonstração da hierarquia. O armazenamento consegue atuar como extensão da memória, só que não adquire a mesma velocidade por receber outro nome.

DRAM: um transistor, um capacitor e uma carga que desaparece

A memória principal de computadores costuma utilizar DRAM, sigla para memória dinâmica de acesso aleatório.

Uma célula DRAM clássica é formada por um transistor e um capacitor. O capacitor guarda uma pequena carga elétrica, enquanto o transistor controla o acesso à célula.

O estado da carga representa a informação. O problema é que capacitores não mantêm essa carga indefinidamente. Pequenas correntes de fuga fazem o valor desaparecer.

Por isso ela é chamada de dinâmica.

O controlador precisa atualizar periodicamente as células, processo conhecido como refresh. Mesmo quando o computador não está alterando determinados dados, a DRAM precisa gastar tempo e energia preservando-os.

A leitura também é delicada. A carga armazenada é minúscula e precisa ser detectada por circuitos chamados amplificadores de sentido. Em muitas implementações, a leitura perturba o estado da célula, exigindo que o valor seja restaurado depois.

Os bits são organizados em matrizes, linhas, colunas, bancos, grupos de bancos, chips, canais e módulos. Antes de acessar uma coluna, o sistema ativa determinada linha e a transfere para os amplificadores de sentido. Se o próximo acesso ocorrer na mesma linha aberta, ele pode ser atendido com menor atraso. Se exigir outra linha, a anterior precisa ser fechada e a nova deve ser ativada.

Essa estrutura ajuda a entender por que a latência da memória não depende apenas da frequência anunciada na embalagem. Temporizações, número de canais, padrão de acesso, controlador, organização dos bancos e concorrência entre núcleos também influenciam o resultado.

DDR significa Double Data Rate. A tecnologia transfere dados em mais de uma transição do sinal de clock, ampliando a taxa efetiva. Gerações como DDR4 e DDR5 aumentaram capacidade, paralelismo e largura de banda, mas a distância de desempenho entre a CPU e a memória principal continua existindo.

A Micron resume a célula DRAM como a combinação de um transistor e um capacitor destinada ao acesso rápido enquanto o sistema está em funcionamento.

Cache: pequena demais para guardar tudo, rápida o suficiente para evitar esperas

Entre a CPU e a DRAM existem as caches.

Elas costumam ser baseadas em SRAM, ou memória estática de acesso aleatório. Diferentemente da DRAM, uma célula SRAM não depende de um capacitor que precisa ser atualizado periodicamente. Uma implementação clássica pode utilizar seis transistores para manter um bit.

Essa estrutura ocupa muito mais área no silício que a célula DRAM de um transistor e um capacitor. Construir gigabytes de SRAM dentro de uma CPU seria caro, grande e energeticamente problemático.

Mas ela é rápida. Então usamos pequenas quantidades.

A cache L1 fica muito próxima das unidades de execução e normalmente é dividida entre instruções e dados. Cada núcleo costuma possuir suas próprias caches L1. Elas têm pouca capacidade, mas oferecem baixa latência e alta largura de banda.

A L2 é maior e um pouco mais lenta. Dependendo da arquitetura, pode ser privada para cada núcleo ou organizada de outra forma.

A L3, também chamada em muitos projetos de cache de último nível, costuma ser compartilhada entre vários núcleos. Possui capacidade maior, mas sua latência é superior à da L1 e da L2.

Os detalhes variam entre processadores. Nem toda arquitetura usa exatamente a mesma organização, e “L3 compartilhada” não significa que todos os núcleos acessem qualquer região com custo idêntico.

Quando a CPU procura um dado, verifica primeiro os níveis mais próximos. Se ele estiver presente, ocorre um cache hit. Se não estiver, temos um cache miss, e a busca continua no próximo nível.

Uma falha em L1 pode ser atendida pela L2. Uma falha em L2 pode encontrar o dado na L3. Se nenhum nível possuir a linha necessária, o controlador precisa buscá-la na DRAM.

A diferença é suficiente para interromper o fluxo eficiente de instruções. Processadores tentam esconder parte desse atraso executando outras operações, prevendo acessos e trazendo dados antecipadamente por meio de mecanismos de prefetch. Nem sempre conseguem.

Sistemas com vários núcleos também precisam manter coerência. Se um núcleo modifica uma informação que outro núcleo possui em sua cache, o hardware precisa impedir que versões incompatíveis sejam usadas como se ambas fossem atuais.

É muita lógica para fazer o acesso à memória parecer simples.

Registradores: onde a operação realmente acontece

No topo estão os registradores.

Eles ficam dentro do núcleo do processador e armazenam operandos, endereços, resultados intermediários, ponteiros e estados usados diretamente pelas instruções.

Quando uma CPU soma dois valores, esses valores normalmente precisam estar em registradores ou ser entregues às unidades de execução por mecanismos internos equivalentes. O resultado também é colocado em um registrador antes de seguir para outro destino.

Registradores são extremamente rápidos, mas existem em quantidade muito limitada. A arquitetura expõe determinados registradores ao conjunto de instruções, enquanto a microarquitetura pode possuir um número físico maior para técnicas como renomeação e execução fora de ordem.

Compiladores trabalham para manter valores importantes nos registradores sempre que possível. Quando faltam registradores, alguns valores precisam ser temporariamente enviados à pilha na memória, operação chamada spill. Isso adiciona acessos e pode reduzir o desempenho.

A Intel descreve a hierarquia começando pelos registradores, passando pela cache L1 e seguindo para níveis progressivamente mais distantes.

Quanto mais perto das unidades de execução, menor a capacidade. Não é coincidência. A área física do chip e o tempo de propagação dos sinais também participam dessa história.

Um dado atravessa várias camadas antes de ser processado

Imagine uma fotografia armazenada em um SSD.

Quando um programa abre o arquivo, o controlador do SSD localiza as páginas NAND correspondentes. Os dados passam pela interface de armazenamento e são colocados em regiões da RAM administradas pelo sistema operacional.

Ao processar a imagem, a CPU solicita blocos dessa memória. Linhas são copiadas para a cache L3, L2 e L1 conforme o padrão de acesso. Valores específicos chegam aos registradores e entram nas unidades vetoriais ou aritméticas.

O resultado segue o caminho inverso. Pode permanecer temporariamente nas caches, voltar à RAM e, quando o programa salva o arquivo, ser enviado ao SSD.

Nem toda etapa acontece de maneira rigidamente sequencial. Existem acesso direto à memória, buffers, filas, caches do sistema operacional, controladores inteligentes e operações assíncronas. O modelo simplificado, porém, revela uma coisa importante: processar dados significa também movimentá-los.

Em muitos programas, a matemática não é a parte mais cara. Levar os operandos até o lugar certo custa tempo e energia.

O problema da “parede da memória”

A capacidade computacional dos processadores cresceu mais rapidamente que a redução da latência da memória principal. Essa diferença é conhecida como memory wall, a parede da memória.

Podemos construir mais unidades de execução, aumentar o paralelismo e realizar mais operações por ciclo. Só que essas unidades precisam receber dados.

Se a memória não fornece informações com largura de banda suficiente, parte do processador fica ociosa. É como construir mais caixas em um supermercado sem ampliar os corredores, o estoque e as esteiras que levam produtos até eles.

Caches maiores ajudam. Prefetch ajuda. Compressão, reorganização de dados, execução fora de ordem e novas interconexões também ajudam.

Nenhuma dessas técnicas elimina o problema. Elas tentam administrá-lo.

A inteligência artificial tornou essa limitação muito mais visível porque modelos atuais trabalham com enormes matrizes de pesos, ativações e estados intermediários. Uma GPU pode possuir uma capacidade matemática extraordinária e continuar limitada pela velocidade com que lê e movimenta esses dados.

Por que a IA precisa de tanta memória?

Um modelo de linguagem contém parâmetros numéricos aprendidos durante o treinamento. Se um modelo possui 70 bilhões de parâmetros e cada parâmetro é armazenado em formato de 16 bits, apenas os pesos ocupam aproximadamente 140 bilhões de bytes, ou cerca de 140 GB na contagem decimal.

Isso é apenas uma aproximação dos pesos.

Durante o treinamento, o sistema também pode precisar guardar gradientes, ativações, estados do otimizador, buffers temporários e cópias dos parâmetros em outras precisões. O consumo total pode ser várias vezes maior que o tamanho bruto do modelo.

Técnicas como paralelismo, checkpointing de ativações, precisão reduzida, particionamento de estados e LoRA tentam reduzir ou distribuir esse custo. O FSDP do PyTorch, por exemplo, divide entre GPUs não apenas parâmetros, mas também gradientes e estados do otimizador, conforme explica a documentação sobre treinamento distribuído.

Na inferência, não precisamos armazenar gradientes de treinamento, mas surge outro consumidor importante: o KV cache.

Transformers produzem chaves e valores usados pelo mecanismo de atenção. Durante a geração, esses resultados são preservados para evitar que todo o contexto anterior seja recalculado a cada novo token.

Quanto maior o contexto, maior tende a ser esse cache. Quanto mais usuários simultâneos e mais sequências em andamento, maior a pressão sobre a memória da GPU.

A documentação da NVIDIA observa que sequências de entrada mais longas aumentam os requisitos de memória da fase de prefill, enquanto saídas longas aumentam os requisitos durante a geração. Em sistemas de larga escala, o KV cache pode ocupar uma parte relevante da memória disponível.

É por isso que aumentar apenas o número de operações por segundo não resolve tudo.

HBM: pilhas de DRAM ao lado do acelerador

GPUs e aceleradores de IA utilizam cada vez mais HBM, sigla para High Bandwidth Memory.

HBM continua sendo DRAM, mas organizada de uma maneira diferente dos módulos DDR instalados em placas-mãe convencionais. Diversos dies de memória são empilhados verticalmente e conectados por vias que atravessam o silício, conhecidas como TSVs.

Essas pilhas são colocadas muito perto do processador, normalmente no mesmo encapsulamento avançado, ligadas por uma interface extremamente larga. Em vez de depender apenas de frequências altíssimas sobre um barramento estreito e relativamente longo, a HBM movimenta muitos bits em paralelo por uma distância curta.

A proximidade e a largura da interface permitem alcançar uma enorme largura de banda. Também ajudam a reduzir a energia consumida por bit transferido quando comparadas a certas abordagens convencionais.

A HBM3e representa uma evolução dessa família. Produtos da geração Hopper e Blackwell utilizaram a tecnologia para ampliar capacidade e vazão de dados.

A NVIDIA informa que a GPU H200 possui 141 GB de HBM3e com largura de banda de até 4,8 TB/s. Em plataformas Blackwell Ultra, determinadas configurações chegam a 288 GB por GPU e até 8 TB/s, segundo as especificações divulgadas pela fabricante.

São números máximos de produtos específicos, não uma característica universal de toda memória HBM3e. O desempenho real de uma aplicação também depende do padrão de acesso, do software, da interconexão, da ocupação do acelerador e de vários outros fatores.

Mesmo assim, a escala é reveladora. Um único acelerador pode movimentar terabytes por segundo porque alimentar suas unidades matemáticas exige uma espécie de sistema circulatório de dados.

A memória de uma GPU ainda pode ser pequena demais

Centenas de gigabytes parecem muito até um modelo grande, seus caches e diversos usuários disputarem o mesmo espaço.

Quando o modelo não cabe em uma GPU, ele pode ser dividido entre várias. Camadas, tensores ou partes dos parâmetros são distribuídos, e os aceleradores precisam trocar resultados durante o processamento.

Agora surge outro nível da hierarquia: a interconexão entre GPUs.

Dentro de um servidor, tecnologias como NVLink oferecem uma comunicação mais rápida que interfaces genéricas. Entre servidores, redes de alta velocidade, como InfiniBand ou Ethernet especializada, transportam dados através do cluster.

A memória deixou de ser apenas aquilo que existe dentro de um chip. Passamos a lidar com uma hierarquia distribuída:

  • registradores e memórias internas das unidades de execução;
  • caches do acelerador;
  • HBM local;
  • memória de outras GPUs;
  • DRAM do host;
  • expansão de memória;
  • SSDs NVMe;
  • armazenamento de rede;
  • grandes repositórios de dados.

Cada salto adiciona latência, consome energia e ocupa a interconexão.

Uma plataforma GB200 NVL72, por exemplo, reúne dezenas de GPUs e oferece vários terabytes de HBM3e, acompanhados por uma malha de comunicação de alta velocidade. Isso não transforma o conjunto em uma única memória perfeita. O software precisa distribuir o modelo e coordenar a movimentação dos dados.

Quanto maior o sistema, mais importante se torna saber onde cada informação está.

O caminho de um modelo de linguagem

Quando um modelo é iniciado, seus pesos podem estar armazenados em SSDs locais ou em um sistema distribuído. Eles precisam ser lidos e carregados para a memória do servidor. Depois, são transferidos para a HBM das GPUs.

Durante a fase de prefill, o prompt do usuário é processado em paralelo para construir as representações internas e o KV cache. Essa etapa costuma utilizar bastante capacidade computacional.

Na geração, o modelo produz tokens progressivamente. Para cada novo token, os pesos precisam participar dos cálculos e o sistema consulta o histórico armazenado no KV cache. Dependendo da configuração, essa fase pode ficar bastante limitada pela largura de banda da memória.

Os dados circulam o tempo inteiro.

Pesos são lidos. Ativações são geradas. Partes do KV cache são consultadas. Resultados atravessam unidades de processamento e, em modelos distribuídos, seguem para outros aceleradores.

Essa movimentação consome energia. Em certas cargas, transportar dados entre memória e processador pode custar mais que a própria operação aritmética.

A discussão sobre eficiência de IA não pode ficar restrita ao número de FLOPS. Também precisamos observar bytes movimentados, largura de banda utilizada, capacidade ocupada, padrões de acesso e energia por transferência.

Uma multiplicação executada rapidamente não ajuda se os operandos chegam atrasados.

O armazenamento continua abaixo da inteligência artificial

HBM recebe atenção porque está diretamente associada aos aceleradores, mas a cadeia não começa nela.

Modelos precisam ser treinados com grandes conjuntos de dados. Esses conjuntos vivem em repositórios compostos por SSDs, HDDs, armazenamento de objetos e sistemas distribuídos. Checkpoints de treinamento também precisam ser gravados periodicamente.

Se milhares de GPUs esperarem os dados de treinamento chegarem, equipamentos caríssimos ficam ociosos.

Data centers utilizam caches em SSD, pré-carregamento, formatos otimizados, particionamento e pipelines paralelos para manter os aceleradores ocupados. Dados podem passar do armazenamento distribuído para SSDs locais, depois para a DRAM do servidor e finalmente para a HBM.

Durante a gravação de checkpoints, o caminho se inverte. O sistema precisa retirar grandes volumes de dados dos aceleradores sem interromper o treinamento por tempo demais.

O HDD continua presente em camadas de grande capacidade. SSDs aparecem onde latência e taxa de acesso importam mais. DRAM funciona como área de trabalho. HBM alimenta diretamente o acelerador.

A antiga pirâmide de memória não desapareceu na era da inteligência artificial. Ela ficou maior.

Não existe memória perfeita, apenas escolhas menos ruins

Poderíamos perguntar por que não fabricar um computador usando apenas a tecnologia mais rápida disponível.

Porque a máquina ficaria cara, quente e pequena em capacidade.

Também poderíamos usar apenas a memória mais barata. Nesse caso, o processador passaria a maior parte do tempo esperando.

A engenharia procura um equilíbrio. Pequenas quantidades de memória muito rápida ficam perto do processamento. Grandes quantidades de armazenamento mais lento ocupam os níveis inferiores. Hardware, sistema operacional, compiladores e aplicações trabalham juntos para manter os dados certos no lugar certo.

Essa organização funciona tão bem que normalmente não percebemos sua existência. Abrimos um programa e vemos uma janela. Carregamos um jogo e observamos uma tela. Enviamos um prompt e recebemos uma resposta.

Por trás desse gesto simples, bytes podem ter saído de um SSD, atravessado a RAM, entrado em diferentes níveis de cache, chegado aos registradores e retornado. Em um data center de IA, talvez tenham circulado entre dezenas de aceleradores antes de produzir uma única palavra.

A computação moderna não depende apenas de realizar cálculos. Depende de alimentar esses cálculos sem deixar a máquina esperando.

O processador pode ser o componente que executa as instruções, mas a hierarquia de memória decide com que frequência ele realmente terá algo para fazer.

Entendendo Algoritmos

Algoritmo

Quando se fala em algoritmos, muitos pensam logo em uma linguagem específica: C, Python, Java, JavaScript. Mas, antes de escolher qualquer linguagem, existe uma camada mais básica, que é como a ideia do algoritmo. Ou seja, como escrever o passo a passo de forma clara, organizada e independente de qualquer tecnologia específica.

É disso que trata essa parte: montar uma maneira padrão de apresentar algoritmos, usando um tipo de pseudocódigo. A ideia é ter uma “linguagem de meio de campo”: não é código de verdade que se joga num compilador, mas também não é português solto. É um misto dos dois, pensado para que qualquer pessoa com noção básica de programação consiga entender a lógica sem ficar presa à sintaxe de uma linguagem concreta.

Por que usar uma forma padrão de escrever algoritmos?

Imagine que três pessoas diferentes queiram explicar o mesmo algoritmo:

  • uma escreve em C
  • outra escreve em Python
  • outra escreve em Java

As três versões fazem a mesma coisa, mas quem está aprendendo vai ter que entender três sintaxes, três jeitos de declarar variáveis, três estilos de laço. O risco é que a pessoa se perca nos detalhes da linguagem e não enxergue a ideia central do algoritmo.

Por isso se usa um pseudocódigo: uma linguagem simplificada, parecida com linguagens clássicas, mas sem detalhes de compilador, biblioteca padrão, tipos exatos etc. O que importa é:

  • os comandos básicos;
  • a forma como blocos de código são escritos;
  • o jeito de expressar condições, repetições, chamadas de procedimentos;
  • e alguma convenção mínima para vetores, matrizes, registros e ponteiros.

Essa padronização permite que toda a discussão sobre algoritmos gire em torno do raciocínio, não da sintaxe.

Blocos e indentação: quem está “dentro” de quem

Um conceito essencial é o de bloco de comandos.
Bloco é um conjunto de instruções que funcionam como uma unidade, o corpo de um se, o corpo de um enquanto, o conteúdo de um procedimento, e assim por diante.

Em muitas linguagens, blocos são marcados com palavras como begin e end ou com chaves { e }. Aqui, a ideia é usar algo que você provavelmente já viu:

Indentação (recuo na margem esquerda)

Funciona assim:

  • um comando que controla um bloco (por exemplo, um se ou um enquanto) aparece mais “para a esquerda”;
  • tudo o que estiver logo abaixo, um pouco mais recuado, faz parte do bloco controlado por ele;
  • quando o recuo volta, o bloco terminou.

É exatamente o estilo usado em linguagens como Python. Visualmente, fica muito mais fácil ver quais comandos pertencem a qual estrutura. Isso ajuda principalmente em algoritmos com vários níveis de aninhamento, por exemplo, um se dentro de um enquanto dentro de um para.

Atribuição: o famoso “recebe”

Outro ponto importante é a forma como se escreve a atribuição, isto é, o ato de colocar um valor dentro de uma variável.

Em vez de usar o sinal de igual simples, é comum escrever:

x := 5

Isso se lê como:

“x recebe 5”

O motivo de usar := é evitar confusão com matemática pura.
Em matemática, x = 5 é uma afirmação de que x é igual a 5, simétrico. Em programação, muitas vezes o igual significa “atribuí esse valor a essa variável”, que é uma operação assimétrica: o lado esquerdo é alterado, o lado direito é avaliado.

Com o :=, fica mais claro que se trata de uma ação: o valor anterior de x é substituído por um novo.

Podem aparecer expressões como:

soma := soma + x
i := i + 1
troca := A[i]
A[i] := A[j]
A[j] := troca

Todas elas seguem a mesma ideia: calcular o valor da expressão à direita e gravar o resultado na variável à esquerda.

Comandos de controle: decisões e repetições

Para que um algoritmo seja mais do que uma lista boba de comandos em sequência, ele precisa ser capaz de:

  • tomar decisões (fazer algo se uma condição for verdadeira);
  • repetir ações enquanto certas condições forem satisfeitas.

Os comandos mais comuns que aparecem nessa forma de apresentação de algoritmos são:

1. se ... então e se ... então ... senão

Eles representam o famoso if da maioria das linguagens.

Exemplo:

se x > 0 então
    escreve("x é positivo")

Ou, com alternativa:

se x > 0 então
    escreve("x é positivo")
senão
    escreve("x é menor ou igual a zero")

A condição é alguma expressão lógica (comparações, combinações com e, ou, etc.).
O bloco identado abaixo de então só é executado se a condição for verdadeira.
O bloco abaixo de senão só é executado se a condição for falsa.

2. enquanto ... faça

Representa um laço de repetição condicionada:

enquanto aindaTemPratoSujo faça
    lavaPrato()
    verificaSeAindaTemPratoSujo()

A leitura é: enquanto a condição for verdadeira, execute o bloco.
Assim que a condição se tornar falsa, o laço termina.

Esse tipo de laço é muito útil quando não se sabe de antemão quantas repetições serão necessárias. É como dizer “enquanto o usuário não acertar a senha, continue pedindo”.

3. para ... faça

Representa um laço de repetição em que a quantidade de iterações é conhecida ou determinada por um intervalo:

para i de 1 até n faça
    soma := soma + A[i]

Aqui a leitura é mais direta: o índice i percorre todos os valores de 1 até n, e para cada valor são executados os comandos do bloco. É o padrão quando se precisa percorrer elementos de um vetor, linhas de uma matriz etc.

4. pare

É um comando usado, dentro de um laço, para interromper a repetição antes do fim “natural”.

Por exemplo:

para i de 1 até n faça
    se A[i] = x então
        posicao := i
        pare

Nesse caso, assim que o elemento procurado é encontrado, o pare interrompe o laço, mesmo que i ainda não tenha chegado a n.

Variáveis simples: números, caracteres, lógico

Nesse estilo de algoritmos, assume-se que existem alguns tipos básicos de dados:

  • Inteiros: números sem parte decimal, positivos ou negativos.
  • Reais: números com parte decimal.
  • Caracteres: letras, dígitos, símbolos, normalmente representados entre aspas simples: 'a', 'Z', '7'.
  • Booleanos (lógicos): verdadeiro ou falso.

Uma variável é só um “nome” que aponta para uma posição de memória onde um valor desse tipo é armazenado.
Por exemplo:

i    : inteiro
media: real
letra: caractere
ok   : booleano

(As declarações formais nem sempre aparecem completas no pseudocódigo, mas esse é o espírito.)

Vetores: uma fileira de caixinhas numeradas

Um vetor é uma coleção de elementos todos do mesmo tipo, guardados em posições numeradas.
Você pode imaginar como uma fileira de caixinhas:

A[1], A[2], A[3], ..., A[n]

Cada posição é acessada por um índice. Alguns exemplos:

  • A[1] é o primeiro elemento.
  • A[i] é o elemento da posição i, que pode variar ao longo de um laço.
  • A[n] é o último.

É comum ver coisas como:

para i de 1 até n faça
    soma := soma + A[i]

ou:

A[i] := A[i] + 10

O importante é perceber que, quando aparece S[1..n], por exemplo, isso representa um vetor S com índices indo de 1 até n.

Matrizes: uma espécie de planilha

Uma matriz é uma generalização do vetor: em vez de uma dimensão, são duas (linha e coluna). É como uma planilha de Excel:

B[linha, coluna]

Por exemplo, uma matriz com n linhas e n colunas pode ser escrita como:

  • B[1..n, 1..n]

Para percorrer todos os elementos, usa-se dois laços aninhados:

para i de 1 até n faça
    para j de 1 até n faça
        C[i, j] := A[i, j] + B[i, j]

Aqui, C[i, j] recebe a soma dos elementos de A e B na mesma posição.

Matrizes são fundamentais em algoritmos numéricos, gráficos, simulações físicas etc.


Registros: vários campos sob um só nome

Às vezes, guardar um único valor por variável não é suficiente.
Pense em um “aluno”:

  • nome
  • matrícula
  • idade
  • média

Faria pouco sentido ter quatro vetores separados e tentar ficar sincronizando os índices. Em vez disso, entra o conceito de registro (similar ao struct em C).

Um registro é um agrupamento de campos. A notação típica é:

aluno.nome
aluno.matricula
aluno.idade
aluno.media

Ou seja, aluno é o registro, nome é um dos campos.
Dentro de algoritmos, é comum ver coisas como:

T.chave
T.info

onde T é um registro e chave, info são campos.

Registros são a base para estruturas mais ricas, como listas de alunos, tabelas de produtos, cadastros etc.

Ponteiros: referências para registros

Agora entra um conceito que, de início, assusta um pouco, mas é crucial para certas estruturas de dados: o ponteiro.

Um ponteiro é uma variável cujo valor não é um número comum, nem um caractere; é um endereço de memória. Em vez de guardar diretamente os dados, o ponteiro “aponta” para onde os dados estão.

Uma analogia:

  • o registro é a casa;
  • o ponteiro é o endereço da casa anotado num papel.

A notação pode usar algo como:

pt↑.info

Aqui, pt é o ponteiro. O símbolo indica “o registro para o qual ele aponta”. E .info é um campo desse registro.

Então, pt↑.info significa:

“O campo info do registro cujo endereço está armazenado em pt.”

Isso é muito útil em estruturas como listas encadeadas, em que cada nó guarda não só dados, mas também um ponteiro para o próximo nó.


Procedimentos, funções e parâmetros por referência

Para organizar algoritmos, é comum dividir o código em sub-rotinas:

  • procedimentos: executam uma ação, mas não devolvem diretamente um valor;
  • funções: devolvem um valor (por exemplo, o resultado de um cálculo).

A passagem de parâmetros pode ser vista de duas maneiras principais:

  1. Por valor
    O procedimento recebe uma cópia do valor. Se ele alterar a variável dentro da sub-rotina, isso não se reflete fora.

  2. Por referência
    O procedimento recebe algo que permite acessar diretamente a variável original (por exemplo, o endereço dela). Se o procedimento alterar o parâmetro, a variável do chamador também é alterada.

Na forma de apresentação que estamos usando, é comum assumir passagem por referência. Isso torna natural que um procedimento possa modificar vetores, registros etc., sem precisar devolvê-los explicitamente.

Uma analogia simples:

  • Por valor: você tira uma fotocópia de um documento e deixa a cópia com a pessoa; o original não muda.
  • Por referência: você entrega o documento original; a pessoa pode riscar, corrigir, grampear, e quando ele volta está diferente.

Comentários: explicando em português o que o código faz

Junto do pseudocódigo, aparecem os comentários, geralmente marcados por um símbolo especial, como %.

Tudo o que vier depois desse símbolo, na mesma linha, é ignorado na interpretação do algoritmo, e serve apenas para seres humanos.

Por exemplo:

soma := 0          % zera o acumulador
para i de 1 até n faça
    soma := soma + A[i]    % adiciona o elemento i

Comentários ajudam muito a entender a intenção do algoritmo:

  • explicar o que uma variável representa;
  • indicar por que um laço termina em tal condição;
  • apontar detalhes importantes que, só olhando o código, podem não ficar claros.

Para quem está estudando, vale a pena ler os comentários com calma, porque eles carregam a explicação em linguagem natural.

Um exemplo prático: inverter uma sequência em um vetor

Tudo isso ganha mais sentido com um exemplo.
Considere um vetor S[1..n] com n elementos. O objetivo é inverter a ordem desses elementos dentro do próprio vetor.

Se a sequência for:

S = [ 10, 20, 30, 40, 50 ]

quer-se obter:

S = [ 50, 40, 30, 20, 10 ]

A ideia intuitiva é:

  1. Trocar o primeiro com o último.
  2. Trocar o segundo com o penúltimo.
  3. Continuar assim até chegar no meio do vetor.

Não faz sentido trocar o elemento do meio com ele mesmo. Por isso, se n é ímpar, esse elemento central fica onde está.

Como transformar isso em pseudocódigo?

Primeiro, pensa-se em um laço:

  • Vamos usar um índice i começando em 1 e indo até a metade de n.
  • Para cada i, vamos trocar S[i] com S[n − i + 1].

Por que n − i + 1?

  • Quando i = 1, temos n − 1 + 1 = n, o último elemento.
  • Quando i = 2, temos n − 2 + 1 = n − 1, o penúltimo.
  • E assim por diante.

A “metade de n” é formalizada usando a função piso (a parte inteira da divisão):

  • ⌊n/2⌋

Se n = 5, por exemplo, ⌊5/2⌋ = 2 (a parte inteira de 2,5). Então i vai de 1 a 2:

  • i = 1 → troca o 1º com o 5º;
  • i = 2 → troca o 2º com o 4º;
  • o 3º (do meio) permanece.

O pseudocódigo fica algo assim, em espírito:

para i de 1 até ⌊n/2⌋ faça
    temp := S[i]
    S[i] := S[n - i + 1]
    S[n - i + 1] := temp

Repare na variável temp: ela é uma variável temporária usada para não perder o valor de S[i] enquanto se sobrescreve S[i] com S[n − i + 1]. É como usar um copo extra para trocar o conteúdo de dois copos com água, sem derramar.

Esse pequeno algoritmo mostra:

  • como usar vetores;
  • como usar o laço para;
  • como manipular índices;
  • como usar atribuições e uma variável temporária;
  • como a indentação deixa claro qual é o bloco repetido.

Recursividade

Quando o assunto é algoritmos, uma palavra que sempre aparece e causa certo arrepio em quem está começando é recursividade. Ela tem cara de conceito difícil, parece algo meio filosófico, mas a ideia central é bem direta: um procedimento que se chama a si mesmo para resolver versões menores do mesmo problema.

Dá para imaginar como uma pessoa que recebe uma tarefa complicada, divide em partes menores, passa uma parte para outra pessoa, que por sua vez divide de novo e assim por diante, até o pedaço ficar simples o suficiente para ser resolvido na hora. A recursão faz algo assim, só que todas “essas pessoas” são, na verdade, o mesmo procedimento sendo chamado várias vezes, com parâmetros diferentes.

O que é recursividade

Um algoritmo recursivo é aquele em que uma função ou procedimento, em algum ponto do seu corpo, chama a si próprio.

Para não virar um loop infinito, sempre aparecem dois ingredientes:

  1. Casos base
    Situações simples, em que o problema já está pequeno o bastante para ser resolvido diretamente, sem nova chamada.

  2. Passo recursivo
    Regras que dizem como transformar o problema grande em um ou mais problemas menores do mesmo tipo.

Se só existir passo recursivo, o algoritmo entra em chamada atrás de chamada e nunca termina. Se só existir caso base, não há recursão. O jogo está no equilíbrio entre os dois.

Exemplo clássico: o fatorial

Um exemplo muito usado para explicar recursividade é o fatorial de um número inteiro n ≥ 0.

Definição:

  • 0! = 1
  • 1! = 1
  • n! = n × (n − 1)! para n ≥ 2

Ou seja, o fatorial de n é n vezes o fatorial de n − 1. A própria definição já é recursiva.

Se n = 5, por exemplo:

  • 5! = 5 × 4!
  • 4! = 4 × 3!
  • 3! = 3 × 2!
  • 2! = 2 × 1!
  • 1! = 1

Quando o algoritmo recursivo é escrito, ele costuma seguir exatamente essa definição:

  • Caso base: se n é 0 ou 1, retorna 1.
  • Passo recursivo: se n é maior que 1, retorna n × fatorial(n − 1).

De forma conceitual:

função fat(n)
    se n ≤ 1
        devolve 1
    senão
        devolve n × fat(n − 1)

Cada chamada guarda uma “tarefa pendente”: “preciso multiplicar n pelo resultado da função em n − 1”. Essas tarefas ficam empilhadas na memória até atingir o caso base. Quando o caso base devolve 1, as multiplicações começam a ser feitas na volta.

Essa combinação de “definição recursiva” com “prova por indução” é muito forte em matemática e programação: primeiro se mostra como resolver o problema para o caso simples, depois se mostra como resolver o caso geral supondo que o caso menor já está resolvido. A recursão traduz isso em código.

O que acontece nos bastidores da recursão

Visualmente, quando um procedimento recursivo é chamado, o computador:

  1. Guarda o estado atual da execução (variáveis locais, posição onde deve voltar) em uma pilha.
  2. Entra na nova chamada com parâmetros diferentes.
  3. Repete esse processo a cada chamada recursiva.
  4. Quando encontra um caso base, começa a “desempilhar”, voltando passo a passo.

Essa pilha é uma estrutura de dados do próprio sistema: o último a entrar é sempre o primeiro a sair. Por isso, em problemas com recursão muito profunda, pode acontecer de “estourar a pilha” (stack overflow), porque há chamadas demais pendentes ao mesmo tempo.

Do ponto de vista de quem está aprendendo, não é preciso dominar todos os detalhes de implementação da pilha para entender recursividade, mas é fundamental ter em mente que:

  • cada chamada recursiva ocupa memória;
  • recursão muito profunda exige cuidado;
  • muitas vezes, um algoritmo recursivo elegante pode ser reescrito de forma iterativa (com laços) para economizar memória.

Outro exemplo: sequência de Fibonacci

Outro exemplo famoso é a sequência de Fibonacci:

  • F(0) = 0
  • F(1) = 1
  • F(n) = F(n − 1) + F(n − 2), para n ≥ 2

Mais uma vez, a própria definição é recursiva. Um algoritmo simples copia isso exatamente:

função fib(n)
    se n ≤ 1
        devolve n
    senão
        devolve fib(n − 1) + fib(n − 2)

Esse código é curto e parece muito bonito, mas esconde um problema: explosão de chamadas. Cada fib(n) chama duas vezes a função para valores menores, que por sua vez chamam outras duas e assim por diante. O número de chamadas cresce de forma muito rápida, quase como 2.

Esse exemplo costuma ser usado para mostrar dois pontos:

  • recursão pode deixar o código mais próximo da definição matemática;
  • recursão inocente, sem cuidado, pode ter uma complexidade de tempo péssima.

Para resolver isso, é comum reescrever Fibonacci de forma iterativa ou usar técnicas como memoização, em que se guardam os resultados já calculados para evitar recomputações.

Exemplo visual: a Torre de Hanói

A Torre de Hanói é um quebra-cabeça perfeito para enxergar recursividade em ação.

Cenário:

  • existem três pinos (A, B, C);
  • vários discos de tamanhos diferentes empilhados no pino A, com o maior embaixo e o menor em cima;
  • objetivo: mover toda a pilha para o pino B;
  • regras: só é permitido mover um disco por vez e nunca se pode colocar um disco maior sobre um menor.

Parece complexo, mas recursão organiza o raciocínio.

Para n discos:

  1. mover os n − 1 discos de cima de A para C, usando B como apoio;
  2. mover o disco maior (o último) de A para B;
  3. mover os n − 1 discos de C para B, usando A como apoio.

Aqui aparecem claramente:

  • o caso base: se n = 1, basta mover um disco de um pino a outro;
  • o passo recursivo: resolver o problema com n discos supondo que existe uma solução para n − 1.

Cada chamada da função para n discos gera duas chamadas para n − 1 discos. O número total de movimentos cresce como 2 − 1, ou seja, rapidamente.

Esse problema é muito usado não só porque é visual, mas porque mostra bem o padrão:

“Para resolver o caso grande, imagine que já sabe resolver o caso menor.”

Essa frase é praticamente o coração da recursão.

Como pensar recursivamente

Para quem está começando, pensar recursivamente é quase como trocar o “modo manual” de raciocínio por um “modo declarativo”.

Em vez de ficar imaginando todos os passos de baixo nível, tenta-se responder a duas perguntas:

  1. Qual é a versão mais simples desse problema?
    Esse será o caso base.

  2. Como posso reduzir o problema atual a uma ou mais versões menores do mesmo problema?
    Esse será o passo recursivo.

Por exemplo, ao pensar no fatorial:

  • caso simples: fatorial de 0 ou 1 é igual a 1;
  • redução: fatorial de n é n vezes o fatorial de n − 1.

Na Torre de Hanói:

  • caso simples: mover 1 disco de um pino para outro;
  • redução: para n discos, mover n − 1 para o pino auxiliar, depois o maior, depois os n − 1 de volta.

Um ponto importante: é preciso garantir que essas reduções realmente aproximem o problema do caso base. Se o problema ficar do mesmo tamanho ou maior, não há progresso e a recursão nunca termina.

Relação entre recursão e laços

Quase todo algoritmo recursivo pode ser reescrito de forma iterativa, usando laços enquanto ou para.

No caso do fatorial, por exemplo, é simples escrever:

  • começar com um acumulador igual a 1;
  • multiplicar esse acumulador por 2, depois por 3, depois por 4, até chegar em n.

A recursão, nesse caso, não é a única opção, mas é uma forma natural de traduzir a definição matemática.

Há problemas em que a recursão facilita muito o raciocínio, como em árvores (percorrer uma árvore de diretórios, por exemplo) ou em algoritmos de divisão e conquista (quebrar o problema em subproblemas). Em outros, a recursão só complica ou gasta recursos à toa, e é melhor ficar nos laços.

De forma prática:

  • recursão favorece clareza em muitos cenários;
  • iteração costuma ser melhor em termos de uso de memória e, às vezes, de desempenho.

Alguns compiladores conseguem transformar internamente certas recursões em laços, principalmente nos casos de recursão de cauda, em que a chamada recursiva é a última instrução do procedimento. Isso reduz o custo de memória, mas depende de otimizações específicas.

Medindo o custo de algoritmos recursivos

Quando se analisa a eficiência de um algoritmo recursivo, uma técnica comum é:

  1. Contar quantas chamadas recursivas são feitas em função do tamanho da entrada.
  2. Calcular o custo de cada chamada isolada, sem considerar o custo das outras chamadas recursivas.
  3. Combinar as duas coisas para obter uma fórmula de tempo total.

No fatorial recursivo:

  • cada chamada reduz n em 1;
  • há n chamadas até chegar a 1;
  • cada chamada faz, basicamente, uma multiplicação;
  • resultado: tempo proporcional a n (complexidade linear).

Na Torre de Hanói:

  • o número de movimentos satisfaz a relação T(n) = 2·T(n − 1) + 1;
  • isso leva a T(n) = 2 − 1;
  • resultado: tempo cresce de forma exponencial.

Na sequência de Fibonacci com a definição ingênua:

  • cada fib(n) chama fib(n − 1) e fib(n − 2);
  • o número de chamadas cresce de forma aproximadamente proporcional a ϕ, onde ϕ é o número de ouro;
  • o custo explode rapidamente.

Essa análise mostra que recursão não é sinônimo de lentidão, mas que é importante entender a relação de chamadas para não ser surpreendido por algoritmos aparentemente inocentes.

Quando usar recursão vale a pena

Alguns tipos de problemas praticamente “pedem” recursão, porque a estrutura interna deles é recursiva:

  • Estruturas em árvore
    Percorrer pastas e arquivos em um sistema de arquivos; percorrer nós de uma árvore binária; interpretar expressões matemáticas aninhadas.

  • Dividir para conquistar
    Ordenar um vetor quebrando-o em dois (como no mergesort ou quicksort); encontrar o k-ésimo menor elemento dividindo o problema em subvetores.

  • Problemas definidos recursivamente
    Fatorial, Fibonacci, entre outros, em que a definição formal já usa o caso anterior.

Em todos esses casos, a recursão tende a dar um código mais curto e mais próximo da descrição conceitual do problema.

Cuidados ao usar recursividade

Alguns cuidados práticos:

  1. Definir claramente o caso base
    Sem caso base bem definido, o algoritmo não termina. E se o caso base estiver errado, pode terminar com resposta errada.

  2. Garantir progresso real
    Toda chamada recursiva deve aproximar a entrada do caso base. Reduzir n, encurtar uma lista, subir ou descer em uma árvore.

  3. Pensar na profundidade da recursão
    Em entradas muito grandes, o número de chamadas aninhadas pode ser alto demais e estourar a pilha de execução.

  4. Avaliar o custo de tempo
    É importante saber se a recursão gera apenas uma chamada por nível (como no fatorial) ou várias (como em Fibonacci ingênuo ou Torre de Hanói).

  5. Reescrever recursões perigosas
    Quando a recursão gera explosão de chamadas, vale buscar alternativas iterativas ou técnicas de otimização.

Recursividade como forma de pensar

Recursividade não é só uma técnica de programação, é um jeito de enxergar problemas. Sempre que um problema puder ser descrito como:

“versão simples” + “versão geral em termos de versões menores”

há espaço para um raciocínio recursivo.

Dominar recursão ajuda a:

  • compreender melhor provas por indução;
  • entender estruturas como árvores, grafos e expressões aninhadas;
  • escrever algoritmos mais próximos da descrição matemática.

Para quem está começando,:

  • praticar com exemplos simples (fatorial, máximo de um vetor, soma de elementos);
  • depois avançar para exemplos mais ricos (árvores, Hanói, algoritmos de ordenação recursivos);
  • sempre parar para identificar o caso base e o passo recursivo, como se estivesse contando uma história que vai se simplificando até chegar no final natural.

Quando essa forma de pensar entra no repertório, recursividade deixa de parecer um “bicho de sete cabeças” e passa a ser mais uma ferramenta, poderosa, ao lado dos laços, das estruturas de dados e de todo o resto que compõe o universo dos algoritmos.

Algoritmo eficiente

Quando alguém começa a estudar programação, sempre surge aquela pergunta que parece meio abstrata: “esse algoritmo é eficiente?”. Todo mundo entende o que é “funcionar” ou “não funcionar”, mas medir o quão pesado é um algoritmo já parece conversa de matemático. É justamente disso que trata o tema complexidade de algoritmos.

Imaginar que cada algoritmo tem um “custo” para ser rodado, e que esse custo depende do tamanho da entrada. Não é a mesma coisa somar 10 números e somar 10 milhões. Não é a mesma coisa procurar um nome em uma lista com 20 pessoas e em uma com 20 milhões. Complexidade é a forma organizada de responder a perguntas como:

  • Quantos passos esse algoritmo executa quando a entrada cresce?
  • Quanto tempo ele tende a gastar?
  • Quanta memória ele ocupa?

Tudo isso sem precisar ficar medindo com cronômetro em cada computador diferente.

Por que não basta medir com tempo de relógio

À primeira vista, alguém pode pensar: “por que não roda o programa e mede o tempo?”. Essa estratégia até funciona em testes práticos, mas tem vários problemas.

O tempo medido depende de coisas que não têm nada a ver com o algoritmo em si:

  • processador mais rápido ou mais lento;
  • quantidade de memória;
  • se o sistema operacional está ocupado com outras tarefas;
  • linguagem e compilador usados.

Um mesmo algoritmo pode parecer mais rápido em um computador e mais lento em outro, apenas por causa do hardware ou da implementação. Fica difícil comparar duas ideias de solução usando só segundos como unidade.

Por isso, quando se fala de complexidade, entra outro tipo de análise, teórica, baseado em um modelo idealizado. Em vez de se preocupar com o tempo em segundos, olha-se para o número de passos que o algoritmo executa, em função do tamanho da entrada.

Tamanho da entrada: o tal do “n”

Quase sempre aparece uma letrinha para representar o tamanho do problema. Em geral se utiliza n:

  • se o algoritmo recebe uma lista, n é o número de elementos;
  • se mexe com uma matriz quadrada, n pode ser a quantidade de linhas (e colunas);
  • se trata de um texto, n pode ser a quantidade de caracteres;
  • se trabalha com um grafo, n pode ser o número de vértices ou de arestas, conforme o contexto.

Complexidade de tempo passa a ser uma função como:

  • T(n) = número de passos que o algoritmo executa para uma entrada de tamanho n.

A grande pergunta deixa de ser “quantos segundos”, e vira “como T(n) cresce quando n aumenta”.

O que é um “passo” no algoritmo

Para conseguir conversar sobre complexidade sem cair em detalhes esotéricos, costuma-se definir um passo como um pequeno bloco de operações básicas:

  • uma soma;
  • uma comparação;
  • uma atribuição de variável;
  • acessar uma posição de vetor;
  • coisas desse nível.

O truque é agrupar essas operações em blocos que se repetem muitas vezes. Em vez de contar uma por uma, diz-se que:

  • determinado trecho é um passo;
  • o algoritmo faz esse passo tantas vezes.

Por exemplo, se houver um laço que percorre um vetor do primeiro ao último elemento somando tudo, o passo pode ser:

  • acessar o elemento atual;
  • somá-lo a um acumulador;
  • avançar o índice.

Uma repetição do laço corresponde a um passo. Se o vetor tiver n elementos, o laço executa n passos.

Operação dominante: o que realmente pesa

Nem todos os trechos do algoritmo têm o mesmo peso.
Um pequeno bloco de inicialização pode rodar só uma vez, enquanto outro bloco pode ser repetido milhares ou milhões de vezes.

Complexidade de algoritmos costuma prestar atenção na operação dominante, isto é, naquela parte do código que é repetida mais vezes e que, de fato, manda na conta final.

Se um algoritmo faz:

  1. um pequeno preparo inicial com 10 ou 20 passos;
  2. depois roda um laço que executa mil passos;
  3. no fim imprime um resultado;

a parte que mais contribui para o custo é o laço, não o preparo nem a impressão.
Quando se escreve a função T(n), normalmente as parcelas menores são engolidas pela que cresce mais rápido.

Ignorando detalhes que não mudam a ordem

Outro ponto fundamental: em análise de complexidade, não se liga para constantes exatas.

Se um algoritmo, para uma entrada de tamanho n, faz:

  • 3n + 5 passos;

e outro faz:

  • 7n + 12 passos;

ambos crescem “na mesma linha” quando n é muito grande. A diferença entre 3n e 7n é real, claro, mas em termos de ordem de grandeza ambos são proporcionais a n. Costuma-se dizer que os dois têm complexidade linear no tamanho da entrada.

Na prática, isso significa que:

  • dobra o tamanho da entrada → o número de passos dobra aproximadamente;
  • triplica o tamanho da entrada → o número de passos triplica, e assim por diante.

Código real sempre tem constantes e detalhes finos, mas a análise de complexidade está interessada em como o esforço cresce lá longe, quando n explode.

Exemplos concretos: somar e multiplicar matrizes

Um exemplo clássico de comparação de complexidade aparece quando se trabalha com matrizes. Imagine duas matrizes quadradas A e B, cada uma com n linhas e n colunas.

Soma de matrizes

Para somar duas matrizes, basta somar elemento a elemento:

  • resultado[i, j] = A[i, j] + B[i, j].

É preciso visitar cada posição uma vez.
Em uma matriz n × n, existem n² elementos. Logo:

  • o número de somas é n²;
  • a complexidade de tempo cresce como .

Esse padrão recebe o nome de complexidade quadrática: se o tamanho linear (n) dobra, a quantidade de operações cresce por um fator de quatro.

Produto de matrizes

O produto de duas matrizes já é mais trabalhoso.
O elemento C[i, j] é calculado como:

  • C[i, j] = soma, para k de 1 até n, de A[i, k] × B[k, j].

Ou seja:

  • para cada par (i, j), é preciso fazer n multiplicações e n − 1 somas;
  • existem n² pares (i, j).

O total de operações fica proporcional a n³.
É um salto enorme: se o n dobra, o custo aumenta oito vezes; se n triplica, sobe para 27 vezes.

Aí já se fala em complexidade cúbica, típica de certos algoritmos que usam três laços aninhados.

Melhor caso, pior caso e caso médio

Um algoritmo nem sempre executa o mesmo número de passos para toda entrada de mesmo tamanho. Isso aparece bem em operações de busca.

Imagine uma função que procura um número x em um vetor de n posições, percorrendo da primeira até a última:

  • Se x estiver logo na primeira posição, o algoritmo faz pouquíssimo trabalho.
  • Se x não estiver no vetor, o algoritmo precisa olhar todas as posições antes de desistir.

Para falar disso com mais precisão, utiliza-se três conceitos:

  • Complexidade de melhor caso:
    número mínimo de passos que o algoritmo pode executar para entradas de tamanho n.
    No exemplo da busca, seria quando o elemento procurado está na primeira posição.

  • Complexidade de pior caso:
    número máximo de passos entre todas as entradas de tamanho n.
    Na busca, ocorre quando o elemento não está presente ou está na última posição.

  • Complexidade de caso médio:
    média do número de passos, considerando alguma distribuição de probabilidades sobre as entradas.
    Por exemplo, supondo que todas as posições sejam igualmente prováveis para o elemento procurado.

Na prática, quem projeta algoritmos costuma se preocupar bastante com o pior caso, porque ele funciona como uma espécie de “garantia de teto”: assegura que o tempo nunca extrapola certa ordem de grandeza.

O caso médio é muito interessante teoricamente, mas depende da hipótese de como as entradas são distribuídas. Se essa hipótese estiver errada, a conta não representa bem o comportamento real.

Um exemplo com dois comportamentos diferentes

Para ficar mais claro, imagine um algoritmo muito simples:

  • a entrada tem um parâmetro x, que pode ser 0 ou 1, e duas matrizes n × n;
  • se x = 0, o algoritmo soma as matrizes;
  • se x = 1, o algoritmo multiplica as matrizes.

Aqui, o tamanho da entrada, em termos de n, é o mesmo. Só que o “caminho interno” muda completamente:

  • quando x = 0, o tempo cres­ce como n² (soma de matrizes);
  • quando x = 1, o tempo cresce como n³ (produto de matrizes).

Olhando por esse lado:

  • melhor caso: quando sempre se soma, complexidade quadrática;
  • pior caso: quando sempre se multiplica, complexidade cúbica;
  • caso médio: depende de com que frequência x é 0 ou 1.

Se, por exemplo, x = 0 tiver probabilidade q, e x = 1 tiver probabilidade 1 − q, a complexidade média Tm(n) seria algo como:

  • Tm(n) ≈ q · n² + (1 − q) · n³.

Quando n é grande, o termo n³ domina sempre que 1 − q não for zero. Isso significa que, mesmo se multiplicar for um pouco raro, o custo do algoritmo vai ser puxado para a ordem cúbica.

Complexidade de tempo e complexidade de espaço

Embora quase sempre o foco das conversas esteja na complexidade de tempo, existe também a complexidade de espaço, que mede quanta memória o algoritmo consome.

Alguns algoritmos economizam tempo às custas de usar mais memória, guardando resultados intermediários para evitar recomputações. Outros fazem o contrário: usam pouca memória, mas repetem cálculos.

Complexidade de espaço também é expressa em função de n:

  • um algoritmo que precisa de um vetor de n posições tem espaço proporcional a n;
  • um algoritmo que trabalha com uma matriz auxiliar n × n usa memória proporcional a n²;
  • um algoritmo recursivo pode consumir espaço proporcional à profundidade das chamadas.

Em muitos problemas, a memória disponível é tão importante quanto o tempo.
Um algoritmo teoricamente brilhante, mas que exige uma quantidade absurda de memória, pode ser inviável na prática.

Crescimentos típicos que aparecem

Quando se começa a analisar algoritmos, algumas formas de crescimento aparecem com frequência:

  • Constante: o esforço não depende de n (ou cresce tão pouco que é considerado fixo).
    Exemplo: retornar o primeiro elemento de uma lista que já está em memória.

  • Logarítmico: cresce como log n, muito devagar.
    Exemplo clássico: busca binária em lista ordenada.

  • Linear: cresce proporcionalmente a n.
    Exemplo: percorrer um vetor fazendo uma operação simples em cada posição.

  • Quadrático: cresce como n².
    Exemplo: dois laços aninhados que percorrem todos os pares (i, j) de um conjunto com n elementos.

  • Cúbico: cresce como n³, como no produto ingênuo de matrizes n × n.

  • Exponencial: cresce como 2, 3 ou algo do tipo.
    Exemplo: certos algoritmos recursivos que exploram todas as combinações possíveis.

Na prática, esse tipo de classificação ajuda a ter uma intuição imediata:

  • linear é, em geral, aceitável mesmo para entradas muito grandes;
  • quadrático pode ser problemático se n for da ordem de milhões;
  • cúbico costuma ser viável só para n relativamente pequenos;
  • exponencial explode tão rápido que só serve para tamanhos bem modestos de entrada.

Um modelo mental: como olhar um algoritmo e “sentir” a complexidade

Quando se pega um algoritmo escrito, uma forma simples de começar a analisar é:

  1. Identificar os laços principais (para, enquanto).
    Ver quantas vezes cada laço se repete em função de n.

  2. Ver se existem laços aninhados.
    Um laço dentro de outro tende a gerar multiplicação de fatores: se o laço externo roda n vezes e o interno também n vezes, já aparece um n².

  3. Observar se há chamadas recursivas.
    Contar quantas chamadas são geradas e quanto cada uma custa.

  4. Ignorar por enquanto os trechos que rodam poucas vezes (inicializações, impressões simples).
    Esses trechos raramente mudam a ordem de grandeza do custo.

Por exemplo:

  • Um único laço que vai de 1 até n, fazendo operações simples, costuma dar complexidade linear.
  • Dois laços um dentro do outro, ambos indo de 1 até n, costumam dar complexidade quadrática.
  • Três laços aninhados, cada um de 1 até n, tendem a levar a complexidade cúbica.

Claro que existem exceções, mas esse raciocínio já ajuda a criar uma espécie de “radar” intuitivo.

Por que essa história toda importa tanto

À primeira vista, toda essa conversa de “ordem de grandeza”, “n²”, “n³” pode parecer um pouco distante do mundo real. Só que, em sistemas de verdade, o impacto é enorme.

Imagine duas situações:

  1. Um algoritmo com complexidade n² rodando com n = 1000 precisa de algo em torno de 1 milhão de operações.
  2. Um algoritmo com complexidade n³ rodando com n = 1000 precisa de algo em torno de 1 bilhão de operações.

Agora pense se o n cresce para 10 mil:

  • n² vira 100 milhões;
  • n³ vira 1 trilhão.

Essa diferença pode ser a fronteira entre um sistema que responde em segundos e um que simplesmente não termina em tempo útil. É por isso que, em áreas como bancos de dados, redes, criptografia, inteligência artificial, tudo gira em torno de algoritmos que consigam lidar com dados enormes sem explodir o custo de tempo ou memória.

Complexidade de algoritmos é justamente o ferramental teórico para:

  • comparar duas soluções possíveis;
  • saber se vale a pena trocar um algoritmo por outro;
  • prever como o sistema vai se comportar quando o volume de dados aumentar.

Quem domina esse jeito de pensar deixa de ver código apenas como “faz ou não faz” e passa a enxergar uma camada a mais: “faz, mas com qual custo?”. Essa mudança de olhar é o que separa um simples “programinha que funciona” de um software robusto, planejado para sobreviver a entradas cada vez maiores.

Complexidade dos algoritmos

Quando alguém começa a mexer com complexidade de algoritmos, aparece uma notação que parece meio esquisita à primeira vista: o tal do O grande, ou notação O. Muitos encaram aquilo como um símbolo misterioso grudado em fórmulas do tipo O(n²) ou O(n³) e segue em frente sem entender direito o que está acontecendo ali.

A ideia, na verdade, é bem pé no chão: a notação O é um jeito compacto de dizer “qual é a ordem de crescimento” do custo de um algoritmo, ignorando detalhes que, para entradas muito grandes, quase não fazem diferença. Em vez de anotar uma expressão cheia de termos, a notação O funciona como um resumo do que realmente manda na conta.

Por que resumir fórmulas de complexidade?

Imagine que você analisou um algoritmo e chegou nesta expressão para o número de passos:

  • T(n) = 3n + 20.

Aqui, n é o tamanho da entrada (números em um vetor, por exemplo) e T(n) é quantos “passinhos básicos” o algoritmo faz. Em termos matemáticos, está lindo. Agora pense no que acontece quando n cresce:

  • se n = 10, T(10) = 50;
  • se n = 1.000, T(1000) = 3.020;
  • se n = 1.000.000, T(1.000.000) ≈ 3.000.020.

Repare que aquele + 20 fica cada vez mais irrelevante. Quando se está lidando com milhões de elementos, pouco importa se faz 3.000.000 ou 3.000.020 operações. A parte que realmente manda é o 3n.

Por causa disso, em análise de algoritmos se adotam duas simplificações importantes :

  1. Ignorar constantes multiplicativas.
    Se o número de passos é 3n, diz-se que é “da ordem de n”. Aquela constante 3 some do radar.

  2. Ignorar termos de menor grau.
    Se o número de passos é n² + n, o termo domina quando n cresce. O + n é pequeno perto de para valores grandes de n.

Alguns exemplos típicos:

  • 3n é aproximado por n;
  • n² + n é aproximado por ;
  • 6n³ + 4n − 9 é aproximado por .

Em vez de ficar repetindo “estamos ignorando constantes e termos menores”, usa-se um operador matemático que incorpora essa ideia: a notação O.

Definição intuitiva da notação O

Pense em duas funções:

  • f(n): o custo real (ou uma expressão) do seu algoritmo;
  • h(n): uma função mais simples, que vai servir como limite superior para f(n).

Dizer que f(n) é O(h(n)) significa, em termos informais:

“Para valores grandes de n, f(n) cresce no máximo como uma constante vezes h(n).”

Usando a definição mais formal, costuma-se escrever assim :

f(n) = O(h(n)) quando existe uma constante c > 0 e um valor n₀ tal que, para todo n > n₀,
f(n) ≤ c · h(n).

Ou seja:

  • a partir de um certo ponto n₀,
  • h(n) serve como um teto assintótico para f(n),
  • talvez multiplicada por alguma constante c.

A palavra chave é assintótico: o interesse está no comportamento para n grande, não em valores pequenos e detalhes finos.

Exemplos concretos para pegar o jeito

Alguns exemplos clássicos ajudam a “sentir” a notação O :

  1. f(n) = n² − 1

    • Para n grande, −1 não faz diferença.
    • Dá para encontrar uma constante c que torne n² − 1 ≤ c · n² a partir de algum n₀.
    • Então se escreve:
      f(n) = O(n²).

    Também é verdade que f(n) = O(n³), porque n³ cresce ainda mais rápido que n². Só que O(n²) é uma descrição bem mais ajustada.

  2. f(n) = 403

    • O número de passos é constante, não depende de n.
    • Diz-se que f(n) é O(1), lido como “ordem de 1”.
    • É um jeito de dizer: “não cresce com o tamanho da entrada”.
  3. f(n) = 5 + 2 log n + 3 (log n)²

    Para n grande, o termo que domina é (log n)². Os outros ficam relativamente pequenos perto dele.
    Então se escreve:

    • f(n) = O((log n)²).

    É verdade também que f(n) = O(n), porque n cresce ainda mais rápido que (log n)². Mas, se a intenção é descrever bem a ordem de grandeza, (log n)² é muito mais fiel.

  4. f(n) = 3n + 5 log n + 2

    Aqui, o termo dominante é o 3n. Os outros são menores para n grande.
    Fica:

    • f(n) = O(n).
  5. f(n) = 5·2ⁿ + 5n¹⁰

    Mesmo que n¹⁰ pareça enorme, o termo 2ⁿ cresce MUITO mais rápido do que qualquer potência fixa de n.
    Então, assintoticamente:

    • f(n) = O(2ⁿ).

Esses exemplos mostram a lógica geral: procura-se o termo que cresce mais rápido quando n aumenta, e é esse termo que aparece dentro da notação O.

Propriedades úteis da notação O

A notação O se comporta de maneira bem amigável com operações simples entre funções. Algumas propriedades são usadas o tempo todo :

Se g(n) e h(n) são funções positivas e k é uma constante, então:

  1. O(g + h) = O(g) + O(h)
  2. O(k · g) = k · O(g) = O(g)

Traduzindo em linguagem mais direta:

  • Quando você soma duas funções, a ordem de grandeza fica na mesma escala da maior delas.
    Exemplo: O(n² + n³) vira O(n³).

  • Multiplicar uma função por uma constante não muda sua ordem de grandeza.
    Exemplo: O(7n²) continua sendo O(n²).

Isso combina com a ideia de “ignorar constantes e termos menores”. Na prática, quando você vê uma expressão com vários termos, olha para aquele que domina o crescimento e joga o resto para baixo do tapete da notação O.

Como isso conversa com complexidade de algoritmos

Até aqui, falamos de funções em abstrato. Mas o objetivo real é descrever a complexidade de tempo (ou de espaço) de algoritmos usando essa notação.

Suponha que você já contou os passos de vários algoritmos simples:

  • Um algoritmo que inverte um vetor faz sempre ⌊n/2⌋ trocas.
    Para n grande, isso é da ordem de n.
    Diz-se: complexidade O(n).

  • Um algoritmo iterativo para fatorial faz n multiplicações, uma para cada valor de 2 até n.
    De novo, isso é proporcional a n.
    Complexidade: O(n).

  • A soma de duas matrizes n × n faz n² somas.
    Complexidade: O(n²).

  • O produto de duas matrizes n × n, na forma ingênua com três laços aninhados, faz um número de operações proporcional a n³.
    Complexidade: O(n³).

Repare como a notação O esconde todos os detalhes da contagem exata (se é n², n² + 3n, 2n³ etc.) e guarda só a “classe” do crescimento.

Essa simplificação tem duas grandes vantagens:

  1. Permite comparar algoritmos de forma independente de constante e de máquina.
  2. Foca no que realmente importa em aplicações grandes: como o custo explode (ou não) quando a entrada cresce.

Por que um mesmo f(n) pode ter vários “O( )” diferentes?

Uma dúvida natural: se n² − 1 é O(n³) e também é O(2ⁿ), por que escolher O(n²)? Não está tudo certo?

Sim, está. A definição permite dizer que n² − 1 é O(n³), O(n⁴), O(2ⁿ) e assim por diante, porque todas essas funções crescem mais rápido que . A questão é que algumas descrições são bem mais informativas do que outras.

Pense em duas frases:

  • “Eu moro no Brasil.”
  • “Eu moro no bairro X, na cidade Y, no estado Z.”

As duas podem estar corretas, mas a segunda é bem mais específica. Quando se escreve f(n) = O(n²), está se dando um “endereço” muito mais preciso do que f(n) = O(2ⁿ).

Na prática, quem faz análise de algoritmos costuma buscar uma forma de O( ) que seja:

  • correta;
  • o mais apertada possível, isto é, que descreva bem a ordem de grandeza, sem exagerar.

Mais adiante entram notações como Θ (teta) e (ômega) para falar de limites mais ajustados e limites inferiores, mas a notação O, sozinha, já resolve muita coisa do dia a dia.

Ligando O com melhor caso, pior caso e caso médio

Quando você diz que “a complexidade de pior caso de um algoritmo é O(n²)”, está afirmando:

“No pior cenário, o número de passos cresce no máximo na ordem de n².”

Se a complexidade de melhor caso é O(n), significa:

“Mesmo na situação mais favorável, o número de passos não passa de alguma constante vezes n.”

Já para o caso médio, a ideia é parecida, mas a função f(n) usada dentro do O vem de uma média ponderada (de acordo com probabilidades de cada tipo de entrada). O raciocínio da notação O continua igual.

Às vezes, um mesmo algoritmo tem:

  • melhor caso com uma forma de O( );
  • pior caso com outra forma de O( );
  • caso médio com uma terceira.

Um exemplo clássico é o algoritmo que, dependendo de um parâmetro x, decide se vai somar ou multiplicar matrizes:

  • quando x = 0, faz soma → O(n²);
  • quando x = 1, faz produto → O(n³).

Nesse cenário:

  • O melhor caso é O(n²);
  • O pior caso é O(n³);
  • O caso médio vai ser uma mistura, algo como q·n² + (1 − q)·n³, que assintoticamente fica com cara de O(n³) se a multiplicação tiver qualquer probabilidade real de ocorrer.

A notação O serve para registrar cada uma dessas situações sem sufocar a leitura com fórmulas completas.

Resumindo a ideia com uma metáfora

Imagine que você está olhando prédios de uma cidade de longe, do alto de um morro.

  • Lá de cima, você não enxerga se cada prédio tem 10 ou 12 andares.
  • Você percebe se um bairro é de casas baixas, se outro é de prédios médios, e se uma região específica tem arranha-céus gigantes.

A análise de complexidade com notação O faz algo parecido:

  • não se preocupa com detalhes milimétricos (constante a mais, termo de grau menor);
  • foca em “qual bairro” de crescimento a função está: constante, logarítmico, linear, quadrático, cúbico, exponencial.

Quando alguém diz que um algoritmo é O(n log n), está basicamente te avisando:

“Esse algoritmo está na região dos prédios médios: cresce mais do que linear, mas bem menos do que quadrático.”

Quando diz que algo é O(2ⁿ), o recado é:

“Aqui estão os arranha-céus exagerados. Para entradas um pouco maiores, o custo dispara de forma violenta.”

Comparando

Quando alguém começa a estudar algoritmos, uma pergunta aparece quase automaticamente:

“Ok, eu tenho um algoritmo que funciona… mas será que dá para fazer melhor?”

É aqui que entra a ideia de algoritmo ótimo. Não é só “rápido” no sentido vago. É o melhor que se pode conseguir para aquele problema, dentro de um certo modelo de computação. Não existe outro algoritmo que resolva o mesmo problema, de forma geral, com uma ordem de complexidade assintótica menor.

Primeiro passo: lembrar o que é custo de um algoritmo

Antes de falar em “ótimo”, precisa ficar claro o que significa comparar algoritmos.

Quando se estudam algoritmos, o “tempo” não é medido em segundos, e sim em número de passos em função do tamanho da entrada, que costuma ser representado por uma variável n. Esse tempo é escrito com aquelas notações:

  • O(… ) para limite superior assintótico (ordem de grandeza máxima);
  • Ω(… ) para limite inferior assintótico (ordem de grandeza mínima).

De forma bem resumida:

  • dizer que um algoritmo é O(n²) significa que, para n grande, o número de passos cresce no máximo proporcional a n²;
  • dizer que qualquer algoritmo para um certo problema precisa de pelo menos Ω(n²) passos quer dizer: não importa o truque, ninguém escapa de uma quantidade de trabalho da ordem de n².

Esses dois lados são importantes:

  • O(…): o que um algoritmo específico gasta;
  • Ω(…): o que qualquer algoritmo é obrigado a gastar, só por causa da natureza do problema.

Um algoritmo é chamado de ótimo quando essas duas coisas “se encontram”, isto é, quando a complexidade de pior caso do algoritmo bate no limite inferior do problema, até fator constante.

Limite inferior: o piso do esforço

Pense num problema bem simples: inverter os elementos de um vetor com n posições.

Não importa qual algoritmo se invente, uma coisa é inevitável: ler os n elementos em algum momento. Se o vetor tem n posições e o computador nunca olha para algumas delas, como garantir que a saída está correta?

Então, qualquer algoritmo de inversão precisa, no mínimo, fazer algo proporcional a n leituras. Diz-se que o problema tem limite inferior Ω(n).

O raciocínio vale para outros casos:

  • somar duas matrizes n×n exige ler todos os n² elementos; o limite inferior do problema é Ω(n²);
  • não há como somar sem olhar os dados.

Esse tipo de limite é chamado, às vezes, de limite trivial, porque vem direto do tamanho da entrada: se você precisa ler tudo, já tem ali um custo mínimo.

Em termos de metáfora: é como carregar caixas de um caminhão. Mesmo que o processo seja mega organizado, existe um número mínimo de viagens ou de levantadas, porque cada caixa precisa sair de onde está. Esse número mínimo é o “Ω” do problema.

Limite superior: o teto fornecido por um algoritmo real

Agora vem o outro lado: olhar para um algoritmo concreto e perguntar:

“Qual é a ordem de grandeza do número de passos que ele executa no pior caso?”

Se o algoritmo de inverter o vetor faz um laço que troca o primeiro com o último, o segundo com o penúltimo e assim por diante, ele executa algo em torno de n/2 trocas. Isso é da ordem de n operações. Então a complexidade de pior caso é O(n).

Da mesma forma:

  • somar duas matrizes n×n com dois laços aninhados (um para linha, outro para coluna) faz n² somas; complexidade O(n²).

Essas contagens fornecem o limite superior: garantem que, naquele algoritmo, o esforço nunca passa da ordem de n ou de n², de acordo com o caso.

Agora sim: o que é um algoritmo ótimo?

Juntando as duas coisas:

  • se um problema tem um limite inferior Ω(f(n)), ou seja, qualquer algoritmo precisa de pelo menos uma ordem de f(n) passos;
  • e se existe um algoritmo cujo pior caso é O(f(n)), isto é, que resolve o problema dentro dessa mesma ordem de esforço;

então esse algoritmo é considerado ótimo.

Em outras palavras, o problema exige pelo menos f(n) passos, e o algoritmo resolve com, no máximo, uma constante vezes f(n). Não dá para melhorar a ordem de grandeza. Tudo o que se pode brigar dali em diante são constantes (ser 2 vezes mais rápido, 3 vezes mais rápido etc.), mas a curva de crescimento é a melhor possível.

É como saber que, fisicamente, um certo trajeto só pode ser percorrido em, no mínimo, 10 minutos, dadas as leis da física e os limites de velocidade. Se alguém inventa um método que faz esse trajeto estabilizado sempre em algo como 11 ou 12 minutos, esse método é “ótimo” em termos de ordem de grandeza. Qualquer melhoria posterior vai ser em detalhes finos, não no “tipo” de tempo.

Exemplos bem concretos

1. Inversão de sequência

  • Problema: inverter um vetor com n elementos.
  • Limite inferior: Ω(n), porque é preciso ler a sequência.
  • Algoritmo padrão: laço que faz trocas em pares da ponta para o centro, com algo em torno de n/2 trocas.
  • Complexidade: O(n).

Como o limite inferior é Ω(n) e o algoritmo resolve em O(n), conclui-se que se trata de um algoritmo ótimo. Não há jeito, dentro desse modelo de custo, de inverter com menos do que uma ordem linear de operações.

2. Soma de matrizes

  • Problema: somar duas matrizes n×n.
  • Limite inferior: Ω(n²), pela leitura dos n² elementos.
  • Algoritmo padrão: dois laços aninhados, um para linha e outro para coluna, que fazem uma soma para cada posição.
  • Complexidade: O(n²).

De novo, limite inferior e limite superior batem na mesma ordem, então o algoritmo é ótimo.

Esses dois casos são, de certa forma, “fáceis” de analisar porque o limite inferior vem diretamente do fato de ser preciso ler todos os dados.

3. Produto de matrizes

Aqui as coisas ficam interessantes.

  • Problema: multiplicar duas matrizes n×n.
  • Limite inferior trivial: pelo menos tem que ler as matrizes, então Ω(n²).
  • Algoritmo ingênuo: três laços aninhados (linha, coluna, índice de multiplicação), com cerca de n³ operações. Complexidade O(n³).

Dá para dizer que o algoritmo ingênuo é ótimo?
Com base no limite trivial Ω(n²), não. Existe espaço entre n² e n³, e nada impede, em teoria, a existência de um algoritmo O(n²·log n), por exemplo.

Na prática, já foram descobertos algoritmos que multiplicam matrizes com complexidade assintótica menor do que n³. Isso mostra que o algoritmo com três laços não é ótimo.

Ou seja: quando o limite inferior é muito fraco, não basta bater nesse limite para dizer que o algoritmo é o melhor possível.

Quando o limite inferior não é trivial

Os exemplos anteriores usaram limites inferiores que vêm diretamente do tamanho da entrada. Só que, em vários problemas, é possível provar limites inferiores mais fortes, usando propriedades matemáticas do problema, e não só o “precisa ler os dados”.

Um exemplo clássico é o problema de ordenar uma sequência de n elementos usando apenas comparações.

  • Limite trivial (ler os elementos): Ω(n).
  • Mas há uma prova matemática bem conhecida de que qualquer algoritmo baseado só em comparações precisa de, pelo menos, Ω(n log n) comparações em pior caso.

Por outro lado, existem algoritmos como mergesort e heapsort cuja complexidade de pior caso é O(n log n). Ou seja:

  • limite inferior: Ω(n log n);
  • limite superior (de algoritmos reais): O(n log n).

Resultado: esses algoritmos são considerados ótimos dentro do modelo de comparação.

Esse tipo de encaixe é o sonho de quem trabalha com projeto de algoritmos: ter uma prova de que ninguém consegue fazer melhor (assintoticamente) e, ao mesmo tempo, ter uma técnica que chega justamente nesse limite.

A tal “distância” entre limites inferiores e algoritmos

Nem sempre a situação é tão bonita.

Para uma grande quantidade de problemas importantes, acontece algo assim:

  • o melhor limite inferior conhecido é um polinômio em n (tipo n, n², n³);
  • o melhor algoritmo conhecido tem complexidade exponencial, do tipo 2ⁿ, 3ⁿ ou parecido.

Ou seja, há um abismo entre o que se sabe que é necessário e o que de fato se consegue fazer com as técnicas atuais. Não se sabe se esse abismo existe porque:

  • ainda não foi descoberta uma prova de limite inferior mais forte, ou
  • ainda não foi inventado um algoritmo mais rápido, ou
  • as duas coisas ao mesmo tempo.

Esse cenário aparece em vários problemas difíceis em teoria da computação, especialmente em temas ligados à famosa questão P vs NP: será que certos problemas que parecem exigir tempo exponencial têm, na verdade, algoritmos polinomiais escondidos?

Nesses casos, ninguém se arrisca a dizer que um algoritmo é ótimo. O máximo que se pode afirmar é:

  • “Este é o melhor algoritmo conhecido até agora”
  • “O maior limite inferior conhecido é tal”.

A palavra “ótimo”, nesse contexto teórico, é reservada para quando há encaixe claro entre limite inferior e limite superior.

Como enxergar isso com uma analogia

Imagine que se quer viajar de uma cidade a outra, e um físico te diz:

“Pelas leis da física e pela distância, qualquer viagem vai demorar pelo menos 1 hora.”

Esse 1 hora é o limite inferior.

Agora compare três situações:

  1. Você tem um meio de transporte que sempre leva 1h15.
    A diferença é só um fator de 1,25. Em termos de ordem de grandeza, é praticamente a melhor coisa possível. Isso é análogo a um algoritmo ótimo: bate no limite inferior até constante.

  2. Você tem outro meio de transporte que sempre leva 10 horas.
    A diferença já é de 10 vezes o mínimo. Pode ser que exista um método muito melhor que ainda não foi inventado. Esse é o caso em que a complexidade do melhor algoritmo conhecido é bem acima do limite inferior.

  3. Pior: o limite inferior que o físico conseguiu provar é só “pelo menos 10 minutos”, mas todo transporte real leva de horas a semanas.
    Talvez a prova esteja fraca, talvez os meios de transporte sejam muito ruins, talvez os dois. Esse é o cenário de vários problemas difíceis em computação.

E na prática do dia a dia, isso importa mesmo?

Em código real, a discussão sobre “ótimo” pode parecer distante, mas ela tem impacto direto em:

  • desempenho de sistemas grandes: bancos de dados, motores de busca, redes;
  • custo de infraestrutura: menos tempo de CPU significa menos gasto de energia e de máquinas;
  • experiência do usuário: um algoritmo quadrático pode ser aceitável em poucos dados, mas inviável quando a empresa cresce.

Saber que um algoritmo é ótimo dá uma espécie de paz de espírito: se esse problema é crítico para o sistema, não adianta ficar gastando meses tentando encontrar algo com complexidade assintoticamente menor, porque as próprias provas matemáticas dizem que isso não existe naquele modelo.

Por outro lado, quando se sabe que o algoritmo não é ótimo (como o produto ingênuo de matrizes), isso acende uma luz de oportunidade para pesquisa e melhoria: talvez haja um jeito mais esperto de reorganizar o cálculo e ganhar ordens de grandeza em desempenho.

Um detalhe importante: ótimo não quer dizer “o melhor em todos os sentidos”

Vale um cuidado final: na teoria, “ótimo” significa bater o limite inferior na ordem de grandeza. Na prática, dois algoritmos com mesma complexidade assintótica podem ter comportamentos muito diferentes:

  • um pode ser mais simples de implementar, mais fácil de manter;
  • outro pode ter uma constante escondida enorme, usando dezenas de estruturas auxiliares;
  • um pode funcionar melhor em entradas pequenas ou médias, que são justamente o que aparece no mundo real.

Então, na hora de escolher um algoritmo para um sistema de verdade, entram outros fatores:

  • simplicidade;
  • robustez;
  • comportamento em casos típicos, não só no pior caso;
  • consumo de memória;
  • facilidade de testar e depurar.

A teoria da otimização algorítmica ajuda a estabelecer o limite do que é possível. Dentro desse limite, o trabalho do engenheiro de software é encontrar o ponto de equilíbrio entre matemática, desempenho e pragmatismo.


Aqui terminamos.