Análise de algoritmos e modelos de custo de sistemas¶
O que é um algoritmo¶
Algoritmo é um procedimento finito que transforma estado de entrada em estado de saída preservando invariantes definidos.
Para documentação de sistemas, descrever um algoritmo exige no mínimo:
- representação da entrada;
- pré-condições;
- sequência de operações;
- invariantes;
- condição de término;
- pós-condição;
- comportamento de falha;
- consumo de recursos.
Nome de função não é descrição algorítmica. “Alocar uma página” não informa como free state é representado, qual busca é feita, qual sincronização protege o estado nem o que ocorre sem páginas livres.
Tamanho da entrada¶
Toda complexidade precisa declarar sua variável de tamanho.
Exemplos:
- n = elementos de um array;
- p = páginas físicas;
- h = profundidade de page table;
- v = vértices;
- t = triângulos;
- b = blocos de um arquivo;
- s = comprimento do source;
- q = jobs na fila.
Dizer apenas que algo é “linear” é incompleto.
Big-O¶
Big-O expressa classe superior de crescimento. f(n) é O(g(n)) quando não cresce mais rápido que um múltiplo constante de g para n suficientemente grande.
| Classe | Exemplo |
|---|---|
| O(1) | acesso indexado |
| O(log n) | busca em árvore balanceada |
| O(n) | varredura linear |
| O(n log n) | sort por comparação |
| O(n²) | comparação de todos os pares |
| O(2^n) | busca exaustiva de subconjuntos |
Big-O ignora fatores constantes e termos inferiores. Isso é útil para escalabilidade, porém insuficiente para desempenho de baixo nível.
Omega e Theta¶
Big-O fornece bound superior. Omega descreve bound inferior. Theta indica crescimento assintoticamente justo.
Se um algoritmo sempre percorre todos os elementos, seu custo é Theta(n), não apenas O(n).
Precisão importa ao comparar designs. Uma rotina O(n) que normalmente encontra resultado no primeiro item apresenta perfil diferente de uma rotina que obrigatoriamente examina todos.
Pior caso, média e valor esperado¶
Software de sistemas frequentemente privilegia pior caso.
Hash table pode oferecer O(1) esperado e O(n) no pior caso. Isso pode ser aceitável em ferramenta de desktop e perigoso em caminho de hard real-time.
A documentação deve separar:
- pior caso;
- melhor caso;
- média sob distribuição declarada;
- valor esperado de estrutura probabilística;
- custo amortizado de uma sequência.
Análise amortizada¶
Append em array dinâmico ocasionalmente custa O(n) quando a capacidade cresce; se a capacidade dobra, uma longa sequência possui custo amortizado O(1) por append.
Amortizado não significa probabilístico. O custo caro é distribuído contabilmente pela sequência.
No kernel ainda é necessário perguntar onde a operação O(n) individual pode ocorrer. Resize raro dentro de seção crítica com interrupções desabilitadas pode ser inaceitável.
Complexidade espacial¶
Memória é recurso de primeira classe.
Algoritmos reduzem tempo armazenando metadados, tabelas precomputadas ou caches. PMM, filesystem, JIT e gráficos trocam memória por velocidade.
Análise espacial deve contar:
- tamanho persistente;
- scratch temporário;
- profundidade de stack/recursão;
- fragmentação;
- padding/alinhamento;
- duplicação e cache.
Em alguns caminhos do kernel, alocação pode ser proibida.
Localidade¶
Dois algoritmos O(n) podem ter desempenho muito diferente.
Varredura sequencial de array contíguo aproveita localidade espacial e prefetch. Seguir ponteiros por nós espalhados pode causar misses de cache e TLB.
Modelo de custo de sistemas deve considerar:
operações algorítmicas
+ misses da hierarquia de memória
+ sincronização
+ latência de dispositivo
A classe assintótica continua útil, mas o processador executa acessos reais à memória.
Previsibilidade de branch¶
Controle de fluxo também possui custo. Branch previsível pode ser barato; branches dependentes de dados e imprevisíveis podem descartar trabalho especulativo.
Transformações branchless ajudam alguns loops, mas podem executar mais instruções.
Otimização deve ser medida no workload real.
Custo de alocação¶
Uma estrutura que cria um nó por operação depende do alocador.
Perguntas relevantes:
- alocação é O(1), scan ou árvore?
- pode bloquear?
- usa lock global?
- pode falhar?
- fragmenta?
- exige memória física contígua?
Estrutura de dados de livro-texto não pode ser avaliada isoladamente do allocator em kernel.
Complexidade de locks¶
Uma operação O(1) pode esperar atrás de spinlock contendido por tempo muito maior que seu trabalho local.
Análise concorrente acrescenta:
- ordem de aquisição;
- domínio de contenção;
- tamanho da seção crítica;
- propriedade de progresso;
- estado de interrupção/preempção;
- cache-line bouncing.
Big-O mede trabalho; sincronização mede coordenação.
Propriedades de progresso¶
Algoritmos concorrentes podem ser:
- blocking: proprietário parado pode atrasar outros;
- lock-free: há progresso global;
- wait-free: cada operação termina em número limitado de seus próprios passos;
- obstruction-free: há progresso sem contenção.
Uma fila protegida por spinlock é blocking nessa classificação formal mesmo que a seção crítica seja curta.
Categorias mais fortes não são automaticamente melhores; aumentam complexidade e custo de reclaim.
Estruturas limitadas versus dinâmicas¶
Sistemas operacionais frequentemente usam tabelas e rings de capacidade fixa.
Uma estrutura limitada oferece:
- memória previsível;
- independência de allocator;
- falha simples quando cheia;
- endereços estáveis.
O custo é limite rígido e, às vezes, scans lineares.
Árvores ou hash tables dinâmicas escalam, mas introduzem alocação e teardown mais complexos.
A estrutura correta depende da escala e dos invariantes reais.
Tempo real e bounds de latência¶
Em real-time, throughput médio é insuficiente. Importa a latência máxima.
Uma árvore O(log n) pode ser inadequada se aloca ou pega lock imprevisível. Um scan O(n) com n pequeno e rigidamente limitado pode oferecer bound melhor.
Notação assintótica supõe crescimento; sistemas frequentemente limitam n propositalmente.
Latência de dispositivo¶
Drivers interagem com hardware cujo tempo não acompanha contagem de instruções da CPU.
I/O de storage pode gastar microssegundos ou milissegundos aguardando dispositivo. Complexidade de CPU ainda importa para gerenciar filas, mas latência fim a fim inclui hardware.
Polling consome CPU durante a espera. Interrupções adicionam overhead de entrada/saída e sincronização, mas liberam CPU.
Throughput versus latência¶
Latência mede uma operação. Throughput mede operações por unidade de tempo.
Batching pode aumentar throughput e piorar latência individual.
Command buffers gráficos, filas de block I/O e processamento de rede usam batching. Caminhos interativos podem preferir lotes menores.
A documentação deve indicar qual objetivo está sendo otimizado.
Tail latency¶
Percentis 99 ou 99,9 podem ser mais relevantes que média.
Pausas longas surgem de:
- contenção;
- slow path de allocator;
- cache miss;
- page fault;
- retry de dispositivo;
- filas acumuladas;
- garbage collection;
- seções críticas grandes.
Média boa não garante sensação de estabilidade.
Page-table walk¶
Um walk x86-64 de quatro níveis possui profundidade arquitetural fixa, portanto é O(1) em relação ao tamanho do address space.
Porém cada nível pode exigir acesso dependente à memória. TLBs existem porque quatro acessos adicionais por load/store seriam caros.
O(1) não significa barato.
Alocação com bitmap¶
Bitmap registra um bit por recurso. Testar posição conhecida é O(1). Encontrar posição livre por scan é O(n) no número de unidades.
Cursor ou hint melhora comportamento típico evitando recomeçar em zero, mas não remove pior caso.
A densidade de bits torna a representação compacta e amigável ao cache.
Fila circular¶
Ring de capacidade fixa com head, tail e count oferece enqueue/dequeue O(1).
O preço é capacidade limitada. Full/empty precisam de representação inequívoca e produtores/consumidores concorrentes exigem sincronização.
É comum em kernel e dispositivos porque índices avançam sem mover elementos.
Compiladores¶
Lexer normalmente percorre source aproximadamente em O(s). Complexidade do parser depende da gramática e estratégia. Lookup de símbolos varia conforme array, hash ou árvore.
Passes de otimização podem percorrer IR repetidamente, multiplicando custo por tamanho e quantidade de passes.
A documentação precisa registrar o algoritmo atual, não apenas a teoria geral de compiladores.
Gráficos¶
Rasterização depende de geometria e pixels cobertos.
Um algoritmo que testa cada pixel da tela para cada triângulo aproxima O(T × W × H). Bounding boxes reduzem área. Tile binning associa primeiro triângulos a tiles, permitindo trabalho local e paralelo.
Z-buffer adiciona teste O(1) por fragmento candidato, mas também tráfego de memória.
Desempenho real depende de SIMD, cache e overdraw.
Emulação¶
Interpretador simples realiza fetch/decode/execute por instrução guest. Tempo cresce aproximadamente com contagem de instruções multiplicada pelo custo do decoder/executor.
JIT paga compilação inicial para reduzir execução repetida. A vantagem depende de reuso.
É um trade-off clássico entre custo inicial, espaço e steady state.
Medição¶
Complexidade prevê escala; benchmark observa uma implementação específica.
Processo adequado:
- derivar expectativa algorítmica;
- identificar fatores constantes;
- medir workloads representativos;
- perfilar hotspots;
- alterar representação ou algoritmo quando evidência justificar;
- revalidar correção.
Benchmark sem modelo pode otimizar ruído. Modelo sem medição pode otimizar o custo errado.
Regra documental¶
Capítulos concretos devem registrar tabela semelhante:
| Operação | Estrutura | Tempo | Espaço/efeito | Sincronização |
|---|---|---|---|---|
| lookup exemplo | array | O(n) worst | sem alocação | lock do subsistema |
| enqueue exemplo | ring | O(1) | capacidade limitada | lock da fila |
A tabela não substitui a explicação. Ela torna contratos ocultos de desempenho e concorrência visíveis.
O capítulo seguinte apresenta as estruturas de dados fundamentais sobre as quais esses algoritmos são construídos.