Guía 2 - Ejercicio 3: Benchmark DFT

Resolución Ejercicio 3 - Benchmark y Análisis de Complejidad de la DFT

Planteo del Benchmark

El objetivo es empíricamente evaluar el rendimiento de nuestra implementación de la DFT (calcular_dft()) frente al aumento del tamaño de entrada NN. El enunciado nos pide variar la frecuencia de sampleo fs∈{10,100,1000,10000}f_s \in \{10, 100, 1000, 10000\} Hz. Como mantenemos la duración de la señal en T=2 sT = 2\text{ s}, la cantidad total de muestras N=fs⋅TN = f_s \cdot T escala directamente con la frecuencia de sampleo.

Complejidad Computacional (Big O\mathcal{O})

Nuestra implementación es matricial:

X=M⋅xX = M \cdot x

Donde MM es una matriz de N×NN \times N elementos, y xx un vector columna de NN elementos. Analicemos dos tipos de complejidad:

  1. Complejidad Temporal: Para multiplicar una matriz de N×NN \times N por un vector N×1N \times 1, se requieren del orden de N2N^2 operaciones elementales (sumas y multiplicaciones complejas). Por tanto, la complejidad temporal es O(N2)\mathcal{O}(N^2).
  2. Complejidad Espacial: Para generar la matriz de pesos MM, necesitamos alojar en memoria RAM N×NN \times N números complejos de punto flotante de doble precisión (complex128, 16 bytes cada uno). Por tanto, la complejidad espacial también es O(N2)\mathcal{O}(N^2).

El colapso del Hardware

Al ejecutar el script de testeo (ejercicio3.py), presenciamos un evento pedagógico brutal: un colapso directo por límite físico (MemoryError).

fsf_s (Hz) Longitud NN Tiempo (s) Memoria Teórica Requerida
10 20 ≈\approx 0.0001 6.4 KB
100 200 ≈\approx 0.003 640 KB
1000 2000 ≈\approx 0.28 64 MB
10000 20000 FALLA ≈\approx 6 GB contiguos

Cuando N=20000N = 20000, la matriz de pesos MM necesita alojar 20000×20000=400.000.00020000 \times 20000 = 400.000.000 números complejos. Al pesar 16 bytes cada uno, Numpy le solicita al SO un bloque contiguo de ∼6\sim 6 GiB. En una PC de escritorio estándar ejecutando Python en Windows, este intento de asignación contigua falla estrepitosamente arrojando Unable to allocate 5.96 GiB.

Modificamos el script para capturar el MemoryError y continuar graficando los puntos que sí tuvieron éxito.

Análisis Gráfico Log-Log

Para visualizar relaciones de la forma T(N)=c⋅NαT(N) = c \cdot N^\alpha, la técnica estándar es aplicar el logaritmo a ambos lados:

log⁡(T)=αlog⁡(N)+log⁡(c)\log(T) = \alpha \log(N) + \log(c)

Esto significa que, graficado en ejes Log-Log, deberíamos ver una recta cuya pendiente es precisamente el exponente α\alpha. Al ajustar linealmente nuestros puntos exitosos (ignorando el primero por ser demasiado rápido y estar dominado por el overhead de Python), el script arrojó una pendiente de ≈2\approx 2. Esto prueba empíricamente nuestro análisis teórico: la implementación matricial escala cuadráticamente con el tamaño del problema.

Gráfico Benchmark Log-Log

[!infobox] Ejercicio 3: Rendimiento de la DFT matricial Contexto: Análisis de complejidad temporal y espacial empírica. Resolución Analítica/Numérica: Se midieron los tiempos de ejecución mediante time.perf_counter(). Se trazó un gráfico Log-Log estimando la pendiente mediante regresión lineal (np.polyfit), confirmando empíricamente que el tiempo crece como O(N2)\mathcal{O}(N^2). Advertencia/Clave: La implementación naive matricial colapsó por memoria (MemoryError) al llegar a N=20000N=20000 debido a su crecimiento espacial O(N2)\mathcal{O}(N^2) (casi 6GB de RAM requeridos). Este fracaso absoluto demuestra por qué en el mundo real NADIE usa la definición cruda de la Transformada Discreta; se utiliza exclusivamente la FFT (Fast Fourier Transform), que reduce la complejidad temporal a O(Nlog⁡N)\mathcal{O}(N \log N) y elimina la matriz gigante reduciendo la espacial a O(N)\mathcal{O}(N).