Semana 3 — Análisis asintótico y medición
Objetivos
- Describir el crecimiento de un programa con las notaciones O, Ω y Θ.
- Analizar código iterativo y recursivo simple contando operaciones.
- Medir el tiempo de un programa de forma honesta y contrastar la medición con la predicción.
Teoría — Análisis asintótico · Medir tiempos
Lectura: Apuntes, semana 3.
Apoyo: Open Data Structures, secs. 1.3.3 (Asymptotic Notation) y 1.5 (Correctness, Time Complexity, and Space Complexity). CLRS, capítulo Characterizing Running Times, solo la visión general.
Temas:
- Por qué contar operaciones y no segundos. Lo que importa cuando n crece.
- Las clases que se repiten: 1, log n, n, n log n, n², 2ⁿ. Qué tamaño de n alcanza cada una en un segundo.
- O, Ω y Θ con intuición y una definición precisa, sin demostraciones por inducción: Matemática Discreta va en paralelo.
- Mejor caso, peor caso y caso promedio. No son lo mismo que O, Ω y Θ.
- Leer código: ciclos en secuencia, ciclos anidados, ciclos que dividen entre dos. Recursión: dibujar el árbol de llamadas y sumar por niveles.
- Espacio además de tiempo.
- Medir:
std::chrono::steady_clock, compilar con-O2y sin sanitizers, repetir y usar la mediana. El compilador puede borrar un cálculo cuyo resultado no se usa. - El experimento de duplicar: si T(2n)/T(n) tiende a 2, el crecimiento es casi lineal; si tiende a 4, cuadrático.
- Lo que O no ve: las constantes y la cache. Por eso se mide.
- Error común: decir que O(n) siempre gana a O(n²). Para n pequeño puede perder.
Laboratorio — Predecir el crecimiento y medirlo
Sesión 1:
- Para seis funciones misteriosas dadas, predecir su clase de crecimiento leyendo el código y anotarlo antes de correr nada.
- Escribir un pequeño medidor sobre
bench.h: corre una función con varios n, calcula las razones y escribe un CSV. - Hacer el experimento de duplicar con cada función y comparar con la predicción.
Sesión 2:
4. Medir operaciones de std::vector: push_back al final frente a insert al inicio.
5. Medir búsqueda lineal frente a binaria sobre el mismo arreglo ordenado.
6. Compilar el mismo experimento con -O0 y con -O2, y explicar qué cambió y qué no. Escribir una conclusión corta con los números obtenidos.
Preguntas de repaso
- ¿Cuál es el costo de un
forde 0 a n con otroforadentro de 0 a i? ¿Y si el de adentro divide j entre 2 en cada vuelta? - Una función recursiva se llama dos veces con n/2 y hace trabajo constante. Dibujen el árbol: ¿cuántas llamadas hay en total?
- Un programa tarda 1, 2, 4 y 8 segundos para n = 1000, 2000, 4000 y 8000. ¿Qué crecimiento sugiere? ¿Y si tarda 1, 4, 16 y 64?
- ¿Por qué no sirve medir un programa compilado con sanitizers?
- Si el peor caso de una operación es Θ(n), ¿puede tardar menos que eso en alguna entrada?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (10)Errores (3)Pruebas (3)Laboratorio (13)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
// Medidor pequeño para las semanas que comparan tiempos.
// Compilar las mediciones con -O2 y sin sanitizers.
#ifndef BENCH_H
#define BENCH_H
#include <algorithm>
#include <cassert>
#include <chrono>
#include <cstddef>
#include <vector>
// Mediana de una lista de números. Con cantidad par, promedia los dos del medio.
inline double median_of(std::vector<double> values) {
assert(!values.empty());
std::sort(values.begin(), values.end());
std::size_t middle = values.size() / 2;
if (values.size() % 2 == 1) {
return values[middle];
}
return (values[middle - 1] + values[middle]) / 2.0;
}
// Corre fn `reps` veces y devuelve la mediana en segundos.
// Todo lo que pase dentro de fn cuenta como tiempo medido.
template <typename Fn>
double median_seconds(int reps, Fn fn) {
assert(reps >= 1);
std::vector<double> times;
times.reserve(static_cast<std::size_t>(reps));
for (int i = 0; i < reps; i++) {
auto start = std::chrono::steady_clock::now();
fn();
auto stop = std::chrono::steady_clock::now();
times.push_back(std::chrono::duration<double>(stop - start).count());
}
return median_of(times);
}
// Evita que el compilador borre un cálculo cuyo resultado no se usa.
// No genera ninguna instrucción: le avisa al compilador que este bloque vacío
// podría leer value y cualquier memoria. Es una extensión de g++ y clang, no
// parte del estándar de C++.
template <typename T>
void keep(const T& value) {
asm volatile("" : : "r"(&value) : "memory");
}
#endif// El mismo recorrido Θ(n), en dos órdenes: lo que O no ve.
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <numeric>
#include <random>
#include <utility>
#include <vector>
#include "bench.h"
// Sigue la cadena next[position] durante next.size() saltos.
std::uint32_t walk(const std::vector<std::uint32_t>& next) {
std::uint32_t position = 0;
for (std::size_t i = 0; i < next.size(); i++) {
position = next[position];
}
return position;
}
// Un solo ciclo que pasa por todos, de i a i + 1.
std::vector<std::uint32_t> ordered_cycle(std::size_t n) {
std::vector<std::uint32_t> next(n);
for (std::size_t i = 0; i < n; i++) {
next[i] = static_cast<std::uint32_t>((i + 1) % n);
}
return next;
}
// Un solo ciclo que pasa por todos, en un orden al azar.
std::vector<std::uint32_t> random_cycle(std::size_t n) {
std::vector<std::uint32_t> order(n);
std::iota(order.begin(), order.end(), 0);
std::mt19937 generator(2026);
for (std::size_t i = n - 1; i > 0; i--) {
std::uniform_int_distribution<std::size_t> pick(0, i - 1);
std::swap(order[i], order[pick(generator)]);
}
std::vector<std::uint32_t> next(n);
for (std::size_t i = 0; i < n; i++) {
next[order[i]] = order[(i + 1) % n];
}
return next;
}
int main() {
std::printf("%10s %8s %12s %12s %7s\n", "n", "KiB", "ordenado", "al azar", "razon");
for (std::size_t n : {1u << 12, 1u << 16, 1u << 20, 1u << 22}) {
std::vector<std::uint32_t> ordered = ordered_cycle(n);
std::vector<std::uint32_t> shuffled = random_cycle(n);
double ordered_time = median_seconds(5, [&] { keep(walk(ordered)); });
double shuffled_time = median_seconds(5, [&] { keep(walk(shuffled)); });
double count = static_cast<double>(n);
std::printf("%10zu %8zu %9.2f ns %9.2f ns %7.1f\n", n, n * sizeof(std::uint32_t) / 1024,
ordered_time / count * 1e9, shuffled_time / count * 1e9,
shuffled_time / ordered_time);
}
return 0;
}// Cuenta llamadas, trabajo y profundidad de cuatro funciones recursivas.
#include <algorithm>
#include <cstdio>
struct Tally {
long long calls = 0;
long long work = 0;
int deepest = 0;
};
// T(n) = T(n/2) + 1
void one_half(long long n, int depth, Tally& tally) {
tally.calls++;
tally.work++;
tally.deepest = std::max(tally.deepest, depth);
if (n <= 1) {
return;
}
one_half(n / 2, depth + 1, tally);
}
// T(n) = 2 T(n/2) + 1
void two_halves(long long n, int depth, Tally& tally) {
tally.calls++;
tally.work++;
tally.deepest = std::max(tally.deepest, depth);
if (n <= 1) {
return;
}
two_halves(n / 2, depth + 1, tally);
two_halves(n / 2, depth + 1, tally);
}
// T(n) = 2 T(n/2) + n
void two_halves_linear(long long n, int depth, Tally& tally) {
tally.calls++;
tally.work += n;
tally.deepest = std::max(tally.deepest, depth);
if (n <= 1) {
return;
}
two_halves_linear(n / 2, depth + 1, tally);
two_halves_linear(n / 2, depth + 1, tally);
}
// T(n) = 2 T(n-1) + 1
void two_minus_one(long long n, int depth, Tally& tally) {
tally.calls++;
tally.work++;
tally.deepest = std::max(tally.deepest, depth);
if (n == 0) {
return;
}
two_minus_one(n - 1, depth + 1, tally);
two_minus_one(n - 1, depth + 1, tally);
}
using Recursive = void (*)(long long, int, Tally&);
void print_table(const char* title, Recursive fn, const long long* sizes, int count) {
std::printf("%s\n", title);
std::printf("%10s %12s %12s %8s\n", "n", "llamadas", "trabajo", "profund.");
for (int i = 0; i < count; i++) {
Tally tally;
fn(sizes[i], 0, tally);
std::printf("%10lld %12lld %12lld %8d\n", sizes[i], tally.calls, tally.work, tally.deepest);
}
std::printf("\n");
}
int main() {
const long long powers[] = {8, 16, 1024, 2048};
const long long others[] = {5, 6, 7, 9, 1000, 1500};
const long long small[] = {5, 10, 20};
print_table("T(n) = T(n/2) + 1", one_half, powers, 4);
print_table("T(n) = 2 T(n/2) + 1", two_halves, powers, 4);
print_table("T(n) = 2 T(n/2) + 1, n que no es potencia de 2", two_halves, others, 6);
print_table("T(n) = 2 T(n/2) + n", two_halves_linear, powers, 4);
print_table("T(n) = 2 T(n-1) + 1", two_minus_one, small, 3);
return 0;
}// Cuenta comparaciones en el mejor caso, el peor caso y el caso promedio.
#include <algorithm>
#include <cassert>
#include <cstddef>
#include <cstdio>
#include <numeric>
#include <random>
#include <vector>
// Devuelve el índice de key, o -1. Suma a comparisons cada comparación con un elemento.
int linear_search(const std::vector<int>& values, int key, long long& comparisons) {
for (std::size_t i = 0; i < values.size(); i++) {
comparisons++;
if (values[i] == key) {
return static_cast<int>(i);
}
}
return -1;
}
// Ordena values y devuelve cuántas comparaciones entre elementos hizo.
long long insertion_sort(std::vector<int>& values) {
long long comparisons = 0;
for (std::size_t i = 1; i < values.size(); i++) {
int current = values[i];
std::size_t j = i;
while (j > 0) {
comparisons++;
if (values[j - 1] <= current) {
break;
}
values[j] = values[j - 1];
j--;
}
values[j] = current;
}
return comparisons;
}
void search_table(int n) {
std::vector<int> values(static_cast<std::size_t>(n));
std::iota(values.begin(), values.end(), 0);
long long best = 0;
linear_search(values, 0, best);
long long worst = 0;
linear_search(values, -1, worst);
long long all_positions = 0;
for (int key = 0; key < n; key++) {
linear_search(values, key, all_positions);
}
double average = static_cast<double>(all_positions) / n;
std::printf("%8d %8lld %12.1f %8lld\n", n, best, average, worst);
}
void sort_table(int n, std::mt19937& generator) {
std::vector<int> ascending(static_cast<std::size_t>(n));
std::iota(ascending.begin(), ascending.end(), 0);
std::vector<int> descending(ascending.rbegin(), ascending.rend());
std::vector<int> shuffled = ascending;
std::shuffle(shuffled.begin(), shuffled.end(), generator);
long long best = insertion_sort(ascending);
long long random = insertion_sort(shuffled);
long long worst = insertion_sort(descending);
assert(std::is_sorted(ascending.begin(), ascending.end()));
assert(std::is_sorted(shuffled.begin(), shuffled.end()));
assert(std::is_sorted(descending.begin(), descending.end()));
std::printf("%8d %10lld %12lld %10lld\n", n, best, random, worst);
}
int main() {
std::printf("busqueda lineal: comparaciones\n");
std::printf("%8s %8s %12s %8s\n", "n", "mejor", "promedio", "peor");
for (int n : {1000, 2000, 4000}) {
search_table(n);
}
std::printf("\nordenamiento por insercion: comparaciones\n");
std::printf("%8s %10s %12s %10s\n", "n", "ordenado", "al azar", "invertido");
std::mt19937 generator(2026);
for (int n : {1000, 2000, 4000}) {
sort_table(n, generator);
}
return 0;
}// Cuenta las vueltas de los patrones de ciclos de los apuntes.
#include <cassert>
#include <cstdio>
#include <string>
#include <vector>
long long two_in_sequence(long long n) {
long long count = 0;
for (long long i = 0; i < n; i++) {
count++;
}
for (long long i = 0; i < n; i++) {
count++;
}
return count;
}
long long nested_full(long long n) {
long long count = 0;
for (long long i = 0; i < n; i++) {
for (long long j = 0; j < n; j++) {
count++;
}
}
return count;
}
long long nested_triangle(long long n) {
long long count = 0;
for (long long i = 0; i < n; i++) {
for (long long j = 0; j < i; j++) {
count++;
}
}
return count;
}
long long halving(long long n) {
long long count = 0;
for (long long j = n; j > 0; j /= 2) {
count++;
}
return count;
}
long long nested_halving(long long n) {
long long count = 0;
for (long long i = 0; i < n; i++) {
for (long long j = i; j > 0; j /= 2) {
count++;
}
}
return count;
}
struct Pattern {
std::string name;
long long (*fn)(long long);
};
int main() {
const std::vector<Pattern> patterns = {
{"secuencia", two_in_sequence},
{"anidados", nested_full},
{"triangulo", nested_triangle},
{"divide", halving},
{"triangulo_divide", nested_halving},
};
const long long sizes[] = {1000, 2000, 4000, 8000};
assert(two_in_sequence(1000) == 2 * 1000);
assert(nested_full(1000) == 1000 * 1000);
assert(nested_triangle(1000) == 1000 * 999 / 2);
assert(halving(1024) == 11);
std::printf("%-18s %10s %10s %10s %10s %7s\n", "patron", "n=1000", "n=2000", "n=4000", "n=8000",
"razon");
for (const Pattern& pattern : patterns) {
std::printf("%-18s", pattern.name.c_str());
long long previous = 0;
long long last = 0;
for (long long n : sizes) {
previous = last;
last = pattern.fn(n);
std::printf(" %10lld", last);
}
std::printf(" %7.2f\n", static_cast<double>(last) / static_cast<double>(previous));
}
return 0;
}// El experimento de duplicar con tres cálculos sobre un arreglo.
#include <algorithm>
#include <cstddef>
#include <cstdio>
#include <random>
#include <vector>
#include "bench.h"
long long sum_values(const std::vector<int>& values) {
long long total = 0;
for (int value : values) {
total += value;
}
return total;
}
// Pares (i, j) con i < j y values[i] > values[j].
long long count_inversions(const std::vector<int>& values) {
long long count = 0;
for (std::size_t i = 0; i < values.size(); i++) {
for (std::size_t j = i + 1; j < values.size(); j++) {
if (values[i] > values[j]) {
count++;
}
}
}
return count;
}
// Cuántas veces repetir fn para que la tanda completa dure al menos 2 ms.
template <typename Fn>
long long batch_size(Fn fn) {
long long batch = 1;
while (true) {
double seconds = median_seconds(1, [&] {
for (long long i = 0; i < batch; i++) {
fn();
}
});
if (seconds >= 0.002) {
return batch;
}
batch *= 2;
}
}
// Tiempo de una llamada a fn: mediana de 7 tandas, entre el tamaño de la tanda.
template <typename Fn>
double seconds_per_call(Fn fn) {
long long batch = batch_size(fn);
double seconds = median_seconds(7, [&] {
for (long long i = 0; i < batch; i++) {
fn();
}
});
return seconds / static_cast<double>(batch);
}
std::vector<int> random_values(std::size_t n, std::mt19937& generator) {
std::vector<int> values(n);
for (int& value : values) {
value = static_cast<int>(generator());
}
return values;
}
// Ordena 15 arreglos distintos, cada uno una sola vez: ordenar otra vez el mismo no sirve.
double sort_seconds(std::size_t n, std::mt19937& generator) {
std::vector<std::vector<int>> arrays;
for (int i = 0; i < 15; i++) {
arrays.push_back(random_values(n, generator));
}
std::size_t next = 0;
return median_seconds(15, [&] {
std::sort(arrays[next].begin(), arrays[next].end());
keep(arrays[next]);
next++;
});
}
void print_header(const char* title) {
std::printf("%s\n", title);
std::printf("%10s %12s %7s %9s\n", "n", "T(n) en us", "razon", "T/n (ns)");
}
void print_row(std::size_t n, double seconds, double previous) {
double per_item = seconds * 1e9 / static_cast<double>(n);
if (previous == 0) {
std::printf("%10zu %12.2f %7s %9.2f\n", n, seconds * 1e6, "-", per_item);
} else {
std::printf("%10zu %12.2f %7.2f %9.2f\n", n, seconds * 1e6, seconds / previous, per_item);
}
}
int main() {
std::mt19937 generator(2026);
print_header("suma");
double previous = 0;
for (std::size_t n = 8000; n <= 256000; n *= 2) {
std::vector<int> values = random_values(n, generator);
double seconds = seconds_per_call([&] { keep(sum_values(values)); });
print_row(n, seconds, previous);
previous = seconds;
}
print_header("\nstd::sort");
previous = 0;
for (std::size_t n = 8000; n <= 256000; n *= 2) {
double seconds = sort_seconds(n, generator);
print_row(n, seconds, previous);
previous = seconds;
}
print_header("\ninversiones");
previous = 0;
for (std::size_t n = 1000; n <= 16000; n *= 2) {
std::vector<int> values = random_values(n, generator);
double seconds = seconds_per_call([&] { keep(count_inversions(values)); });
print_row(n, seconds, previous);
previous = seconds;
}
return 0;
}// Primera medición: un reloj, una suma, varias repeticiones.
#include <algorithm>
#include <chrono>
#include <cstddef>
#include <cstdio>
#include <numeric>
#include <vector>
#include "bench.h"
long long sum_values(const std::vector<int>& values) {
long long total = 0;
for (int value : values) {
total += value;
}
return total;
}
// El salto más pequeño que se alcanza a ver entre dos lecturas seguidas del reloj.
double smallest_tick() {
double smallest = 1.0;
for (int i = 0; i < 100000; i++) {
auto first = std::chrono::steady_clock::now();
auto second = std::chrono::steady_clock::now();
while (second == first) {
second = std::chrono::steady_clock::now();
}
smallest = std::min(smallest, std::chrono::duration<double>(second - first).count());
}
return smallest;
}
int main() {
const std::size_t n = 1000000;
std::vector<int> values(n);
std::iota(values.begin(), values.end(), 0);
auto start = std::chrono::steady_clock::now();
long long total = sum_values(values);
auto stop = std::chrono::steady_clock::now();
keep(total);
double seconds = std::chrono::duration<double>(stop - start).count();
std::printf("una medicion: %.0f us\n", seconds * 1e6);
std::vector<double> times;
for (int i = 0; i < 9; i++) {
times.push_back(median_seconds(1, [&] { keep(sum_values(values)); }));
}
double mean = std::accumulate(times.begin(), times.end(), 0.0) / static_cast<double>(times.size());
std::printf("nueve mediciones (us):");
for (double time : times) {
std::printf(" %.0f", time * 1e6);
}
std::printf("\nmedia %.0f us, mediana %.0f us\n", mean * 1e6, median_of(times) * 1e6);
std::printf("salto minimo del reloj: %.0f ns\n", smallest_tick() * 1e9);
return 0;
}// Tabla de crecimiento, y el mayor n que cabe en un presupuesto de operaciones.
#include <cmath>
#include <cstdio>
#include <initializer_list>
using Cost = double (*)(double);
double cost_log(double n) { return std::log2(n); }
double cost_linear(double n) { return n; }
double cost_n_log(double n) { return n * std::log2(n); }
double cost_square(double n) { return n * n; }
double cost_cube(double n) { return n * n * n; }
double cost_exp(double n) { return std::pow(2.0, n); }
// El mayor entero n, desde 2, con cost(n) <= budget. Búsqueda binaria.
long long largest_n(Cost cost, double budget) {
long long low = 2;
long long high = 4;
while (cost(static_cast<double>(high)) <= budget) {
high *= 2;
}
while (high - low > 1) {
long long middle = low + (high - low) / 2;
if (cost(static_cast<double>(middle)) <= budget) {
low = middle;
} else {
high = middle;
}
}
return low;
}
int main() {
std::printf("%8s %8s %10s %10s %10s %10s\n", "n", "log2 n", "n", "n log2 n", "n^2", "2^n");
for (double n : {10.0, 20.0, 40.0, 80.0, 1000.0, 1000000.0}) {
std::printf("%8.0f %8.1f %10.3g %10.3g %10.3g %10.3g\n", n, cost_log(n), n, cost_n_log(n),
cost_square(n), cost_exp(n));
}
const double budget = 1e9;
std::printf("\ncon %.0f operaciones\n", budget);
std::printf("%10s %14lld\n", "n", largest_n(cost_linear, budget));
std::printf("%10s %14lld\n", "n log2 n", largest_n(cost_n_log, budget));
std::printf("%10s %14lld\n", "n^2", largest_n(cost_square, budget));
std::printf("%10s %14lld\n", "n^3", largest_n(cost_cube, budget));
std::printf("%10s %14lld\n", "2^n", largest_n(cost_exp, budget));
return 0;
}// Dos operaciones medidas: se compila con y sin sanitizers para comparar.
#include <cstddef>
#include <cstdio>
#include <numeric>
#include <vector>
#include "bench.h"
long long sum_values(const std::vector<int>& values) {
long long total = 0;
for (int value : values) {
total += value;
}
return total;
}
void many_small_blocks(int count) {
for (int i = 0; i < count; i++) {
std::vector<int> block(8, i);
keep(block);
}
}
int main() {
std::vector<int> values(1000000);
std::iota(values.begin(), values.end(), 0);
double sum_time = median_seconds(9, [&] { keep(sum_values(values)); });
double block_time = median_seconds(9, [] { many_small_blocks(100000); });
std::printf("suma de 1000000 enteros: %9.0f us\n", sum_time * 1e6);
std::printf("100000 bloques pequenos: %9.0f us\n", block_time * 1e6);
std::printf("bloques / suma: %9.2f\n", block_time / sum_time);
return 0;
}// Θ(U + n) contra Θ(n²), con valores distintos: con n pequeño, gana el cuadrático.
#include <algorithm>
#include <cstddef>
#include <cstdio>
#include <initializer_list>
#include <numeric>
#include <random>
#include <vector>
#include "bench.h"
// Los valores están entre 0 y TABLE_SIZE - 1.
constexpr std::size_t TABLE_SIZE = 1 << 20;
// Con valores distintos, Θ(n²): compara cada par.
bool has_duplicate_pairs(const std::vector<int>& values) {
for (std::size_t i = 0; i < values.size(); i++) {
for (std::size_t j = i + 1; j < values.size(); j++) {
if (values[i] == values[j]) {
return true;
}
}
}
return false;
}
// Θ(TABLE_SIZE + n): una pasada, y siempre limpia una tabla de TABLE_SIZE casillas.
bool has_duplicate_table(const std::vector<int>& values) {
std::vector<char> seen(TABLE_SIZE, 0);
for (int value : values) {
std::size_t position = static_cast<std::size_t>(value);
if (seen[position] != 0) {
return true;
}
seen[position] = 1;
}
return false;
}
int main() {
std::vector<int> all(TABLE_SIZE);
std::iota(all.begin(), all.end(), 0);
std::mt19937 generator(2026);
std::shuffle(all.begin(), all.end(), generator);
std::printf("%8s %14s %14s %12s\n", "n", "pares (us)", "tabla (us)", "pares/tabla");
for (std::size_t n : {10u, 100u, 1000u, 4000u, 16000u, 32000u}) {
// Valores distintos, posibles porque n <= TABLE_SIZE: el peor caso de las dos.
std::vector<int> values(all.begin(), all.begin() + static_cast<std::ptrdiff_t>(n));
int reps = n <= 4000 ? 15 : 3;
double pairs = median_seconds(reps, [&] { keep(has_duplicate_pairs(values)); });
double table = median_seconds(reps, [&] { keep(has_duplicate_table(values)); });
std::printf("%8zu %14.2f %14.2f %12.3f\n", n, pairs * 1e6, table * 1e6, pairs / table);
}
return 0;
}Errores
Ejemplos con errores a propósito: no compilan, fallan o dan un resultado incorrecto. Los apuntes explican cada uno.
// Error a propósito: un ciclo que el compilador reemplaza por una fórmula.
#include <cstdio>
#include <initializer_list>
#include "../bench.h"
long long nested_count(long long n) {
long long count = 0;
for (long long i = 0; i < n; i++) {
for (long long j = 0; j < n; j++) {
count++;
}
}
return count;
}
int main() {
for (long long n : {1000LL, 2000LL, 4000LL, 8000LL}) {
long long count = 0;
double seconds = median_seconds(9, [&] {
count = nested_count(n);
keep(count);
});
std::printf("n = %5lld vueltas = %9lld %8.0f ns\n", n, count, seconds * 1e9);
}
return 0;
}// Error a propósito: mide un cálculo cuyo resultado nadie usa.
#include <cstddef>
#include <cstdio>
#include <numeric>
#include <vector>
#include "../bench.h"
long long sum_values(const std::vector<int>& values) {
long long total = 0;
for (int value : values) {
total += value;
}
return total;
}
int main() {
for (std::size_t n : {1000000, 2000000, 4000000}) {
std::vector<int> values(n);
std::iota(values.begin(), values.end(), 0);
double seconds = median_seconds(9, [&] { sum_values(values); });
std::printf("n = %8zu sin keep: %10.0f ns\n", n, seconds * 1e9);
seconds = median_seconds(9, [&] { keep(sum_values(values)); });
std::printf("n = %8zu con keep: %10.0f ns\n", n, seconds * 1e9);
}
return 0;
}// Error a propósito: repite la medición siempre con los mismos datos.
#include <algorithm>
#include <cstddef>
#include <cstdio>
#include <initializer_list>
#include <random>
#include <vector>
#include "../bench.h"
std::vector<int> random_values(std::size_t n, std::mt19937& generator) {
std::vector<int> values(n);
for (int& value : values) {
value = static_cast<int>(generator());
}
return values;
}
int main() {
std::mt19937 generator(2026);
const int reps = 200;
std::printf("%8s %14s %14s\n", "n", "mismo (us)", "distintos (us)");
for (std::size_t n : {1000u, 4000u, 16000u}) {
// Primero 200 veces una copia del mismo arreglo.
std::vector<int> original = random_values(n, generator);
std::vector<std::vector<int>> copies(reps, original);
std::size_t next = 0;
double same = median_seconds(reps, [&] {
std::sort(copies[next].begin(), copies[next].end());
keep(copies[next]);
next++;
});
// Después 200 arreglos distintos.
std::vector<std::vector<int>> arrays;
for (int i = 0; i < reps; i++) {
arrays.push_back(random_values(n, generator));
}
next = 0;
double different = median_seconds(reps, [&] {
std::sort(arrays[next].begin(), arrays[next].end());
keep(arrays[next]);
next++;
});
std::printf("%8zu %14.1f %14.1f\n", n, same * 1e6, different * 1e6);
}
return 0;
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
// Pruebas de bench.h. No comprueban tiempos: cuentan llamadas y calculan medianas.
#include <string>
#include <vector>
#include "bench.h"
#include "check.h"
int main() {
check(median_of({3.0}) == 3.0, "mediana de un valor");
check(median_of({5.0, 1.0, 3.0}) == 3.0, "mediana con cantidad impar");
check(median_of({4.0, 1.0, 3.0, 2.0}) == 2.5, "mediana con cantidad par");
check(median_of({1.0, 1.0, 1.0, 1.0, 900.0}) == 1.0, "un valor atípico no mueve la mediana");
int calls = 0;
double seconds = median_seconds(7, [&] { calls++; });
check(calls == 7, "median_seconds llama a fn exactamente reps veces");
check(seconds >= 0, "un tiempo no es negativo");
calls = 0;
median_seconds(4, [&] { calls++; });
check(calls == 4, "también con reps par");
int number = 7;
std::vector<int> values = {1, 2, 3};
std::string text = "hola";
keep(number);
keep(values);
keep(text);
keep(number + 1);
check(number == 7 && values.size() == 3 && text == "hola", "keep no cambia nada");
std::printf("ok\n");
return 0;
}"""Pruebas del laboratorio. LAB_DIR dice dónde está el código: lab o solution.
Ninguna prueba compara tiempos, porque dependen de la máquina. Comparan cuentas de pasos,
resultados y el formato del CSV y del informe.
"""
import csv
import os
import pathlib
import re
import subprocess
import tempfile
import unittest
HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")
SOURCE = HERE / LAB_DIR
BUILD = HERE / "build" / LAB_DIR
MYSTERIES = ["mystery_" + letter for letter in "abcdef"]
SESSION2 = ["push_back", "insert_front", "linear_search", "binary_search"]
CLASSES = ["1", "log n", "n", "n log n", "n^2", "2^n"]
def run(*command):
return subprocess.run(
[str(part) for part in command], cwd=HERE, text=True, encoding="utf-8",
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=180,
)
def read_csv(path):
with open(path, encoding="utf-8", newline="") as source:
return list(csv.DictReader(source))
def class_from_steps(rows):
"""La clase que se deduce de los pasos contados, que no dependen de la máquina."""
steps = [int(row["steps"]) for row in rows]
sizes = [int(row["n"]) for row in rows]
last_ratio = steps[-1] / steps[-2]
if steps[-1] == steps[-2]:
return "1"
if last_ratio > 8:
return "2^n"
if last_ratio > 3:
return "n^2"
if last_ratio < 1.5:
return "log n"
if (steps[-1] / sizes[-1]) / (steps[0] / sizes[0]) > 1.2:
return "n log n"
return "n"
def report_rows(text):
rows = {}
for line in text.splitlines():
cells = [cell.strip() for cell in line.strip().strip("|").split("|")]
if cells and cells[0] in MYSTERIES:
rows[cells[0]] = cells
return rows
@unittest.skipUnless(SOURCE.exists(), "falta la carpeta del laboratorio")
class Lab(unittest.TestCase):
def test_unit_tests(self):
done = run(BUILD / "lab_tests")
self.assertEqual(done.returncode, 0, done.stdout)
def test_doubling_csv(self):
with tempfile.TemporaryDirectory() as folder:
path = pathlib.Path(folder) / "doubling.csv"
done = run(BUILD / "run_doubling", path)
self.assertEqual(done.returncode, 0, done.stdout)
rows = read_csv(path)
names = {row["function"] for row in rows}
self.assertEqual(names, set(MYSTERIES))
for name in MYSTERIES:
own = [row for row in rows if row["function"] == name]
self.assertGreater(len(own), 3, name)
sizes = [int(row["n"]) for row in own]
self.assertEqual(sizes, [sizes[0] * 2 ** i for i in range(len(sizes))], name)
self.assertTrue(all(float(row["seconds"]) > 0 for row in own), name)
def test_session2_csv(self):
with tempfile.TemporaryDirectory() as folder:
path = pathlib.Path(folder) / "session2.csv"
done = run(BUILD / "run_session2", path)
self.assertEqual(done.returncode, 0, done.stdout)
rows = read_csv(path)
for name in SESSION2:
own = [row for row in rows if row["function"] == name]
self.assertGreater(len(own), 3, f"faltan filas de {name}")
self.assertTrue(all(float(row["seconds"]) > 0 for row in own), name)
linear = [row for row in rows if row["function"] == "linear_search"]
binary = [row for row in rows if row["function"] == "binary_search"]
self.assertTrue(all(int(row["steps"]) == int(row["n"]) for row in linear),
"la lineal debe medirse en su peor caso")
for row in binary:
self.assertLess(int(row["steps"]), 2 + len(bin(int(row["n"]))), "binaria: demasiados pasos")
def test_report(self):
text = (SOURCE / "report.md").read_text(encoding="utf-8")
self.assertNotIn("PENDIENTE", text, "todavía hay partes sin llenar en el informe")
rows = report_rows(text)
self.assertEqual(set(rows), set(MYSTERIES))
with tempfile.TemporaryDirectory() as folder:
path = pathlib.Path(folder) / "doubling.csv"
run(BUILD / "run_doubling", path)
measured = read_csv(path)
for name, cells in rows.items():
self.assertEqual(len(cells), 5, name)
self.assertIn(cells[1], CLASSES, f"{name}: la predicción no es una clase de la lista")
self.assertIn(cells[4], CLASSES, f"{name}: la clase medida no es de la lista")
self.assertRegex(cells[3], r"\d", f"{name}: faltan las razones medidas")
own = [row for row in measured if row["function"] == name]
self.assertEqual(cells[4], class_from_steps(own), f"{name}: revisen la clase medida")
for title in ["push_back", "lineal", "-O0", "Conclusión"]:
match = re.search(r"^#+ .*%s.*\n((?:(?!^#).*\n?)*)" % re.escape(title), text, re.M)
self.assertIsNotNone(match, f"falta la sección de {title}")
self.assertRegex(match.group(1), r"\d", f"la sección de {title} debe citar números")
@unittest.skipUnless((HERE / "solution").exists(), "falta la solución")
class StarterFails(unittest.TestCase):
"""El laboratorio sin resolver debe compilar y no pasar las pruebas."""
def test_starter_compiles_and_fails(self):
built = run("make", "--no-print-directory", "LAB_DIR=lab", "lab")
self.assertEqual(built.returncode, 0, built.stdout)
self.assertNotIn("warning", built.stdout)
done = run(HERE / "build" / "lab" / "lab_tests")
self.assertNotEqual(done.returncode, 0, "el laboratorio sin resolver pasa las pruebas")
self.assertIn("PENDIENTE", (HERE / "lab" / "report.md").read_text(encoding="utf-8"))
if __name__ == "__main__":
unittest.main()"""Vuelve a correr los comandos de los apuntes y de las diapositivas, y comprueba que el
código citado existe de verdad en esta carpeta.
Si una salida depende de la máquina, solo se compara su forma: se cambian los números por N.
Ninguna prueba compara tiempos, salvo una razón enorme y muy estable en test_keep_works."""
import pathlib
import re
import subprocess
import unittest
HERE = pathlib.Path(__file__).parent
ROOT = HERE.parent.parent
NOTES = ROOT / "notes" / "week-03.md"
SLIDES = ROOT / "slides" / "week-03.tex"
# Antes de un bloque cuyos números dependen de la máquina.
MACHINE = "<!-- depende de la máquina -->"
# Antes de un bloque que omite líneas de la salida, marcadas con '...'.
CUT = "<!-- salida recortada -->"
NUMBER = re.compile(r"-?\d+(\.\d+)?(e[+-]?\d+)?")
def normalize(text):
# Según el idioma del entorno, g++ usa comillas tipográficas o rectas.
return text.replace("‘", "'").replace("’", "'")
def run_shell(command):
done = subprocess.run(
["bash", "-c", command], cwd=HERE, text=True, encoding="utf-8",
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120,
)
return normalize(done.stdout)
def clean_lines(text):
return [line.rstrip() for line in text.splitlines()]
def shape(line):
"""La línea sin sus números y sin el ancho de los espacios."""
return " ".join(NUMBER.sub("N", line).split())
def split_commands(lines):
"""Parte un bloque de terminal en pares (comando, líneas de salida)."""
pairs = []
for line in lines:
if line.startswith("$ "):
pairs.append((line[2:], []))
elif pairs:
pairs[-1][1].append(normalize(line.rstrip()))
return pairs
def note_blocks(language):
"""Devuelve (líneas, marca) de cada bloque de los apuntes."""
blocks, current, mark, previous = [], None, "", ""
indent = 0
for raw in NOTES.read_text(encoding="utf-8").splitlines():
line = re.sub(r"^> ?", "", raw)
if current is None:
if line.strip() == "```" + language:
current, mark = [], previous.strip()
indent = len(line) - len(line.lstrip())
elif line.strip():
previous = line
elif line.strip() == "```":
blocks.append((current, mark))
current = None
else:
current.append(line[indent:])
return blocks
def slide_blocks(environments):
text = SLIDES.read_text(encoding="utf-8")
pattern = r"\\begin\{(?:%s)\}[^\n]*\n(.*?)\\end\{(?:%s)\}" % (environments, environments)
return [match.group(1).splitlines() for match in re.finditer(pattern, text, re.S)]
def known_code_lines():
lines = set()
sources = list(HERE.glob("*.cpp")) + list(HERE.glob("*.h"))
sources += list((HERE / "errors").glob("*.cpp"))
for source in sources:
lines.update(line.strip() for line in source.read_text(encoding="utf-8").splitlines())
return lines
class Common(unittest.TestCase):
def assert_in_order(self, command, expected, actual):
"""Las líneas esperadas aparecen en la salida real, en orden. '...' marca un recorte."""
position = 0
for line in expected:
if not line.strip() or line.strip() == "...":
continue
with self.subTest(command=command, line=line):
found = [i for i in range(position, len(actual)) if actual[i] == line]
self.assertTrue(found, f"no aparece en la salida real: {line}")
if found:
position = found[0] + 1
class NotesAreTrue(Common):
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
def test_terminal_blocks_in_notes(self):
checked = 0
for lines, mark in note_blocks("text"):
if not lines or not lines[0].startswith("$ "):
continue
for command, expected in split_commands(lines):
actual = clean_lines(run_shell(command))
checked += 1
if mark == MACHINE:
with self.subTest(command=command):
self.assertEqual([shape(line) for line in actual],
[shape(line) for line in expected])
elif mark == CUT:
self.assert_in_order(command, expected, actual)
else:
with self.subTest(command=command):
self.assertEqual(actual, expected)
self.assertGreater(checked, 8)
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
def test_code_blocks_in_notes_exist(self):
known = known_code_lines()
for lines, _ in note_blocks("cpp"):
for line in lines:
if line.strip() and line.strip() != "...":
with self.subTest(line=line):
self.assertIn(line.strip(), known)
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
def test_links_exist(self):
text = NOTES.read_text(encoding="utf-8")
for target in re.findall(r"\]\(([^)#\s]+)[^)]*\)", text):
if not target.startswith("http"):
with self.subTest(target=target):
self.assertTrue((NOTES.parent / target).exists())
def test_files_named_in_notes_exist(self):
if not NOTES.exists():
self.skipTest("faltan los apuntes")
text = NOTES.read_text(encoding="utf-8")
for name in set(re.findall(r"`((?:errors/)?[a-z_0-9]+\.(?:cpp|h|py))`", text)):
found = (HERE / name).exists() or (HERE / "lab" / name).exists()
with self.subTest(name=name):
self.assertTrue(found)
class Claims(unittest.TestCase):
"""Afirmaciones de los apuntes, comprobadas con cuentas."""
def table_rows(self, title):
output = run_shell("build/call_tree")
lines = output.split(title + "\n", 1)[1].split("\n\n", 1)[0].splitlines()[1:]
return [[int(cell) for cell in line.split()] for line in lines]
def test_two_halves_makes_2n_minus_1_calls(self):
for n, calls, _, _ in self.table_rows("T(n) = 2 T(n/2) + 1"):
self.assertEqual(calls, 2 * n - 1)
def test_two_halves_other_sizes(self):
for n, calls, _, _ in self.table_rows("T(n) = 2 T(n/2) + 1, n que no es potencia de 2"):
self.assertTrue(n <= calls <= 2 * n - 1, n)
def test_two_halves_linear_is_n_log_n(self):
for n, _, work, depth in self.table_rows("T(n) = 2 T(n/2) + n"):
self.assertEqual(work, n * (depth + 1))
def test_two_minus_one_is_exponential(self):
for n, calls, _, depth in self.table_rows("T(n) = 2 T(n-1) + 1"):
self.assertEqual(calls, 2 ** (n + 1) - 1)
self.assertEqual(depth, n)
def test_nested_halving_stays_between_bounds(self):
import math
output = run_shell("build/count_loops")
row = [line for line in output.splitlines() if line.startswith("triangulo_divide")][0]
counts = [int(cell) for cell in row.split()[1:5]]
for n, count in zip([1000, 2000, 4000, 8000], counts):
self.assertLessEqual(count, 2 * n * math.log2(n))
self.assertGreaterEqual(count, n * math.log2(n) / 4)
def test_keep_works(self):
"""Sin keep el ciclo desaparece y con keep no. La diferencia es de miles de veces."""
run_shell("g++ -Wall -Wextra -std=c++20 -O2 errors/no_keep.cpp -o build/no_keep")
values = {"sin": [], "con": []}
for line in run_shell("build/no_keep").splitlines():
match = re.search(r"(sin|con) keep:\s+(\d+) ns", line)
values[match.group(1)].append(int(match.group(2)))
for without, with_keep in zip(values["sin"], values["con"]):
self.assertGreater(with_keep, 100 * (without + 100))
class SlidesAreTrue(Common):
@unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
def test_program_output_in_slides(self):
for block in slide_blocks("salida"):
for command, expected in split_commands(block):
self.assert_in_order(command, expected, clean_lines(run_shell(command)))
@unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
def test_measurements_in_slides_have_the_right_shape(self):
for block in slide_blocks("medicion"):
for command, expected in split_commands(block):
actual = [shape(line) for line in clean_lines(run_shell(command))]
self.assert_in_order(command, [shape(line) for line in expected], actual)
@unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
def test_code_blocks_in_slides_exist(self):
known = known_code_lines()
for block in slide_blocks("codigo|codigochico|codigomini"):
for line in block:
if line.strip() and line.strip() != "...":
with self.subTest(line=line):
self.assertIn(line.strip(), known)
if __name__ == "__main__":
unittest.main()Laboratorio
Archivos de partida del laboratorio de la semana.
// Comprobación pequeña para las pruebas: dice qué falló y termina con error.
#ifndef CHECK_H
#define CHECK_H
#include <cstdio>
#include <cstdlib>
inline void check(bool condition, const char* what) {
if (!condition) {
std::printf("FALLA: %s\n", what);
std::exit(1);
}
}
#endif// Pruebas públicas del laboratorio. Comprueban cuentas y resultados, nunca tiempos.
#include <algorithm>
#include <cstddef>
#include <random>
#include <sstream>
#include <string>
#include <vector>
#include "check.h"
#include "measure.h"
#include "vector_ops.h"
namespace {
void three_steps_per_item(long long n, Work& work) {
for (long long i = 0; i < 3 * n; i++) {
work.step();
}
}
void test_fill_ratios() {
std::vector<Row> rows(3);
rows[0].seconds = 1.0;
rows[1].seconds = 4.0;
rows[2].seconds = 2.0;
fill_ratios(rows);
check(rows[0].ratio == 0.0, "fill_ratios: la primera fila queda en 0");
check(rows[1].ratio == 4.0, "fill_ratios: segunda fila");
check(rows[2].ratio == 0.5, "fill_ratios: tercera fila");
}
void test_doubling_experiment() {
std::vector<Row> rows = doubling_experiment(three_steps_per_item, 100, 4, 3);
check(rows.size() == 4, "doubling_experiment: una fila por cada n");
long long n = 100;
for (std::size_t i = 0; i < rows.size(); i++) {
check(rows[i].n == n, "doubling_experiment: n se duplica desde first_n");
check(rows[i].steps == 3 * n, "doubling_experiment: steps son los de una sola llamada");
check(rows[i].seconds > 0, "doubling_experiment: falta el tiempo");
n *= 2;
}
check(rows[0].ratio == 0.0, "doubling_experiment: la primera razón es 0");
check(rows[1].ratio == rows[1].seconds / rows[0].seconds,
"doubling_experiment: falta llamar a fill_ratios");
}
void test_write_csv() {
Row first;
first.n = 8;
first.steps = 16;
first.seconds = 0.5;
Row second;
second.n = 16;
second.steps = 32;
second.seconds = 1.5;
second.ratio = 3;
std::ostringstream out;
write_csv(out, "demo", {first, second});
check(out.str() == "demo,8,16,0.5,0\ndemo,16,32,1.5,3\n", "write_csv: formato de las líneas");
}
void test_fill() {
check(fill_back(0).empty(), "fill_back(0) es vacío");
check(fill_front(0).empty(), "fill_front(0) es vacío");
check(fill_back(4) == std::vector<int>({0, 1, 2, 3}), "fill_back(4)");
check(fill_front(4) == std::vector<int>({3, 2, 1, 0}), "fill_front(4)");
}
void test_search_counts() {
std::vector<int> values;
for (int i = 0; i < 1000; i++) {
values.push_back(2 * i);
}
long long comparisons = 0;
check(linear_search(values, 0, comparisons) == 0, "lineal: primer elemento");
check(comparisons == 1, "lineal: mejor caso, una comparación");
comparisons = 0;
check(linear_search(values, 1, comparisons) == -1, "lineal: ausente");
check(comparisons == 1000, "lineal: peor caso, n comparaciones");
comparisons = 0;
check(linear_search(values, 20, comparisons) == 10, "lineal: en medio");
check(comparisons == 11, "lineal: índice 10, once comparaciones");
// Con 1000 elementos: piso de log2(1000) = 9. Se admite una vuelta de más.
for (int key : {-5, 0, 1, 998, 1998, 1999, 5000}) {
comparisons = 0;
binary_search(values, key, comparisons);
check(comparisons >= 1, "binaria: debe contar lo que mira");
check(comparisons <= 11, "binaria: demasiadas comparaciones para n = 1000");
}
std::vector<int> empty;
comparisons = 0;
check(binary_search(empty, 3, comparisons) == -1, "binaria: arreglo vacío");
check(linear_search(empty, 3, comparisons) == -1, "lineal: arreglo vacío");
check(comparisons == 0, "arreglo vacío: ninguna comparación");
}
// Operaciones al azar, con semilla fija, comparando contra la biblioteca estándar.
void test_search_random() {
std::mt19937 generator(2026);
for (int round = 0; round < 300; round++) {
std::size_t n = generator() % 60;
std::vector<int> values(n);
for (int& value : values) {
value = static_cast<int>(generator() % 100);
}
std::sort(values.begin(), values.end());
for (int key = -1; key <= 100; key++) {
bool present = std::binary_search(values.begin(), values.end(), key);
long long comparisons = 0;
int linear = linear_search(values, key, comparisons);
int binary = binary_search(values, key, comparisons);
check((linear != -1) == present, "lineal: no coincide con std::binary_search");
check((binary != -1) == present, "binaria: no coincide con std::binary_search");
if (present) {
check(values[static_cast<std::size_t>(linear)] == key, "lineal: índice equivocado");
check(values[static_cast<std::size_t>(binary)] == key, "binaria: índice equivocado");
}
}
}
}
} // namespace
int main() {
test_fill_ratios();
test_doubling_experiment();
test_write_csv();
test_fill();
test_search_counts();
test_search_random();
std::printf("ok\n");
return 0;
}#include "measure.h"
#include <ostream>
void fill_ratios(std::vector<Row>& /* rows */) {
// Paso 2: para cada fila desde la segunda, ratio = seconds / seconds de la anterior.
}
std::vector<Row> doubling_experiment(MysteryFn /* fn */, long long /* first_n */, int /* row_count */,
int /* reps */) {
// Paso 2: para cada n:
// - cuenten los pasos con una llamada sobre un Work nuevo;
// - midan con seconds_per_call, y usen keep para que el resultado no se borre;
// - agreguen la fila.
// Al final, llamen a fill_ratios.
return {};
}
void write_csv(std::ostream& /* out */, const std::string& /* name */,
const std::vector<Row>& /* rows */) {
// Paso 2: una línea por fila, en el orden nombre,n,steps,seconds,ratio
}// El medidor del laboratorio: mide una función con varios n y escribe un CSV.
#ifndef MEASURE_H
#define MEASURE_H
#include <iosfwd>
#include <string>
#include <vector>
#include "bench.h"
#include "mysteries.h"
// Una fila del experimento de duplicar.
struct Row {
long long n = 0;
long long steps = 0; // pasos de trabajo de una llamada
double seconds = 0; // tiempo de una llamada
double ratio = 0; // seconds de esta fila / seconds de la anterior; 0 en la primera
};
// Tiempo de una llamada a fn. Una llamada corta dura menos que el salto del reloj, así que
// se mide una tanda de llamadas que tarde al menos 1 ms y se divide entre su tamaño.
// Este ya está hecho.
template <typename Fn>
double seconds_per_call(int reps, Fn fn) {
long long batch = 1;
while (true) {
double seconds = median_seconds(1, [&] {
for (long long i = 0; i < batch; i++) {
fn();
}
});
if (seconds >= 0.001) {
break;
}
batch *= 2;
}
double seconds = median_seconds(reps, [&] {
for (long long i = 0; i < batch; i++) {
fn();
}
});
return seconds / static_cast<double>(batch);
}
// Llena ratio en cada fila. La primera queda en 0.
void fill_ratios(std::vector<Row>& rows);
// Mide fn con first_n, 2 * first_n, ... (`row_count` filas), con `reps` repeticiones cada una.
// Devuelve las filas con n, steps, seconds y ratio llenos.
std::vector<Row> doubling_experiment(MysteryFn fn, long long first_n, int row_count, int reps);
// Escribe una línea por fila: nombre,n,steps,seconds,ratio
void write_csv(std::ostream& out, const std::string& name, const std::vector<Row>& rows);
#endif#include "mysteries.h"
#include <algorithm>
void mystery_a(long long n, Work& work) {
for (long long i = 1; i < n; i *= 2) {
for (long long j = 0; j < i; j++) {
work.step();
}
}
}
void mystery_b(long long n, Work& work) {
long long left = n;
while (left > 0) {
work.step();
left /= 2;
}
}
void mystery_c(long long n, Work& work) {
for (long long i = n; i > 0; i -= 2) {
for (long long j = 0; j < i; j++) {
work.step();
}
}
}
void mystery_d(long long n, Work& work) {
long long limit = std::min(n, 4096LL);
for (long long i = 0; i < limit; i++) {
work.step();
}
}
void mystery_e(long long n, Work& work) {
if (n <= 1) {
work.step();
return;
}
for (long long i = 0; i < n; i++) {
work.step();
}
mystery_e(n / 2, work);
mystery_e(n / 2, work);
}
void mystery_f(long long n, Work& work) {
work.step();
if (n == 0) {
return;
}
mystery_f(n - 1, work);
mystery_f(n - 1, work);
}
std::vector<Mystery> all_mysteries() {
return {
{"mystery_a", mystery_a, 1 << 12, 9},
{"mystery_b", mystery_b, 1 << 8, 11},
{"mystery_c", mystery_c, 1 << 7, 7},
{"mystery_d", mystery_d, 1 << 8, 11},
{"mystery_e", mystery_e, 1 << 8, 9},
{"mystery_f", mystery_f, 3, 4},
};
}// Las seis funciones misteriosas. No las cambien.
#ifndef MYSTERIES_H
#define MYSTERIES_H
#include <string>
#include <vector>
#include "work.h"
using MysteryFn = void (*)(long long n, Work& work);
// Una función y los n con que conviene medirla: first_n, 2 * first_n, ...
struct Mystery {
std::string name;
MysteryFn fn;
long long first_n;
int rows;
};
void mystery_a(long long n, Work& work);
void mystery_b(long long n, Work& work);
void mystery_c(long long n, Work& work);
void mystery_d(long long n, Work& work);
void mystery_e(long long n, Work& work);
void mystery_f(long long n, Work& work);
std::vector<Mystery> all_mysteries();
#endif# Laboratorio de la semana 3: predecir el crecimiento y medirlo
Los comandos se corren desde `code/week-03`. Las ideas están en los apuntes, en
[notes/week-03.md](../../../notes/week-03.md).
| Archivo | Qué es | ¿Lo cambian? |
|---|---|---|
| `work.h` | `Work`: un paso de trabajo que cuesta Θ(1) y se puede contar | No |
| `mysteries.h`, `mysteries.cpp` | Las seis funciones misteriosas | No |
| `measure.h` | `Row` y `seconds_per_call` | No |
| `measure.cpp` | El medidor | **Sí** |
| `run_doubling.cpp` | Corre el experimento con las seis y escribe un CSV | No |
| `vector_ops.h` | Lo que debe hacer cada operación de la sesión 2 | No |
| `vector_ops.cpp` | Las cuatro operaciones | **Sí** |
| `run_session2.cpp` | Los experimentos de la sesión 2 | **Sí** |
| `lab_tests.cpp`, `check.h` | Las pruebas públicas | No |
| `report.md` | Su informe | **Sí** |
```sh
make lab # compila en build/lab/ las pruebas y los programas que miden
make test-lab # comprueba todo; falla hasta que terminen
```
Los programas que miden se compilan con `-O2` y sin sanitizers. También se compila una copia con
`-O0` de cada uno, con el sufijo `_O0`. Las pruebas sí llevan sanitizers. Los tiempos dependen de su
máquina y de lo que esté corriendo en ella, así que ninguna prueba compara tiempos: comparan cuentas.
Cada sesión está pensada para unos 90 minutos de trabajo, y el resto queda para preguntar.
## Sesión 1
### Paso 1: predecir, sin correr nada (30 minutos)
Lean `mysteries.cpp`. Para cada función, anoten en `report.md` su clase de crecimiento y una línea que la
justifique. Sumen vueltas, o dibujen el árbol de llamadas. Si la función tiene varias partes, sumen las
partes. No corran nada todavía: si miden primero, ya no están prediciendo.
Una predicción equivocada no es un error del laboratorio. Corregirla a escondidas sí sería un problema, porque lo que
se aprende está en explicar la diferencia.
### Paso 2: el medidor (40 minutos)
En `measure.cpp`, escriban `fill_ratios`, `doubling_experiment` y `write_csv`. Lean antes
`seconds_per_call` en `measure.h`.
```sh
make lab && build/lab/lab_tests
```
Las pruebas se detienen en la primera falla y dicen cuál es. Las tres primeras son de este paso. Después de
pasarlas, `lab_tests` sigue con las de la sesión 2 y falla ahí: es lo esperado por ahora.
### Paso 3: duplicar (20 minutos)
```sh
build/lab/run_doubling build/lab/doubling.csv
```
Tarda unos segundos. Para cada función, llenen las dos últimas columnas de la tabla del informe. Lean las columnas
así:
- `razon` es T(n) / T(n/2). Cerca de 2 sugiere algo casi lineal, y cerca de 4, cuadrático.
- `T-Tant` es lo que sumó esta duplicación. Sirve cuando la razón se acerca a 1.
- `T/n` es el tiempo por elemento. Sirve para distinguir n de n log n.
No se queden solo con la última fila: miren la tabla entera.
## Sesión 2
### Paso 4: `push_back` frente a `insert` al inicio (25 minutos)
En `vector_ops.cpp`, escriban `fill_back` y `fill_front`. En `run_session2.cpp`, escriban
`measure_fill`. Todavía no expliquen por qué `push_back` es tan barato si el arreglo a veces tiene que
crecer: esa cuenta es de la semana 4. Sí expliquen `insert` al inicio: ¿cuántos elementos mueve?
### Paso 5: búsqueda lineal frente a binaria (30 minutos)
Escriban `linear_search` y `binary_search` en `vector_ops.cpp`, y `measure_search` en `run_session2.cpp`.
Busquen el peor caso de las dos y expliquen cómo lo lograron. Cuidado con buscar siempre la misma key: se
miden muchas llamadas seguidas, y el procesador puede aprender el camino.
```sh
make lab && build/lab/run_session2 build/lab/session2.csv
```
### Paso 6: `-O0` y `-O2` (20 minutos)
```sh
build/lab/run_doubling_O0 build/lab/doubling_O0.csv
build/lab/run_session2_O0 build/lab/session2_O0.csv
```
Comparen con los CSV de `-O2`. ¿Qué cambió? ¿Qué no cambió? ¿Cambió lo mismo en todas las operaciones?
Escriban las secciones de la sesión 2 y la conclusión con los números que obtuvieron.
```sh
make test-lab
```# Informe del laboratorio de la semana 3
Reemplacen cada `PENDIENTE`. La columna de predicción se llena **antes** de correr nada, y no se
corrige después: si la medición los contradice, eso se explica en la conclusión.
En las columnas de predicción y de clase medida usen exactamente uno de estos textos:
`1`, `log n`, `n`, `n log n`, `n^2`, `2^n`.
## Sesión 1: las seis funciones
| función | predicción | por qué (una línea) | razones medidas | clase medida |
|---|---|---|---|---|
| mystery_a | PENDIENTE | PENDIENTE | PENDIENTE | PENDIENTE |
| mystery_b | PENDIENTE | PENDIENTE | PENDIENTE | PENDIENTE |
| mystery_c | PENDIENTE | PENDIENTE | PENDIENTE | PENDIENTE |
| mystery_d | PENDIENTE | PENDIENTE | PENDIENTE | PENDIENTE |
| mystery_e | PENDIENTE | PENDIENTE | PENDIENTE | PENDIENTE |
| mystery_f | PENDIENTE | PENDIENTE | PENDIENTE | PENDIENTE |
## Sesión 2
### push_back frente a insert al inicio
PENDIENTE
### Búsqueda lineal frente a binaria
PENDIENTE
### -O0 frente a -O2
PENDIENTE
## Conclusión
PENDIENTE// Corre el experimento de duplicar con las seis funciones.
// Uso: run_doubling [archivo.csv]
#include <cstddef>
#include <cstdio>
#include <fstream>
#include "measure.h"
void print_table(const std::vector<Row>& rows) {
std::printf("%10s %12s %14s %8s %12s %10s\n", "n", "pasos", "T (ns)", "razon", "T-Tant (ns)",
"T/n (ns)");
for (std::size_t i = 0; i < rows.size(); i++) {
const Row& row = rows[i];
double nanos = row.seconds * 1e9;
double difference = i == 0 ? 0 : nanos - rows[i - 1].seconds * 1e9;
std::printf("%10lld %12lld %14.1f %8.2f %12.1f %10.3f\n", row.n, row.steps, nanos, row.ratio,
difference, nanos / static_cast<double>(row.n));
}
}
int main(int argc, char* argv[]) {
const char* path = argc > 1 ? argv[1] : "doubling.csv";
std::ofstream out(path);
if (!out) {
std::printf("no se pudo abrir %s\n", path);
return 1;
}
out << "function,n,steps,seconds,ratio\n";
for (const Mystery& mystery : all_mysteries()) {
std::vector<Row> rows = doubling_experiment(mystery.fn, mystery.first_n, mystery.rows, 5);
std::printf("\n%s\n", mystery.name.c_str());
print_table(rows);
write_csv(out, mystery.name, rows);
}
return 0;
}// Experimentos de la sesión 2. Uso: run_session2 [archivo.csv]
#include <cstdio>
#include <fstream>
#include <vector>
#include "measure.h"
#include "vector_ops.h"
// Paso 4: una fila por n, duplicando desde 2000 hasta 32000, con fill_back o fill_front.
// Usen seconds_per_call, keep y fill_ratios. steps se queda en 0.
std::vector<Row> measure_fill(std::vector<int> (* /* fill */)(int)) {
return {};
}
// Paso 5: una fila por n, duplicando desde 1000 hasta 512000, sobre un arreglo ordenado.
// Elijan bien qué key buscan en cada llamada, y explíquenlo en el informe.
// steps guarda las comparaciones de una búsqueda.
std::vector<Row> measure_search(int (* /* search */)(const std::vector<int>&, int, long long&)) {
return {};
}
int main(int argc, char* argv[]) {
const char* path = argc > 1 ? argv[1] : "session2.csv";
std::ofstream out(path);
if (!out) {
std::printf("no se pudo abrir %s\n", path);
return 1;
}
out << "function,n,steps,seconds,ratio\n";
write_csv(out, "push_back", measure_fill(fill_back));
write_csv(out, "insert_front", measure_fill(fill_front));
write_csv(out, "linear_search", measure_search(linear_search));
write_csv(out, "binary_search", measure_search(binary_search));
std::printf("escrito %s\n", path);
return 0;
}#include "vector_ops.h"
std::vector<int> fill_back(int /* n */) {
// Paso 4.
return {};
}
std::vector<int> fill_front(int /* n */) {
// Paso 4.
return {};
}
int linear_search(const std::vector<int>& /* values */, int /* key */, long long& /* comparisons */) {
// Paso 5.
return -2;
}
int binary_search(const std::vector<int>& /* values */, int /* key */, long long& /* comparisons */) {
// Paso 5.
return -2;
}// Operaciones que se miden en la sesión 2.
#ifndef VECTOR_OPS_H
#define VECTOR_OPS_H
#include <vector>
// Devuelve {0, 1, ..., n - 1}, agregando cada valor con push_back.
std::vector<int> fill_back(int n);
// Devuelve {n - 1, ..., 1, 0}, agregando 0, 1, ..., n - 1 siempre al inicio con insert.
std::vector<int> fill_front(int n);
// Devuelve un índice donde está key, o -1. Suma a comparisons una unidad por cada elemento
// que mira.
int linear_search(const std::vector<int>& values, int key, long long& comparisons);
// Lo mismo, con values ordenado de menor a mayor.
int binary_search(const std::vector<int>& values, int key, long long& comparisons);
#endif// Un paso de trabajo. Cada llamada a step() cuesta lo mismo: Θ(1).
#ifndef WORK_H
#define WORK_H
#include <cstdint>
class Work {
public:
void step() {
state_ ^= state_ >> 7;
state_ *= 0x9E3779B97F4A7C15ULL;
steps_++;
}
std::uint64_t state() const { return state_; }
long long steps() const { return steps_; }
private:
std::uint64_t state_ = 1;
long long steps_ = 0;
};
#endifCompilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 3: análisis asintótico y medición. Se usa dentro del entorno del curso.
#
# make compila los programas de la clase en build/
# make lab compila el laboratorio en build/lab/
# make test corre las pruebas de los ejemplos y de los apuntes
# make test-lab prueba su laboratorio; falla hasta que lo terminen
# make clean borra lo generado
#
# Los programas que cuentan y las pruebas llevan sanitizers. Los que miden tiempos se
# compilan con -O2 y sin sanitizers. Los programas de errors/ miden mal a propósito:
# no se compilan con make, sino con los comandos de los apuntes.
CXX = g++
WARNINGS = -Wall -Wextra -std=c++20
CHECKED = $(WARNINGS) -g -fsanitize=address,undefined
FAST = $(WARNINGS) -O2
SLOW = $(WARNINGS) -O0
BUILD = build
COUNTING = count_loops call_tree cases growth_table test_bench
TIMED = first_timer doubling cache_walk small_n sanitizer_cost
LAB_DIR ?= lab
OUT = $(BUILD)/$(LAB_DIR)
LAB_HEADERS = bench.h lab/check.h lab/work.h lab/mysteries.h lab/measure.h lab/vector_ops.h
LAB_SOURCES = lab/mysteries.cpp $(LAB_DIR)/measure.cpp $(LAB_DIR)/vector_ops.cpp
LAB_TARGETS = lab_tests run_doubling run_doubling_O0 run_session2 run_session2_O0
.PHONY: all lab test test-lab clean
all: $(COUNTING:%=$(BUILD)/%) $(TIMED:%=$(BUILD)/%) $(BUILD)/sanitizer_cost_san
$(COUNTING:%=$(BUILD)/%): $(BUILD)/%: %.cpp bench.h lab/check.h | $(BUILD)
$(CXX) $(CHECKED) -Ilab $< -o $@
$(TIMED:%=$(BUILD)/%): $(BUILD)/%: %.cpp bench.h | $(BUILD)
$(CXX) $(FAST) $< -o $@
# La misma medición, ahora con sanitizers: solo para comparar.
$(BUILD)/sanitizer_cost_san: sanitizer_cost.cpp bench.h | $(BUILD)
$(CXX) $(FAST) -fsanitize=address,undefined $< -o $@
$(OUT)/lab_tests: lab/lab_tests.cpp $(LAB_SOURCES) $(LAB_HEADERS) | $(OUT)
$(CXX) $(CHECKED) -I. -Ilab lab/lab_tests.cpp $(LAB_SOURCES) -o $@
$(OUT)/run_doubling: lab/run_doubling.cpp $(LAB_SOURCES) $(LAB_HEADERS) | $(OUT)
$(CXX) $(FAST) -I. -Ilab lab/run_doubling.cpp $(LAB_SOURCES) -o $@
$(OUT)/run_doubling_O0: lab/run_doubling.cpp $(LAB_SOURCES) $(LAB_HEADERS) | $(OUT)
$(CXX) $(SLOW) -I. -Ilab lab/run_doubling.cpp $(LAB_SOURCES) -o $@
$(OUT)/run_session2: $(LAB_DIR)/run_session2.cpp $(LAB_SOURCES) $(LAB_HEADERS) | $(OUT)
$(CXX) $(FAST) -I. -Ilab $(LAB_DIR)/run_session2.cpp $(LAB_SOURCES) -o $@
$(OUT)/run_session2_O0: $(LAB_DIR)/run_session2.cpp $(LAB_SOURCES) $(LAB_HEADERS) | $(OUT)
$(CXX) $(SLOW) -I. -Ilab $(LAB_DIR)/run_session2.cpp $(LAB_SOURCES) -o $@
$(BUILD):
mkdir -p $@
$(OUT): | $(BUILD)
mkdir -p $@
lab: $(LAB_TARGETS:%=$(OUT)/%)
# Si existe solution/, las pruebas también comprueban la solución del laboratorio.
test: all
$(BUILD)/test_bench
@if [ -d solution ]; then $(MAKE) --no-print-directory LAB_DIR=solution lab; fi
python3 -m unittest discover -p 'test_*.py'
test-lab: all lab
LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab
clean:
rm -rf $(BUILD) __pycache__