Laços: o motor que repete o que a máquina precisa
Laços: o motor que repete o que a máquina precisa
Nos artigos anteriores desta série explorámos dois conceitos fundamentais: a variável, como a unidade mínima de estado, e o condicional, como a unidade mínima de decisão. Hoje enfrentamos o terceiro pilar da programação: o laço, a estrutura que permite que um programa repita uma operação sem que o programador tenha de a escrever uma e outra vez.
Os laços são, provavelmente, o conceito que mais transformou a história da computação. Antes deles, um programa era uma sequência linear de instruções executada do princípio ao fim. Com eles, um programa pode processar milhões de dados, percorrer estruturas complexas, esperar eventos e repetir cálculos até que uma condição se cumpra. Sem laços, os videojogos não existiriam, os navegadores não poderiam renderizar páginas, os servidores não poderiam atender múltiplos pedidos e a inteligência artificial não poderia treinar modelos.
Mas, o que é exatamente um laço? Como funciona no nível mais baixo da máquina? Porque existem tantas variantes e em que se diferenciam? E o mais importante para os tempos que correm: que papel desempenham os laços na era dos agentes de código?
1. O que é um laço?
Um laço é uma estrutura de controlo que permite executar um bloco de código repetidamente, seja um número determinado de vezes, até que uma condição se cumpra, ou enquanto se percorre uma coleção de elementos. Em essência, é a forma como dizemos à máquina: "Repete isto até eu dizer-te para parar".
Na maioria das linguagens de programação, os laços são implementados através de três mecanismos fundamentais:
- Iteração controlada por contador: repete um número fixo de vezes (por exemplo,
for i = 0; i < 10; i++). - Iteração controlada por condição: repete enquanto uma condição for verdadeira (por exemplo,
while (condição)). - Iteração sobre coleções: percorre cada elemento de uma estrutura de dados (por exemplo,
for elemento in lista).
Cada um destes mecanismos tem o seu próprio modelo mental, a sua própria implementação em baixo nível e os seus próprios casos de uso.
2. O laço em baixo nível: o salto que mudou tudo
Para compreender realmente o que é um laço, devemos descer ao nível do assembly e, mais além, ao nível do hardware. No fundo, um laço reduz-se a uma operação de comparação seguida de um salto condicional que aponta para trás no código.
2.1. A estrutura fundamental: comparar, saltar, repetir
No artigo sobre condicionais vimos que o processador utiliza flags de condição (ZF, SF, CF, OF) para decidir se um salto deve ser tomado ou não. Um laço não é mais do que um condicional que, em vez de saltar para a frente para evitar um bloco de código, salta para trás para o repetir.
O diagrama seguinte mostra a estrutura básica de um laço for traduzido para assembly.
flowchart TD
A["Inicialização:i = 0"] --> B["Comparação:CMP i, 10"]
B --> C{"i < 10?"}
C -->|Sim| D["Corpo do laço:executar instruções"]
D --> E["Incremento:i = i + 1"]
E --> B
C -->|Não| F["Sair do laço:continuar com o resto do programa"]
style A fill:#e3f2fd,stroke:#1565c0
style B fill:#fff3e0,stroke:#ef6c00
style D fill:#e8f5e9,stroke:#2e7d32
style F fill:#f3e5f5,stroke:#7b1fa2
Em assembly, este laço ficaria assim em x86-64:
mov ecx, 0 ; i = 0
.loop:
cmp ecx, 10 ; comparar i com 10
jge .end ; se i >= 10, sair do laço
; corpo do laço
; ...
inc ecx ; i = i + 1
jmp .loop ; voltar ao início do laço
.end:
; continuar com o resto do programa
A instrução jmp .loop é a chave. É um salto incondicional que devolve a execução ao início do bloco. A instrução jge .end é um salto condicional que abandona o laço quando a condição deixa de se cumprir.
2.2. O impacto na pipeline do processador
No artigo sobre condicionais explicámos que os processadores modernos utilizam uma pipeline para executar várias instruções simultaneamente, e que os saltos condicionais podem quebrar essa pipeline se o preditor de saltos falhar.
Os laços são particularmente interessantes neste contexto porque apresentam um padrão de salto altamente previsível. Na maioria dos casos, o processador prevê corretamente que o salto para trás será tomado (porque o laço se repete muitas vezes) e só falha na última iteração, quando o laço termina.
Os preditores de saltos modernos incluem componentes especializados em laços, como o Loop Stream Detector da Intel, que deteta padrões de laços pequenos e os executa diretamente a partir de um buffer de instruções, evitando o custo de descodificação em cada iteração.
2.3. Loop unrolling: a otimização que elimina o laço
Uma das otimizações mais comuns realizadas pelos compiladores é o desenrolar de laços (loop unrolling). Consiste em replicar o corpo do laço várias vezes no código gerado, reduzindo o número de saltos e comparações.
Por exemplo, um laço que soma os elementos de um array poderia ser desenrolado da seguinte forma:
; Laço original
mov ecx, 0
.loop:
add eax, [array + ecx*4]
inc ecx
cmp ecx, 1000
jl .loop
; Laço desenrolado (4 iterações por volta)
mov ecx, 0
.loop:
add eax, [array + ecx*4]
add eax, [array + (ecx+1)*4]
add eax, [array + (ecx+2)*4]
add eax, [array + (ecx+3)*4]
add ecx, 4
cmp ecx, 1000
jl .loop
O desenrolar reduz o número de saltos e comparações, mas aumenta o tamanho do código. É uma técnica clássica de otimização que continua válida nos compiladores modernos.
3. Tipos de laços e as suas diferenças
Ao longo da história da programação surgiram múltiplas variantes de laços, cada uma com a sua própria semântica e os seus próprios casos de uso. As mais importantes são for, for each, while e do-while.
3.1. for: o laço com contador explícito
O laço for clássico (também chamado for de três cláusulas) tem a seguinte estrutura em C, Java, JavaScript e linguagens semelhantes:
for (inicialização; condição; incremento) {
// corpo do laço
}
Utiliza-se quando se conhece de antemão o número de iterações ou quando se precisa de um contador explícito que se incremente em cada volta. É o laço mais comum em algoritmos que percorrem arrays ou realizam cálculos repetitivos.
Em Python, o for tem uma sintaxe diferente, mas a sua semântica é semelhante quando se usa com range():
for i in range(10):
print(i)
3.2. for each: o laço sobre coleções
O laço for each (também chamado for-of em JavaScript, foreach em C#, ou simplesmente for em Python) percorre diretamente os elementos de uma coleção, sem necessidade de um contador explícito.
for elemento in lista:
print(elemento)
for (const elemento of lista) {
console.log(elemento);
}
foreach (var elemento in lista)
{
Console.WriteLine(elemento);
}
A diferença fundamental com o for clássico é que o for each não expõe o índice da iteração (pelo menos não de forma direta), e não permite modificar a coleção durante o percurso na maioria das linguagens. Internamente, o for each é implementado através de um iterador, que é um objeto que mantém o estado do percurso e expõe um método next() que devolve o elemento seguinte.
flowchart TD
A["Início do for each"] --> B["Obter iterador da coleção"]
B --> C{"Há mais elementos?"}
C -->|Sim| D["Obter elemento seguinte"]
D --> E["Executar corpo do laço"]
E --> C
C -->|Não| F["Fim do laço"]
style B fill:#e3f2fd,stroke:#1565c0
style C fill:#fff3e0,stroke:#ef6c00
style E fill:#e8f5e9,stroke:#2e7d32
A principal vantagem do for each é a legibilidade: o código expressa a intenção de percorrer uma coleção sem se distrair com os detalhes do contador. A principal desvantagem é que não se pode modificar a coleção durante o percurso (na maioria das linguagens) e não se tem acesso direto ao índice.
3.3. while: o laço controlado por condição
O laço while avalia uma condição no início de cada iteração. Se a condição for verdadeira, executa o corpo do laço e volta a avaliar a condição. Se for falsa, sai do laço.
while (condição) {
// corpo do laço
}
Utiliza-se quando não se sabe de antemão quantas iterações serão necessárias, e o laço deve continuar até que uma condição específica se cumpra (por exemplo, ler um ficheiro até ao fim, esperar uma resposta do utilizador, ou processar uma fila de mensagens).
A diferença-chave com o for clássico é que o while não tem uma cláusula de inicialização nem de incremento explícita. O programador é responsável por gerir o estado que controla a condição.
3.4. do-while: o laço que se executa pelo menos uma vez
O laço do-while avalia a condição no final de cada iteração, o que garante que o corpo do laço se execute pelo menos uma vez, independentemente de a condição ser verdadeira ou falsa desde o início.
do {
// corpo do laço
} while (condição);
Utiliza-se quando se precisa que o corpo do laço se execute pelo menos uma vez, como em menus interativos ou na leitura de dados que devem ser validados antes de continuar.
A diferença fundamental com o while é o momento da avaliação da condição: o while avalia antes de executar o corpo (pode executar-se zero vezes), enquanto o do-while avalia depois (executa-se pelo menos uma vez).
3.5. Tabela comparativa de tipos de laços
| Tipo | Avaliação da condição | Número de iterações | Uso típico |
|---|---|---|---|
for |
Antes do corpo | Conhecido ou controlado por contador | Percorrer arrays com índice, cálculos repetitivos |
for each |
Antes do corpo | Igual ao número de elementos | Percorrer coleções sem modificar |
while |
Antes do corpo | Desconhecido (depende da condição) | Esperar eventos, processar filas, ler ficheiros |
do-while |
Depois do corpo | Pelo menos uma vez | Menus interativos, validação de dados |
4. Laços em diferentes linguagens de programação
Vejamos como se escrevem os laços em várias linguagens e que particularidades tem cada uma.
4.1. C
// for clássico
for (int i = 0; i < 10; i++) {
printf("%d\n", i);
}
// while
int i = 0;
while (i < 10) {
printf("%d\n", i);
i++;
}
// do-while
int j = 0;
do {
printf("%d\n", j);
j++;
} while (j < 10);
C não tem um for each nativo. Para percorrer um array usa-se o for clássico com um índice.
4.2. Java
// for clássico
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
// for each (enhanced for)
int[] numeros = {1, 2, 3, 4, 5};
for (int numero : numeros) {
System.out.println(numero);
}
// while
int i = 0;
while (i < 10) {
System.out.println(i);
i++;
}
// do-while
int j = 0;
do {
System.out.println(j);
j++;
} while (j < 10);
Java introduziu o for each (enhanced for) no Java 5 (2004), simplificando o percurso de arrays e coleções.
4.3. Python
# for sobre range (equivalente a for clássico)
for i in range(10):
print(i)
# for each sobre uma lista
numeros = [1, 2, 3, 4, 5]
for numero in numeros:
print(numero)
# while
i = 0
while i < 10:
print(i)
i += 1
# do-while não existe nativamente em Python
# Simula-se com um while True e um break
i = 0
while True:
print(i)
i += 1
if i >= 10:
break
Python não tem um do-while nativo. O for de Python é, na realidade, um for each que percorre qualquer iterável.
4.4. JavaScript
// for clássico
for (let i = 0; i < 10; i++) {
console.log(i);
}
// for each (for-of)
const numeros = [1, 2, 3, 4, 5];
for (const numero of numeros) {
console.log(numero);
}
// while
let i = 0;
while (i < 10) {
console.log(i);
i++;
}
// do-while
let j = 0;
do {
console.log(j);
j++;
} while (j < 10);
JavaScript também tem for-in, que percorre as chaves de um objeto, mas não é um for each no sentido estrito.
4.5. Rust
// for sobre um intervalo
for i in 0..10 {
println!("{}", i);
}
// for each sobre uma coleção
let numeros = vec![1, 2, 3, 4, 5];
for numero in &numeros {
println!("{}", numero);
}
// while
let mut i = 0;
while i < 10 {
println!("{}", i);
i += 1;
}
// loop (laço infinito com break)
let mut j = 0;
loop {
println!("{}", j);
j += 1;
if j >= 10 {
break;
}
}
Rust não tem do-while; em seu lugar, usa-se loop com uma condição de saída explícita.
5. Diagrama completo: do código fonte ao hardware
O diagrama seguinte resume a viagem completa de um laço, desde o código fonte até à execução no processador.
flowchart TD
A["Código fonte:for (i = 0; i < 10; i++)"] --> B["Compilador / Intérprete"]
B --> C{"Linguagem compiladaou interpretada?"}
C -->|Compilada C, Rust| D["Código assembly:mov ecx, 0.loop:cmp ecx, 10jge .end...inc ecxjmp .loop"]
C -->|Bytecode Java, Python| E["Bytecode:LOAD_CONST 0STORE_FAST i...JUMP_ABSOLUTE"]
C -->|JIT JavaScript| F["Compilação JITpara código máquina"]
D --> G["Assemblypara código máquina"]
E --> H["Máquina Virtualinterpreta bytecode"]
F --> G
G --> I["Processador:Fetch, Decode, Execute"]
H --> I
I --> J["Branch Predictor:prevê o salto do laço"]
J --> K{"Previsão correta?"}
K -->|Sim| L["Execução continuasem penalização"]
K -->|Não| M["Descartar instruçõesespeculativasPenalização: 10-20 ciclos"]
M --> N["Carregar caminho correto"]
L --> O["Resultado final"]
N --> O
style A fill:#e3f2fd,stroke:#1565c0
style J fill:#fff3e0,stroke:#ef6c00
style M fill:#fce4ec,stroke:#c62828
6. A combinação que dá origem aos programas
Os três conceitos que explorámos nesta série —variáveis, condicionais e laços— não são entidades isoladas. São os tijolos com que se constroem todos os programas que usamos diariamente.
Um programa é, em essência, uma sequência de operações que combinam estes três elementos:
- As variáveis armazenam o estado do programa.
- Os condicionais permitem tomar decisões com base nesse estado.
- Os laços permitem repetir operações até que o estado atinja um valor desejado.
Vejamos um exemplo concreto. Um programa que calcula a soma dos números pares de 1 a 100:
soma = 0 # variável
for i in range(1, 101): # laço
if i % 2 == 0: # condicional
soma += i # variável
print(soma)
Este programa combina os três conceitos: uma variável (soma), um laço (for) e um condicional (if). O resultado é 2550, mas o importante não é o resultado, mas a estrutura: o laço itera sobre um intervalo, o condicional filtra os números pares, e a variável acumula a soma.
Este mesmo padrão repete-se em todos os programas, do mais simples ao mais complexo. Um navegador web percorre os nós do DOM (laço), verifica se cada nó cumpre certas condições (condicional) e atualiza o estado da página (variável). Um videojogo itera sobre os objetos da cena (laço), verifica colisões (condicional) e atualiza as posições (variável). Um servidor web itera sobre os pedidos dos clientes (laço), verifica se o pedido é válido (condicional) e gera uma resposta (variável).
Na era dos agentes de código, estes conceitos continuam a ser fundamentais. Um agente de IA que gera código deve compreender a estrutura dos laços para produzir código correto e eficiente. E os programadores que trabalham com agentes devem saber ler e escrever laços para rever e modificar o código gerado.
7. Conclusão
O laço é muito mais do que uma palavra-chave numa linguagem de programação. É a expressão de uma das operações mais fundamentais da computação: a capacidade de repetir uma operação. Desde os saltos condicionais no assembly até aos iteradores de alto nível, cada laço é uma pequena máquina de repetição que permite que os programas processem grandes quantidades de dados sem que o programador tenha de escrever cada passo manualmente.
Os diferentes tipos de laços —for, for each, while, do-while— não são variantes arbitrárias. Cada um tem uma semântica específica que o torna adequado para um tipo de problema particular. Compreender essa semântica é o que distingue um programador que escreve código correto de um que escreve código elegante.
E quando combinamos os laços com as variáveis e os condicionais, obtemos a tríade fundamental da programação. Com esses três elementos constroem-se todos os programas, do mais simples ao mais complexo. E nessa simplicidade reside a sua grandeza.
8. Referências
- Hennessy, J. L., Patterson, D. A., & Kozyrakis, C. (2025). Computer architecture: A quantitative approach (7th ed.). Morgan Kaufmann.
- Mittal, S. (2018). A survey of techniques for dynamic branch prediction. Concurrency and Computation: Practice and Experience, 30(1), e4707. https://doi.org/10.1002/cpe.4707
- Intel Corporation. (2024). Intel 64 and IA-32 architectures optimization reference manual. Intel.
- ECMA International. (2025). ECMAScript 2025 language specification (15th ed.). https://www.ecma-international.org
- Python Software Foundation. (2026). The Python language reference (Version 3.13). https://docs.python.org/3/reference/
- Oracle. (2026). The Java Virtual Machine specification. https://docs.oracle.com/javase/specs/
- Rust Team. (2026). The Rust programming language. https://doc.rust-lang.org/book/
- Stallman, R. M., & GCC Developer Community. (2025). GNU Compiler Collection (GCC) internals: Loop optimization. Free Software Foundation. https://gcc.gnu.org/onlinedocs/gccint/
Carregando reacoes...
Comentarios (0)
Carregando sessao...
Ainda nao ha comentarios. Seja o primeiro a comentar.