03
De C a C++ y costos

Análisis asintótico y medición

De C a C++ y costos

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 -O2 y 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:

  1. Para seis funciones misteriosas dadas, predecir su clase de crecimiento leyendo el código y anotarlo antes de correr nada.
  2. Escribir un pequeño medidor sobre bench.h: corre una función con varios n, calcula las razones y escribe un CSV.
  3. 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

  1. ¿Cuál es el costo de un for de 0 a n con otro for adentro de 0 a i? ¿Y si el de adentro divide j entre 2 en cada vuelta?
  2. Una función recursiva se llama dos veces con n/2 y hace trabajo constante. Dibujen el árbol: ¿cuántas llamadas hay en total?
  3. 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?
  4. ¿Por qué no sirve medir un programa compilado con sanitizers?
  5. 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.

bench.hDescargar bench.h
// 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
cache_walk.cppDescargar cache_walk.cpp
// 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;
}
call_tree.cppDescargar call_tree.cpp
// 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;
}
cases.cppDescargar cases.cpp
// 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;
}
count_loops.cppDescargar count_loops.cpp
// 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;
}
doubling.cppDescargar doubling.cpp
// 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;
}
first_timer.cppDescargar first_timer.cpp
// 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;
}
growth_table.cppDescargar growth_table.cpp
// 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;
}
sanitizer_cost.cppDescargar sanitizer_cost.cpp
// 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;
}
small_n.cppDescargar small_n.cpp
// Θ(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.

errors/closed_form.cppDescargar errors/closed_form.cpp
// 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;
}
errors/no_keep.cppDescargar errors/no_keep.cpp
// 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;
}
errors/same_array.cppDescargar errors/same_array.cpp
// 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.

test_bench.cppDescargar test_bench.cpp
// 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;
}
test_lab.pyDescargar test_lab.py
"""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()
test_notes.pyDescargar test_notes.py
"""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.

lab/check.hDescargar lab/check.h
// 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
lab/lab_tests.cppDescargar lab/lab_tests.cpp
// 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;
}
lab/measure.cppDescargar lab/measure.cpp
#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
}
lab/measure.hDescargar lab/measure.h
// 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
lab/mysteries.cppDescargar lab/mysteries.cpp
#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},
    };
}
lab/mysteries.hDescargar lab/mysteries.h
// 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
lab/README.mdDescargar lab/README.md
# 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
```
lab/report.mdDescargar lab/report.md
# 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
lab/run_doubling.cppDescargar lab/run_doubling.cpp
// 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;
}
lab/run_session2.cppDescargar lab/run_session2.cpp
// 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;
}
lab/vector_ops.cppDescargar lab/vector_ops.cpp
#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;
}
lab/vector_ops.hDescargar lab/vector_ops.h
// 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
lab/work.hDescargar lab/work.h
// 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;
};

#endif

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# 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__