C++: Otimização de Desempenho
Última atualização: 2026-08-26
Na aula 48, aprendemos sobre os novos recursos do C++17/20.
Agora, vamos aprender sobre otimização de desempenho — fazendo programas C++ rodarem mais rápido.
A vantagem do C++ é o desempenho, mas usá-lo bem não é fácil.
1. Visão Geral da Otimização de Desempenho
(1) 1.1 Por Que Otimizar?
Princípios:
- Não otimize prematuramente (Premature Optimization)
- Meça primeiro, depois otimize
- Otimize o algoritmo primeiro, depois o código
(2) 1.2 Gargalos de Desempenho
| Gargalo | Proporção |
|---|---|
| Algoritmo | 70% |
| Acesso à memória | 20% |
| Outros | 10% |
Conclusão: Otimizar o algoritmo é mais importante do que otimizar o código.
2. Alinhamento de Memória
(1) 2.1 O Que É Alinhamento de Memória?
Alinhamento de memória significa que os dados são armazenados em endereços de memória que são múltiplos de um determinado valor (geralmente uma potência de 2).
Por que é importante?
- Acesso a memória não alinhada é mais lento (ou pode até travar)
- A CPU lê memória alinhada mais rápido
(2) 2.2 Exemplo: Impacto do alinhamento de memória (Dificuldade ⭐⭐)
▶ Exemplo 1: Demo de programação orientada a objetos (Dificuldade ⭐)
#include <iostream>
struct BadAlignment {
char c; // 1 byte
int i; // 4 bytes (pode ser alinhado ao offset 4)
};
struct GoodAlignment {
int i; // 4 bytes
char c; // 1 byte
};
int main() {
std::cout << "Bad: " << sizeof(BadAlignment) << " bytes" << std::endl;
std::cout << "Good: " << sizeof(GoodAlignment) << " bytes" << std::endl;
return 0;
}
Saída:
Bad: 8 bytes
Good: 8 bytes
Possível resultado de execução:
Bad: 8 bytes
Good: 8 bytes
💡 Dica:
- Colocar membros maiores primeiro pode reduzir o padding
3. Amigabilidade ao Cache
(1) 3.1 Cache da CPU
O cache da CPU é 100 vezes mais rápido que a memória principal.
Princípios de otimização:
- Princípio da localidade: Acessar memória adjacente
- Acesso sequencial: Mais rápido que acesso aleatório
- Evitar cache misses: Minimizar acesso a memória não contígua
(2) 3.2 Exemplo: Ordem por linhas vs por colunas (Dificuldade ⭐⭐⭐)
#include <iostream>
### ▶ Exemplo 2: Uso de contêineres STL (Dificuldade ⭐)
#include <vector>
#include <chrono>
int main() {
const int N = 1000;
std::vector<std::vector<int>> matrix(N, std::vector<int>(N));
// Ordem por linhas (amigável ao cache)
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
matrix[i][j] = 1;
}
}
auto end = std::chrono::high_resolution_clock::now();
auto row_time = std::chrono::duration<double>(end - start).count();
// Ordem por colunas (não amigável ao cache)
start = std::chrono::high_resolution_clock::now();
for (int j = 0; j < N; j++) {
for (int i = 0; i < N; i++) {
matrix[i][j] = 1;
}
}
end = std::chrono::high_resolution_clock::now();
auto col_time = std::chrono::duration<double>(end - start).count();
std::cout << "Row-major time: " << row_time << " seconds" << std::endl;
std::cout << "Column-major time: " << col_time << " seconds" << std::endl;
return 0;
}
Saída:
Row-major time: 0.001 seconds
Column-major time: 0.003 seconds
4. Otimização do Compilador
(1) 4.1 Níveis de Otimização
| Nível | Flag | Descrição |
|---|---|---|
| O0 | Nenhum | Sem otimização (para depuração) |
| O1 | -O1 |
Otimização básica |
| O2 | -O2 |
Recomendado (equilibrado) |
| O3 | -O3 |
Otimização agressiva |
| Os | -Os |
Otimizar por tamanho |
(2) 4.2 Exemplo: Efeito da otimização do compilador (Dificuldade ⭐)
#include <iostream>
int main() {
int sum = 0;
for (int i = 0; i < 1000000; i++) {
sum += i;
}
std::cout << sum << std::endl;
return 0;
}
Saída:
499999500000
Compilação:
g++ -O0 main.cpp # Lento
g++ -O2 main.cpp # Rápido (compilador pode calcular o resultado diretamente)
5. Ferramentas de Profiling
(1) 5.1 Ferramentas Comuns
| Ferramenta | Plataforma | Descrição |
|---|---|---|
| gprof | Linux | Profiler do GCC |
| perf | Linux | Ferramenta de análise de desempenho do Linux |
| Valgrind | Multiplataforma | Análise de memória |
| Visual Studio Profiler | Windows | Integrado no VS |
(2) 5.2 Exemplo: Medindo tempo com chrono (Dificuldade ⭐)
#include <iostream>
#include <chrono>
int main() {
auto start = std::chrono::high_resolution_clock::now();
// Código a medir
long sum = 0;
for (int i = 0; i < 100000000; i++) {
sum += i;
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration<double>(end - start).count();
std::cout << "Time elapsed: " << duration << " seconds" << std::endl;
return 0;
}
Saída:
Time elapsed: 0.05 seconds
6. Resumo de Técnicas de Otimização
(1) 6.1 Nível de Código
| Técnica | Descrição |
|---|---|
| Usar semântica de movimentação | Reduzir cópias |
| Usar emplace_back | Evitar objetos temporários |
| Usar reserve | Reduzir realocação do vector |
| Usar unordered_map | Tabela hash, busca O(1) |
(2) 6.2 Nível de Algoritmo
| Técnica | Descrição |
|---|---|
| Escolher a estrutura de dados certa | vector vs list vs map |
| Usar algoritmos apropriados | sort vs partial_sort |
| Evitar cópias desnecessárias | Usar referências, move |
▶ Exemplo 3: Comparação de desempenho passagem por valor vs referência (Dificuldade ⭐)
#include <iostream>
#include <vector>
#include <chrono>
struct BigData {
std::vector<int> data;
BigData() : data(10000, 0) {}
};
// Passagem por valor (cópia)
void processByValue(BigData d) {
(void)d;
}
// Passagem por referência (sem cópia)
void processByRef(const BigData& d) {
(void)d;
}
int main() {
BigData big;
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 10000; i++) {
processByValue(big);
}
auto end = std::chrono::high_resolution_clock::now();
std::cout << "Pass by value time: " << std::chrono::duration<double>(end - start).count() << " seconds" << std::endl;
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 10000; i++) {
processByRef(big);
}
end = std::chrono::high_resolution_clock::now();
std::cout << "Pass by reference time: " << std::chrono::duration<double>(end - start).count() << " seconds" << std::endl;
return 0;
}
Saída:
Pass by value time: 0.15 seconds
Pass by reference time: 0.001 seconds
❓ Perguntas Frequentes
P: A otimização é sempre eficaz? R: Não necessariamente. Meça primeiro, encontre o gargalo, depois otimize.
P: C++ é sempre mais rápido que Python? R: Não necessariamente. Se o algoritmo for o mesmo, C++ geralmente é mais rápido. Mas com um algoritmo ruim, C++ também pode ser lento.
P: Como determino onde está o gargalo? R: Use um profiler.
📖 Resumo
| Ponto-Chave | Resumo |
|---|---|
| Alinhamento de memória | Reduzir padding |
| Amigabilidade ao cache | Acesso sequencial, localidade |
| Otimização do compilador | -O2 recomendado |
| Profiling | Meça primeiro, depois otimize |
| Técnicas de otimização | Semântica de movimentação, emplace_back |
📝 Exercícios
-
Básico (Dificuldade ⭐): Armazene 100.000 inteiros usando
vectorelist, e compare a diferença de tempo para inserção no final e acesso aleatório. -
Intermediário (Dificuldade ⭐⭐): Teste a diferença de desempenho entre passagem por valor e passagem por referência. Escreva uma função que processa um struct grande (contendo
vector<int>(10000)), medindo o tempo para passagem por valor e por referência const. -
Desafio (Dificuldade ⭐⭐⭐): Use cronometragem de alta precisão do
std::chronopara comparar as diferenças de desempenho entre um loopfor, ofor_eachdo STL e oforbaseado em range ao percorrer. Analise os resultados.
- Flags de otimização do compilador: -O0/-O1/-O2/-O3/-Os
- Reduzir cópias: passagem por referência, semântica de movimentação
- Seleção de contêineres: vector tem memória contígua, amigável ao cache
- Funções inline reduzem overhead de chamada de função
- Otimização orientada por perfil: meça gargalos primeiro, depois otimize
Próxima aula: Testes Unitários (#50)