Aula 12 - Exercícios preparatórios para AI
Exercícios para estudar para AI¶
A Ninja Laíz Freitas trabalhou na elaboração de exercícios para que vocês se ambientem com o estilo de prova e exercitem os seus conhecimentos, os Exercícios estão separados por aula, você pode acessa-los clicando aqui ou em "Exercícios" ao lado de "Suporte";
Simulado AI¶
Além dos exercícios preparados pela Ninja Laíz, também disponibilizo um simulado. A prova contará com questões teóricas no Blackboard e questões práticas no Classroom, elaboradas no mesmo nível de dificuldade do simulado disponibilizado.
"É fundamental que você realize os testes necessários para garantir que o SMOWL esteja funcionando corretamente em seu computador antes da prova. A responsabilidade pela infraestrutura adequada é inteiramente do aluno. Caso a ferramenta não esteja disponível, a sua prova será anulada. Em situações de dificuldade técnica, procure o Helpdesk.
Questões Teóricas¶
ATENÇÃO: As respostas estão gigantes para que você tenha material para estudar, ao responder na hora da prova, você pode ser mais direto.
1. Tiling
Explique por que a técnica de tiling melhora o desempenho de programas paralelos.
- Relacione com hierarquia de memória (L1, L2, L3, RAM).
- Diga por que, na prática, costuma-se usar o tamanho da cache L2 como referência para o tamanho dos blocos.
Ver Resposta
A técnica de tiling consiste em dividir um problema grande (matriz, vetor, dados)
em sub-blocos menores, chamados de tiles, que são processados um de cada vez.
O processador tem múltiplos níveis de memória cache:
- L1: muito rápida, mas muito pequena.
- L2: maior que L1, ainda rápida.
- L3: bem maior, mas mais lenta que L1/L2 e é compartilhada entre os cores.
- RAM: muito maior, mas muito lenta comparada às caches.
Se os dados acessados couberem na cache, evitamos o custo de buscar na RAM.
Trabalhar em blocos significa utilizar dados que já estão na cache antes que sejam descartados.
Por que usar L2 como referência para o tamanho dos blocos?
O tiling melhora o desempenho porque organiza o acesso à memória de forma que os dados
“fiquem mais tempo nas caches rápidas” e menos tempo sendo buscados na RAM.
Usar o tamanho da L2 é a prática comum porque ela oferece
o melhor equilíbrio entre capacidade e latência.
2. Balanceamento de carga — OpenMP
Considere um laço paralelizado com OpenMP em que cada iteração tem custo variável.
- Compare
schedule(static)
eschedule(dynamic)
. - Explique em qual cenário cada estratégia é mais vantajosa.
- Cite uma situação em que
schedule(guided)
pode ser melhor.
Ver Resposta
Quando usamos OpenMP, a forma como as iterações de um laço são distribuídas entre as threads pode impactar o desempenho. O static divide o espaço de iterações em blocos fixos e atribui cada bloco a uma thread antes da execução começar. Essa estratégia tem a vantagem de ter overhead muito baixo, porque a divisão é feita apenas uma vez, e mantém uma boa localidade de memória, já que cada thread tende a percorrer regiões contíguas do vetor ou da matriz. Por isso, o schedule(static)
é bastante eficiente quando o custo por iteração é previsível e relativamente uniforme.
O dynamic funciona de forma adaptativa: as threads recebem blocos de iterações sob demanda e, assim que terminam, pegam novos blocos disponíveis. Esse modelo introduz mais overhead, porque é necessário sincronizar a fila de tarefas várias vezes, e pode prejudicar um pouco a localidade de cache, já que as iterações não necessariamente ficam contíguas para cada thread. A contrapartida é que essa abordagem consegue lidar melhor com laços em que o custo das iterações é irregular. Se algumas iterações demandam muito mais trabalho do que outras, o dynamic
evita que uma thread fique sobrecarregada enquanto outras ficam ociosas, promovendo melhor balanceamento da carga de trabalho.
O guided, é uma variação do dynamic. Os blocos começam grandes e vão diminuindo de tamanho conforme o laço avança. Assim, o início da execução tem menos overhead de gerenciamento, enquanto o final se beneficia de blocos menores que ajudam a equilibrar a carga restante. Essa estratégia é útil quando temos um número muito grande de iterações com custos variáveis, pois combina a eficiência inicial de blocos maiores com a adaptabilidade final de blocos pequenos.
Na prática, escolhemos static
para problemas regulares e previsíveis, dynamic
para casos irregulares ou imprevisíveis, e guided
quando queremos um meio-termo entre overhead baixo e balanceamento eficiente, quando temos laços longos e diferentes complexidades computacionais.
3. MPI — Comunicação
Explique a diferença entre comunicação ponto-a-ponto e coletiva no MPI. Dê um exemplo de uso para cada categoria.
Ver Resposta
A comunicação ponto-a-ponto ocorre quando dois processos trocam mensagens diretamente entre si, de forma explícita. Normalmente, isso é feito com funções como MPI_Send
e MPI_Recv
. Nesse modelo, um processo envia uma mensagem contendo dados e outro processo, identificado pelo seu rank, recebe essa mensagem. Esse tipo de comunicação é útil quando queremos ter controle fino sobre quem fala com quem e em que momento, como em um pipeline onde cada processo realiza uma etapa do cálculo e passa o resultado para o próximo.
Já a comunicação coletiva envolve um grupo inteiro de processos dentro de um comunicador, coordenando a troca de informações de forma padronizada. Exemplos típicos são MPI_Bcast
, que envia uma mensagem de um processo para todos os outros, MPI_Reduce
, que combina dados de todos os processos em um único resultado, e MPI_Gather
, que junta dados espalhados em um único processo. Esse tipo de comunicação é mais conveniente quando todos os processos precisam participar da mesma operação, como calcular a soma global de resultados parciais de um vetor distribuído entre diferentes processos.
Assim, enquanto a comunicação ponto-a-ponto é flexível e granular, permitindo desenhar padrões específicos de interação, a comunicação coletiva simplifica operações envolvendo todos os processos.
4. Códigos híbridos MPI+OpenMP
Quais são as vantagens de combinar MPI e OpenMP em um cluster de HPC?
Ver Resposta
A principal vantagem de combinar MPI e OpenMP em um cluster de HPC é aproveitar dois níveis de paralelismo ao mesmo tempo, MPI divide o trabalho entre os diferentes nós do cluster (memória distribuída). O OpenMP divide o trabalho entre os núcleos de CPU dentro de cada nó usando thread(memória compartilhada).
Isso reduz o número de processos MPI, diminui o tráfego de mensagens, e melhora o uso da cache dos nós do cluster.
5. Passagem por valor, referência e ponteiro
a) Explique as diferenças entre passagem por valor, referência e ponteiro em C++. b) Em termos de cópia de dados e eficiência, qual é a diferença prática?
Ver Resposta
Em C++, quando passamos um parâmetro por valor, o compilador cria uma cópia independente da variável original. Isso garante segurança (o original nunca é alterado), mas pode ser custoso se o objeto for grande, já que toda a cópia precisa ser feita.
Na passagem por referência, não há cópia: a função recebe um “apelido” para a variável original. Assim, qualquer modificação feita dentro da função afeta o valor fora dela. Essa forma é eficiente porque evita cópias desnecessárias, mas exige cuidado para não alterar dados de forma indesejada.
Já na passagem por ponteiro, a função recebe o endereço da variável. A eficiência é semelhante à da referência (não há cópia), mas o código fica mais verboso e exige atenção extra, pois ponteiros podem ser nulos ou apontar para locais inválidos na memória.
Na prática, a diferença está no custo de cópia: valores grandes (como vetores ou objetos complexos) são muito mais eficientes quando passados por referência ou ponteiro. A passagem por valor só é recomendada para tipos pequenos e triviais (como int
ou bool
).
6. Escalonamento e desempenho
a) Por que a escolha do escalonamento schedule
pode afetar o desempenho de um programa paralelo com OpenMP?
b) O que isso tem a ver com tiling (divisão de blocos de dados) para encaixar na cache?
Ver Resposta
a) A escolha do schedule
em OpenMP afeta diretamente como as iterações de um loop são distribuídas entre as threads, e isso tem impacto no equilíbrio de carga e no uso eficiente da memória. Se usamos schedule(static)
, cada thread recebe blocos fixos de iterações, o que funciona bem quando todas as iterações têm custo parecido. Mas, se o custo variar, algumas threads podem terminar mais cedo e ficar ociosas, desperdiçando desempenho. Já schedule(dynamic)
e guided
permitem redistribuir iterações conforme as threads vão terminando, equilibrando melhor a carga, mas com um pouco mais de overhead de gerenciamento.
b) Esse conceito se conecta ao tiling porque dividir as os dados em blocos também é uma forma de escalonamento, mas voltada para a hierarquia de memória. Ao quebrar os dados em blocos do tamanho adequado para caber na cache, garantimos que cada thread trabalhe em um conjunto de dados contíguos e reutilizáveis. Tanto o schedule
quanto o tiling
lidam com a divisão do trabalho, o primeiro com balanceamento de carga entre nós, o segundo com otimização da memória cache.
Questões Práticas¶
7. Tiling em produto vetorial
Implemente a multiplicação de dois vetores grandes em blocos tiling
, comparando o desempenho com a versão ingênua.
- Use
Bsize
definido para caber na cache L2. - Meça o tempo de execução com e sem tiling.
// q07.cpp
#include <iostream>
#include <vector>
#include <chrono>
int main(int argc, char** argv) {
int N = (argc > 1 ? std::atoi(argv[1]) : 1000000);
int Bsize = (argc > 2 ? std::atoi(argv[2]) : 32768); // ajuste pensando na L2
std::vector<float> A(N, 1.1f), B(N, 2.2f), C_naive(N, 0.0f), C_tile(N, 0.0f);
// ===== Versão ingênua =====
std::chrono::high_resolution_clock::time_point t0 = std::chrono::high_resolution_clock::now();
// TODO: percorrer i=0..N-1 e preencher C_naive[i] = A[i] * B[i]
std::chrono::high_resolution_clock::time_point t1 = std::chrono::high_resolution_clock::now();
// ===== Versão com tiling =====
std::chrono::high_resolution_clock::time_point t2 = std::chrono::high_resolution_clock::now();
// TODO: laço de blocos
std::chrono::high_resolution_clock::time_point t3 = std::chrono::high_resolution_clock::now();
double naive_ms = std::chrono::duration<double, std::milli>(t1 - t0).count();
double tile_ms = std::chrono::duration<double, std::milli>(t3 - t2).count();
std::cout << "N=" << N << " Bsize=" << Bsize
<< " naive_ms=" << naive_ms
<< " tile_ms=" << tile_ms << "\n";
// TODO: validar (comparar C_naive vs C_tile)
return 0;
}
Ver Resposta
#include <iostream>
#include <vector>
#include <chrono>
// Objetivo: comparar a versão ingênua (varre o vetor todo) com a versão em BLOCOS (tiling).
// Melhor aproveitamento da cache: processamos blocos contíguos que "cabem" na L2.
int main(int argc, char** argv) {
int N = (argc > 1 ? std::atoi(argv[1]) : 1000000);
int Bsize = (argc > 2 ? std::atoi(argv[2]) : 32768); // ajuste pensando na L2 (50–75% da L2/sizeof(float))
std::vector<float> A(N, 1.1f), B(N, 2.2f), C_naive(N, 0.0f), C_tile(N, 0.0f);
// ===== Versão ingênua (baseline) =====
std::chrono::high_resolution_clock::time_point t0 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < N; i++) {
C_naive[i] = A[i] * B[i];
}
std::chrono::high_resolution_clock::time_point t1 = std::chrono::high_resolution_clock::now();
// ===== Versão com tiling =====
// Percorre o vetor em janelas [start, end) contíguas → melhor localidade espacial.
std::chrono::high_resolution_clock::time_point t2 = std::chrono::high_resolution_clock::now();
for (int start = 0; start < N; start += Bsize) {
int end = (start + Bsize < N) ? (start + Bsize) : N;
for (int i = start; i < end; i++) {
C_tile[i] = A[i] * B[i];
}
}
std::chrono::high_resolution_clock::time_point t3 = std::chrono::high_resolution_clock::now();
double naive_ms = std::chrono::duration<double, std::milli>(t1 - t0).count();
double tile_ms = std::chrono::duration<double, std::milli>(t3 - t2).count();
// Validação simples
int erros = 0;
for (int i = 0; i < N; i++) {
if (C_naive[i] != C_tile[i]) { erros++; break; }
}
std::cout << "N=" << N << " Bsize=" << Bsize
<< " naive_ms=" << naive_ms
<< " tile_ms=" << tile_ms
<< " ok=" << (erros == 0) << "\n";
return 0;
}
8. Balanceamento de carga em OpenMP
Implemente um programa em C++ que conta quantos números primos existem em um vetor de inteiros grandes.
- Paralelize com
#pragma omp parallel for
. - Encontre qual é o menor custo de hardware para o melhor benefício de otimização
#include <iostream>
#include <vector>
#include <random>
#include <omp.h>
static bool is_prime(unsigned x) {
// TODO: implementar teste de primalidade simples (ou copiar do material de exercícios)
return false;
}
int main(int argc, char** argv) {
int N = (argc > 1 ? std::atoi(argv[1]) : 500000);
unsigned seed = (argc > 2 ? (unsigned)std::atoi(argv[2]) : 123u);
std::vector<unsigned> v(N);
std::mt19937 rng(seed);
std::uniform_int_distribution<unsigned> U(1u, 5000000u);
for (int i = 0; i < N; i++) v[i] = U(rng);
long long total_primos = 0;
double t0 = omp_get_wtime();
//TODO paralelize adequadamente esse loop
for (int i = 0; i < N; i++) {
// TODO: se for primo, incrementa a lista de primos
}
double t1 = omp_get_wtime();
std::cout << "N=" << N << " primos=" << total_primos
<< " tempo_s=" << (t1 - t0) << "\n";
return 0;
}
Ver Resposta
#include <iostream>
#include <vector>
#include <random>
#include <omp.h>
// Função para verificar se um número é primo
static bool eh_primo(unsigned int numero) {
if (numero < 2) return false; // menores que 2 não são primos
if (numero % 2 == 0) return numero == 2; // múltiplos de 2 só são primos se forem o próprio 2
// Testa apenas divisores ímpares até a raiz quadrada do número
unsigned int divisor = 3;
while (divisor * divisor <= numero) {
if (numero % divisor == 0) return false; // achou divisor → não é primo
divisor += 2; // incrementa de 2 em 2 → só ímpares
}
return true;
}
int main(int argc, char** argv) {
// Quantos números vamos testar (padrão = 500.000)
int quantidade_numeros = (argc > 1 ? std::atoi(argv[1]) : 500000);
// Semente para o gerador de números aleatórios (padrão = 123)
unsigned int semente = (argc > 2 ? (unsigned int)std::atoi(argv[2]) : 123);
// Vetor que guarda os números aleatórios
std::vector<unsigned int> numeros(quantidade_numeros);
// Gerador pseudoaleatório
std::mt19937 gerador(semente);
std::uniform_int_distribution<unsigned int> distribuicao(1, 5000000);
// Preenche o vetor com números aleatórios entre 1 e 5.000.000
for (int i = 0; i < quantidade_numeros; i++) {
numeros[i] = distribuicao(gerador);
}
long long contador_primos = 0; // contador de primos encontrados
// Marca tempo inicial
double tempo_inicio = omp_get_wtime();
// Loop paralelo com OpenMP
// - Cada thread pega um pedaço do vetor
// - reduction(+:contador_primos) → soma local de cada thread é acumulada corretamente
// - schedule(static,4) → podemos testar com OMP_SCHEDULE=dynamic,4 ou guided,4
#pragma omp parallel for reduction(+:contador_primos) schedule(static,4)
for (int i = 0; i < quantidade_numeros; i++) {
if (eh_primo(numeros[i])) {
contador_primos += 1;
}
}
// Marca tempo final
double tempo_fim = omp_get_wtime();
// Exibe resultados
std::cout << "Total de numeros testados = " << quantidade_numeros << "\n";
std::cout << "Quantidade de primos encontrados = " << contador_primos << "\n";
std::cout << "Tempo de execucao = " << (tempo_fim - tempo_inicio) << " segundos\n";
return 0;
}
9. MPI — Somas distribuídas
Escreva um programa MPI que:
- Inicializa um vetor de tamanho
N
norank 0
. - Divide o vetor com
MPI_Scatter
. - Cada processo calcula a soma parcial.
- Usa
MPI_Reduce
para calcular a soma total norank 0
.
// q09_mpi_soma.cpp
#include <mpi.h>
#include <iostream>
#include <vector>
#include <cstdlib>
int main(int argc, char** argv) {
MPI_Init(&argc, &argv);
int rank = 0, size = 1;
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &size);
int N = (argc > 1 ? std::atoi(argv[1]) : (1 << 20));
std::vector<int> v;
if (rank == 0) {
v.resize(N);
for (int i = 0; i < N; i++) v[i] = i % 10;
}
// Assumimos N % size == 0 neste esqueleto
int chunk = N / size;
std::vector<int> local(chunk);
// TODO dividir o vetor com MPI scatter
long long soma_local = 0;
// TODO: somar todos os elementos do vetor local[0..chunk-1] em soma_local
long long soma_total = 0;
// TODO: implementar MPI Reduce
if (rank == 0) {
std::cout << "Soma total = " << soma_total << "\n";
}
MPI_Finalize();
return 0;
}
Ver Resposta
#include <mpi.h>
#include <iostream>
#include <vector>
#include <cstdlib>
// Padrão: assume N % size == 0 para simplicidade.
int main(int argc, char** argv) {
MPI_Init(&argc, &argv);
int rank = 0, size = 1;
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &size);
int N = (argc > 1 ? std::atoi(argv[1]) : (1 << 20)); // ~1M
std::vector<int> v;
if (rank == 0) {
v.resize(N);
for (int i = 0; i < N; i++) v[i] = i % 10; // dados simples
}
int chunk = N / size;
std::vector<int> local(chunk);
// Divide o vetor v em pedaços contíguos para todos os ranks
MPI_Scatter(v.data(), chunk, MPI_INT,
local.data(), chunk, MPI_INT,
0, MPI_COMM_WORLD);
// Soma local do pedaço recebido
long long soma_local = 0;
for (int i = 0; i < chunk; i++) soma_local += local[i];
// Reduz todas as somas locais para o rank 0
long long soma_total = 0;
MPI_Reduce(&soma_local, &soma_total, 1, MPI_LONG_LONG, MPI_SUM, 0, MPI_COMM_WORLD);
if (rank == 0) {
std::cout << "Soma total = " << soma_total << "\n";
}
MPI_Finalize();
return 0;
}
10. Código híbrido MPI+OpenMP
Implemente um programa que calcula o produto escalar entre dois vetores grandes:
- Distribua os vetores entre os processos com
MPI_Scatter
. - Cada processo calcula a soma parcial com OpenMP reduction.
- Combine os resultados no
rank 0
comMPI_Reduce
.
// q10_hibrido_dot.cpp
#include <mpi.h>
#include <omp.h>
#include <iostream>
#include <vector>
#include <cstdlib>
int main(int argc, char** argv) {
MPI_Init(&argc, &argv);
int rank = 0, size = 1;
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &size);
int N = (argc > 1 ? std::atoi(argv[1]) : (1 << 20));
std::vector<float> v, w;
if (rank == 0) {
v.assign(N, 1.0f);
w.assign(N, 2.0f);
}
int chunk = N / size; // esqueleto simples (N múltiplo de size)
std::vector<float> vl(chunk), wl(chunk);
//TODO: implementar o MPI_scatter para o vetor v
//TODO: implementar o MPI_scatter para o vetor w
double soma_local = 0.0;
// TODO: paralelizar com OpenMP e calcular soma_local
double soma_total = 0.0;
// TODO implementar MPI_Reduce
if (rank == 0) {
std::cout << "dot = " << soma_total << "\n";
}
MPI_Finalize();
return 0;
}
Ver respostas
#include <mpi.h>
#include <omp.h>
#include <iostream>
#include <vector>
#include <cstdlib>
// Estratégia: Scatter v e w, cada processo calcula produto parcial com OpenMP (reduction),
// depois Reduce (soma) no rank 0.
int main(int argc, char** argv) {
MPI_Init(&argc, &argv);
int rank = 0, size = 1;
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &size);
int N = (argc > 1 ? std::atoi(argv[1]) : (1 << 20));
std::vector<float> v, w;
if (rank == 0) {
v.assign(N, 1.0f);
w.assign(N, 2.0f);
}
int chunk = N / size; // simples: N múltiplo de size
std::vector<float> vl(chunk), wl(chunk);
MPI_Scatter(v.data(), chunk, MPI_FLOAT, vl.data(), chunk, MPI_FLOAT, 0, MPI_COMM_WORLD);
MPI_Scatter(w.data(), chunk, MPI_FLOAT, wl.data(), chunk, MPI_FLOAT, 0, MPI_COMM_WORLD);
// Redução em double reduz erro de arredondamento
double soma_local = 0.0;
// Paralelismo intra-nó com OpenMP
#pragma omp parallel for reduction(+:soma_local) schedule(static)
for (int i = 0; i < chunk; i++) {
soma_local += (double)vl[i] * (double)wl[i];
}
double soma_total = 0.0;
MPI_Reduce(&soma_local, &soma_total, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
if (rank == 0) {
std::cout << "dot = " << soma_total << "\n";
}
MPI_Finalize();
return 0;
}
11. Otimização com passagem por referência
Implemente duas versões de uma função que calcula a média móvel de um vetor de double
:
- Versão (a) recebe os dados por valor.
- Versão (b) recebe os dados por referência constante.
- Compare os tempos de execução para vetores de 10⁶ elementos e indique qual foi a versão mais eficiente e justifique porque.
#include <iostream>
#include <vector>
#include <random>
#include <chrono>
// (a) Média móvel passando POR VALOR (copia 'dados')
std::vector<double> media_movel_por_valor(std::vector<double> dados, std::size_t janela_K) {
// TODO: validar janela_K (0 < janela_K <= dados.size())
// TODO: somar os 'janela_K' primeiros elementos
// TODO: empurrar a primeira média para o vetor de saída
// TODO: janela deslizante: para i = janela_K..N-1
// soma += dados[i] - dados[i - janela_K];
// empurrar nova média
return std::vector<double>();
}
// (b) TODO passe os dados POR REFERÊNCIA
std::vector<double> media_movel_por_referencia(std::vector<double> dados, std::size_t janela_K){
// TODO: mesma lógica da versão por valor, mas SEM copiar 'dados'
return std::vector<double>();
}
int main(int argc, char** argv) {
// Parâmetros de entrada
std::size_t tamanho_N = (argc > 1 ? (std::size_t)std::atoll(argv[1]) : 1000000);
std::size_t janela_K = (argc > 2 ? (std::size_t)std::atoll(argv[2]) : 128);
// Gera dados aleatórios em [0,1)
std::vector<double> vetor_dados(tamanho_N);
std::mt19937_64 gerador(42u);
std::uniform_real_distribution<double> distribuicao(0.0, 1.0);
for (std::size_t i = 0; i < tamanho_N; i++) {
vetor_dados[i] = distribuicao(gerador);
}
// Mede tempo da versão por VALOR
std::chrono::high_resolution_clock::time_point inicio_valor = std::chrono::high_resolution_clock::now();
std::vector<double> medias_valor = media_movel_por_valor(vetor_dados, janela_K);
std::chrono::high_resolution_clock::time_point fim_valor = std::chrono::high_resolution_clock::now();
// Mede tempo da versão por REFERÊNCIA
std::chrono::high_resolution_clock::time_point inicio_referencia = std::chrono::high_resolution_clock::now();
std::vector<double> medias_referencia = media_movel_por_referencia(vetor_dados, janela_K);
std::chrono::high_resolution_clock::time_point fim_referencia = std::chrono::high_resolution_clock::now();
// Cálculo de tempos (ms)
double tempo_valor_ms = std::chrono::duration<double, std::milli>(fim_valor - inicio_valor).count();
double tempo_referencia_ms = std::chrono::duration<double, std::milli>(fim_referencia - inicio_referencia).count();
// Validação simples (tolerância numérica)
bool resultados_iguais = (medias_valor.size() == medias_referencia.size());
for (std::size_t i = 0; resultados_iguais && i < medias_valor.size(); i++) {
if (std::abs(medias_valor[i] - medias_referencia[i]) > 1e-12) {
resultados_iguais = false;
}
}
// Saída
std::cout << "tempo_valor_ms=" << tempo_valor_ms
<< " tempo_referencia_ms=" << tempo_referencia_ms
<< " iguais=" << (resultados_iguais ? 1 : 0) << "\n";
return 0;
}
Ver resposta
#include <iostream>
#include <vector>
#include <random>
#include <chrono>
#include <cmath>
// (a) Média móvel passando POR VALOR (copia 'dados')
std::vector<double> media_movel_por_valor(std::vector<double> dados, std::size_t janela_K) {
std::vector<double> medias;
if (janela_K == 0 || janela_K > dados.size()) return medias;
// soma inicial dos K primeiros
double soma = 0.0;
for (std::size_t i = 0; i < janela_K; i++) {
soma += dados[i];
}
medias.push_back(soma / (double)janela_K);
// janela deslizante
for (std::size_t i = janela_K; i < dados.size(); i++) {
soma += dados[i]; // entra o novo
soma -= dados[i - janela_K]; // sai o antigo
medias.push_back(soma / (double)janela_K);
}
return medias;
}
// (b) Média móvel passando POR REFERÊNCIA
std::vector<double> media_movel_por_referencia(const std::vector<double>& dados, std::size_t janela_K) {
std::vector<double> medias;
if (janela_K == 0 || janela_K > dados.size()) return medias;
double soma = 0.0;
for (std::size_t i = 0; i < janela_K; i++) {
soma += dados[i];
}
medias.push_back(soma / (double)janela_K);
for (std::size_t i = janela_K; i < dados.size(); i++) {
soma += dados[i];
soma -= dados[i - janela_K];
medias.push_back(soma / (double)janela_K);
}
return medias;
}
int main(int argc, char** argv) {
// Parâmetros de entrada
std::size_t tamanho_N = (argc > 1 ? (std::size_t)std::atoll(argv[1]) : 1000000);
std::size_t janela_K = (argc > 2 ? (std::size_t)std::atoll(argv[2]) : 128);
// Gera dados aleatórios em [0,1)
std::vector<double> vetor_dados(tamanho_N);
std::mt19937_64 gerador(42u);
std::uniform_real_distribution<double> distribuicao(0.0, 1.0);
for (std::size_t i = 0; i < tamanho_N; i++) {
vetor_dados[i] = distribuicao(gerador);
}
// Mede tempo da versão por VALOR
std::chrono::high_resolution_clock::time_point inicio_valor = std::chrono::high_resolution_clock::now();
std::vector<double> medias_valor = media_movel_por_valor(vetor_dados, janela_K);
std::chrono::high_resolution_clock::time_point fim_valor = std::chrono::high_resolution_clock::now();
// Mede tempo da versão por REFERÊNCIA
std::chrono::high_resolution_clock::time_point inicio_referencia = std::chrono::high_resolution_clock::now();
std::vector<double> medias_referencia = media_movel_por_referencia(vetor_dados, janela_K);
std::chrono::high_resolution_clock::time_point fim_referencia = std::chrono::high_resolution_clock::now();
// Cálculo de tempos (ms)
double tempo_valor_ms = std::chrono::duration<double, std::milli>(fim_valor - inicio_valor).count();
double tempo_referencia_ms = std::chrono::duration<double, std::milli>(fim_referencia - inicio_referencia).count();
// Validação simples (tolerância numérica)
bool resultados_iguais = (medias_valor.size() == medias_referencia.size());
for (std::size_t i = 0; resultados_iguais && i < medias_valor.size(); i++) {
if (std::abs(medias_valor[i] - medias_referencia[i]) > 1e-12) {
resultados_iguais = false;
}
}
// Saída
std::cout << "tempo_valor_ms=" << tempo_valor_ms
<< " tempo_referencia_ms=" << tempo_referencia_ms
<< " iguais=" << (resultados_iguais ? 1 : 0) << "\n";
return 0;
}
12. Otimização com ponteiros
Implemente uma função em C++ que calcula a média móvel de um vetor usando aritmética de ponteiros em vez de índices.
- Compare o desempenho com a versão que usa índices tradicionais.
- Mostre que
*(ptr + i)
é equivalente aptr[i]
.
#include <iostream>
#include <vector>
#include <random>
#include <chrono>
std::vector<double> media_movel_ptr(const double* ptr, std::size_t N, std::size_t K) {
// TODO: validar ponteiro e K
// TODO: somar os K primeiros usando *(ptr + i) e empurrar a média
// TODO: janela deslizante: soma += *(ptr + i) - *(ptr + i - K); push média
return std::vector<double>();
}
int main(int argc, char** argv) {
std::size_t N = (argc > 1 ? (std::size_t)std::atoll(argv[1]) : 1000000);
std::size_t K = (argc > 2 ? (std::size_t)std::atoll(argv[2]) : 256);
std::vector<double> v(N);
std::mt19937_64 rng(123u);
std::uniform_real_distribution<double> U(0.0, 1.0);
for (std::size_t i = 0; i < N; i++) v[i] = U(rng);
std::chrono::high_resolution_clock::time_point t0 = std::chrono::high_resolution_clock::now();
std::vector<double> mv = media_movel_ptr(v.data(), v.size(), K);
std::chrono::high_resolution_clock::time_point t1 = std::chrono::high_resolution_clock::now();
std::cout << "ptr_ms=" << std::chrono::duration<double, std::milli>(t1 - t0).count()
<< " out_size=" << mv.size() << "\n";
// TODO: comparar com versão por referência/índices
return 0;
}
Ver Resposta
#include <iostream>
#include <vector>
#include <random>
#include <chrono>
#include <cmath>
// Função: calcula a média móvel de tamanho janela_K usando ponteiros
std::vector<double> media_movel_ptr(const double* dados_ptr, size_t tamanho_N, size_t janela_K) {
// Validação de entrada
if (dados_ptr == nullptr) return std::vector<double>();
if (janela_K == 0 || janela_K > tamanho_N) return std::vector<double>();
std::vector<double> medias; // vetor de saída
double soma = 0.0; // acumulador da janela
// Soma inicial dos primeiros K elementos
for (size_t i = 0; i < janela_K; i++) {
soma += *(dados_ptr + i); // equivalente a dados_ptr[i]
}
medias.push_back(soma / (double)janela_K);
// Janela deslizante: entra um elemento novo, sai o mais antigo
for (size_t i = janela_K; i < tamanho_N; i++) {
soma += *(dados_ptr + i); // adiciona o novo
soma -= *(dados_ptr + i - janela_K); // remove o antigo
medias.push_back(soma / (double)janela_K);
}
return medias;
}
int main(int argc, char** argv) {
// Tamanho do vetor e janela definidos pela linha de comando ou valores padrão
size_t tamanho_N = (argc > 1 ? (size_t)std::atoll(argv[1]) : 1000000);
size_t janela_K = (argc > 2 ? (size_t)std::atoll(argv[2]) : 256);
// Cria vetor de dados aleatórios no intervalo [0, 1)
std::vector<double> vetor_dados(tamanho_N);
std::mt19937 gerador(123); // gerador pseudoaleatório com semente fixa
std::uniform_real_distribution<double> distribuicao(0.0, 1.0);
for (size_t i = 0; i < tamanho_N; i++) {
vetor_dados[i] = distribuicao(gerador);
}
// Mede tempo da versão com ponteiros
auto inicio = std::chrono::high_resolution_clock::now();
std::vector<double> medias = media_movel_ptr(vetor_dados.data(), vetor_dados.size(), janela_K);
auto fim = std::chrono::high_resolution_clock::now();
double tempo_ms = std::chrono::duration<double, std::milli>(fim - inicio).count();
std::cout << "tempo_ptr_ms=" << tempo_ms
<< " tamanho_saida=" << medias.size() << "\n";
return 0;
}