Flags de compilação (-O1, -O2, -O3, -Ofast).¶
As flags de compilação são opções fornecidas ao compilador para controlar o nível de otimização aplicada ao código durante o processo de compilação. Diferentes níveis de otimização podem influenciar o desempenho e o tamanho do código resultante. Vamos explorar as principais flags de otimização usadas com o compilador GCC (GNU Compiler Collection): -O1
, -O2
, -O3
, e -Ofast
.
1. Flag O1
¶
Descrição:
- Nível Básico de Otimização: Aplica otimizações que melhoram o desempenho do código sem aumentar significativamente o tempo de compilação.
Otimizações Comuns:
- Remoção de código morto.
- Simplificação de expressões.
- Inlining básico de funções.
Quando Usar:
- Quando o tempo de compilação é uma preocupação, mas algum nível de otimização é desejado.
g++ -O1 -o meu_programa meu_programa.cpp
2. Flag O2
¶
Descrição:
- Nível Moderado de Otimização: Aplica um conjunto mais agressivo de otimizações que melhoram ainda mais o desempenho do código.
- Maior tempo de compilação comparado ao
O1
, mas melhor desempenho do código.
Otimizações Comuns:
- Inclui todas as otimizações do
O1
. - Otimizações de loop (desenrolamento, fusão de loops).
- Melhorias na alocação de registradores.
- Otimizações de fluxo de controle.
Quando Usar:
- Para a maioria dos casos onde o desempenho é mais crítico do que o tempo de compilação.
- Quando se quer um bom desempenho na performance do código.
g++ -O2 -o meu_programa meu_programa.cpp
3. Flag O3
¶
Descrição:
- Nível Alto de Otimização: Aplica otimizações muito agressivas que podem aumentar significativamente o tempo de compilação e o uso de memória.
- Foco em maximizar o desempenho do código, mesmo que isso aumente o tempo de compilação.
Otimizações Comuns:
- Inclui todas as otimizações do
O2
. - Inlining mais agressivo de funções.
- Vetorização (uso de SIMD).
- Transformações mais avançadas de loop.
Quando Usar:
- Quando o desempenho máximo do código é crucial e o tempo de compilação é menos importante.
- Em aplicações onde cada gota de desempenho é necessária.
g++ -O3 -o meu_programa meu_programa.cpp
4. Flag Ofast
¶
Descrição:
- Nível Máximo de Otimização: Aplica todas as otimizações do
O3
e desconsidera a conformidade estrita com os padrões, o que pode levar a um desempenho ainda maior.
Otimizações Comuns:
- Inclui todas as otimizações do
O3
. - Otimizações de matemática rápida (por exemplo, assume que não há overflow de ponto flutuante).
- Desconsidera o padrão IEEE para operações de ponto flutuante.
Quando Usar:
- Quando o desempenho é a única prioridade e a conformidade estrita com os padrões não é uma preocupação.
- Em cenários de HPC onde a precisão pode ser ligeiramente sacrificada por ganhos de desempenho.
g++ -Ofast -o meu_programa meu_programa.cpp
Comparação dos Níveis de Otimização¶
Flag | Tempo de Compilação | Desempenho | Segurança e Conformidade |
---|---|---|---|
-O1 | Baixo | Moderado | Alta |
-O2 | Moderado | Alto | Alta |
-O3 | Alto | Muito Alto | Alta |
-Ofast | Muito Alto | Máximo | Média/Baixa |
As flags de otimização são ferramentas poderosas que podem ajudar a melhorar significativamente o desempenho do seu código C++. Entender como e quando usá-las é essencial para aproveitar ao máximo os recursos de seu ambiente de compilação e execução.
Exemplos de Compilação com Diferentes Flags de Otimização¶
Para demonstrar os efeitos das diferentes flags de otimização (-O1
, -O2
, -O3
, -Ofast
) no desempenho de códigos C++, vamos utilizar três exemplos representativos de HPC.
Exemplo 1: Multiplicação de Matrizes¶
A multiplicação de matrizes é uma operação computacionalmente intensiva com muitas aplicações em HPC.
Código Base¶
#include <iostream>
#include <vector>
#include <chrono>
void multiplyMatrices(const std::vector<std::vector<double>>& A, const std::vector<std::vector<double>>& B, std::vector<std::vector<double>>& C, int N) {
for (int i = 0; i < N; ++i) {
for (int j = 0; j < N; ++j) {
C[i][j] = 0;
for (int k = 0; k < N; ++k) {
C[i][j] += A[i][k] * B[k][j];
}
}
}
}
int main() {
const int N = 1000;
std::vector<std::vector<double>> A(N, std::vector<double>(N, 1.0));
std::vector<std::vector<double>> B(N, std::vector<double>(N, 1.0));
std::vector<std::vector<double>> C(N, std::vector<double>(N, 0.0));
auto start = std::chrono::high_resolution_clock::now();
multiplyMatrices(A, B, C, N);
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> duration = end - start;
std::cout << "Duration: " << duration.count() << " seconds" << std::endl;
return 0;
}
Compilação e Execução¶
- Compilação com
O1
:
g++ -O1 -o matrix_multiplication_O1 matrix_multiplication.cpp
- Compilação com
O2
:
g++ -O2 -o matrix_multiplication_O2 matrix_multiplication.cpp
- Compilação com
O3
:
g++ -O3 -o matrix_multiplication_O3 matrix_multiplication.cpp
- Compilação com
Ofast
:
g++ -Ofast -o matrix_multiplication_Ofast matrix_multiplication.cpp
Comparação de Desempenho¶
Execute cada versão do programa compilado e compare a duração relatada:
time ./matrix_multiplication_O1
time ./matrix_multiplication_O2
time ./matrix_multiplication_O3
time ./matrix_multiplication_Ofast
Explicando o Output do time
¶
Quando você usa o comando time
para medir o tempo de execução de um programa, ele fornece três valores principais no output: real, user, e sys. Esses valores representam diferentes aspectos do tempo de execução do programa.
Exemplo de Output do time
¶
real 0m10.123s
user 0m8.456s
sys 0m1.234s
O Que Cada Valor Representa¶
1. real¶
- Tempo Real: Representa o tempo total que passou desde o início até o fim da execução do comando. Esse valor inclui todo o tempo de espera do programa, como I/O (input/output), troca de contexto, e tempo de espera por recursos.
Se você iniciar o programa e cronometra-lo com um cronômetro, o valor real é o que você veria no cronômetro.
Fatores que Afetam:
- Tempo gasto aguardando acesso ao disco.
- Tempo de espera na fila da CPU.
- Troca de contexto e outros tempos de espera.
2. user¶
Descrição:
- Tempo de Usuário: Representa a quantidade de tempo que a CPU gastou executando o código do programa em modo usuário. Esse tempo não inclui o tempo gasto em chamadas de sistema (system calls) ou o tempo gasto aguardando operações de I/O.
Medida de quanto tempo de CPU foi usado para executar as instruções do seu programa.
Fatores que Afetam:
- Processamento computacional pesado.
- Cálculos matemáticos e loops intensivos.
3. sys¶
Descrição:
- Tempo de Sistema: Representa a quantidade de tempo que a CPU gastou executando o código do kernel em nome do seu programa. Isso inclui o tempo gasto em chamadas de sistema, como operações de I/O, gerenciamento de memória, e outras operações de kernel.
Tempo de CPU gasto para executar funções de sistema solicitadas pelo seu programa.
Fatores que Afetam:
- Operações de leitura/escrita de disco.
- Operações de rede.
- Alocação e gerenciamento de memória.
Interpretação do Output¶
Vamos considerar novamente o exemplo de output:
real 0m10.123s
user 0m8.456s
sys 0m1.234s
Interpretação:
- real (0m10.123s): O programa levou 10.123 segundos para ser executado do início ao fim. Isso inclui todo o tempo de espera.
- user (0m8.456s): A CPU gastou 8.456 segundos executando o código do seu programa.
- sys (0m1.234s): A CPU gastou 1.234 segundos executando funções do sistema em nome do seu programa.
Comparação de Desempenho com Diferentes Flags de Compilação¶
Ao usar time
para comparar programas compilados com diferentes flags de otimização (-O1
, -O2
, -O3
, -Ofast
), você deve prestar atenção principalmente ao valor real para ver o impacto geral no tempo de execução. No entanto, os valores user e sys também são importantes para entender como as otimizações afetam o uso da CPU e o tempo gasto em operações do sistema.
¶
# Compilação com -O3
g++ -O3 -o matrix_multiplication_O3 matrix_multiplication.cpp
# Medição de tempo de execução
time ./matrix_multiplication_O3
Output esperado:
real 0m7.123s
user 0m6.789s
sys 0m0.234s
Interpretação:
- real (0m7.123s): O tempo total de execução foi de 7.123 segundos.
- user (0m6.789s): A CPU gastou 6.789 segundos executando o código do programa.
- sys (0m0.234s): A CPU gastou 0.234 segundos em chamadas de sistema.
Os valores fornecidos pelo comando time
ajudam a entender o comportamento do seu programa e o impacto das otimizações no desempenho geral. Analisar esses valores pode revelar gargalos e oportunidades de otimização adicional.
Exemplo 2: Regressão Linear (IA)¶
A regressão linear é um algoritmo básico de aprendizado de máquina comumente usado em IA.
Código Base¶
#include <iostream>
#include <vector>
#include <chrono>
double linearRegression(const std::vector<double>& X, const std::vector<double>& Y) {
double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
int n = X.size();
for (int i = 0; i < n; ++i) {
sumX += X[i];
sumY += Y[i];
sumXY += X[i] * Y[i];
sumX2 += X[i] * X[i];
}
return (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
}
int main() {
const int N = 1000000;
std::vector<double> X(N, 1.0);
std::vector<double> Y(N, 2.0);
auto start = std::chrono::high_resolution_clock::now();
double slope = linearRegression(X, Y);
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> duration = end - start;
std::cout << "Slope: " << slope << ", Duration: " << duration.count() << " seconds" << std::endl;
return 0;
}
Compilação e Execução¶
- Compilação com
O1
:
g++ -O1 -o linear_regression_O1 linear_regression.cpp
- Compilação com
O2
:
g++ -O2 -o linear_regression_O2 linear_regression.cpp
- Compilação com
O3
:
g++ -O3 -o linear_regression_O3 linear_regression.cpp
- Compilação com
Ofast
:
g++ -Ofast -o linear_regression_Ofast linear_regression.cpp
Comparação de Desempenho¶
Execute cada versão do programa compilado e compare a duração relatada:
time ./linear_regression_O1
time ./linear_regression_O2
time ./linear_regression_O3
time ./linear_regression_Ofast
Exemplo 3: Processamento de Grandes Conjuntos de Dados (Data Science)¶
Um exemplo comum em Data Science é a normalização de um grande conjunto de dados.
Código Base¶
#include <iostream>
#include <vector>
#include <chrono>
#include <cmath>
void normalize(std::vector<double>& data) {
double mean = 0.0;
double stddev = 0.0;
int n = data.size();
for (int i = 0; i < n; ++i) {
mean += data[i];
}
mean /= n;
for (int i = 0; i < n; ++i) {
stddev += (data[i] - mean) * (data[i] - mean);
}
stddev = std::sqrt(stddev / n);
for (int i = 0; i < n; ++i) {
data[i] = (data[i] - mean) / stddev;
}
}
int main() {
const int N = 10000000;
std::vector<double> data(N, 1.0);
auto start = std::chrono::high_resolution_clock::now();
normalize(data);
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> duration = end - start;
std::cout << "Duration: " << duration.count() << " seconds" << std::endl;
return 0;
}
Compilação e Execução¶
- Compilação com
O1
:
g++ -O1 -o normalize_O1 normalize.cpp
- Compilação com
O2
:
g++ -O2 -o normalize_O2 normalize.cpp
- Compilação com
O3
:
g++ -O3 -o normalize_O3 normalize.cpp
- Compilação com
Ofast
:
g++ -Ofast -o normalize_Ofast normalize.cpp
Comparação de Desempenho¶
Execute cada versão do programa compilado e compare a duração relatada:
time ./normalize_O1
time ./normalize_O2
time ./normalize_O3
time ./normalize_Ofast
Depois de compilar e executar os programas com diferentes flags de otimização, compare os tempos de execução relatados por cada um. Isso ajudará a entender como diferentes níveis de otimização afetam o desempenho de operações computacionalmente intensivas.