cProfile¶
cProfile es un profiler nativo de Python que permite analizar la ejecución de programas, proporcionando información detallada sobre:
- Número de llamadas a cada función
- Tiempos de ejecución (total y por llamada)
- Estadísticas de uso de memoria (en algunas implementaciones)
- Jerarquía de llamadas entre funciones
Ejemplo de uso¶
from cProfile import Profile
from pstats import SortKey, Stats
from functools import lru_cache
@lru_cache(maxsize=None)
def fibonacci(n):
"""Implementación de Fibonacci con memoización usando lru_cache"""
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
def fibonacci_recursive(n):
"""Implementación recursiva simple de Fibonacci (sin optimización)"""
if n <= 1:
return n
return fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2)
if __name__ == "__main__":
n = 35
print("Sin memoization:")
with Profile() as profiler:
print(fibonacci_recursive(n))
(
Stats(profiler)
.sort_stats(SortKey.CUMULATIVE)
.print_stats()
)
print("Con memoization:")
with Profile() as profiler:
print(fibonacci(n))
(
Stats(profiler)
.sort_stats(SortKey.CUMULATIVE)
.print_stats()
)
Sin memoization:
9227465
29860713 function calls (10 primitive calls) in 12.023 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
29860703/1 12.023 0.000 12.023 12.023 /home/cardel/repositorios/infra/clase4/ejemplo3.py:11(fibonacci_recursive)
1 0.000 0.000 0.000 0.000 /usr/lib/python3.13/pstats.py:108(__init__)
1 0.000 0.000 0.000 0.000 /usr/lib/python3.13/pstats.py:118(init)
1 0.000 0.000 0.000 0.000 /usr/lib/python3.13/pstats.py:137(load_stats)
1 0.000 0.000 0.000 0.000 /usr/lib/python3.13/cProfile.py:54(create_stats)
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
1 0.000 0.000 0.000 0.000 {built-in method builtins.print}
1 0.000 0.000 0.000 0.000 {built-in method builtins.isinstance}
1 0.000 0.000 0.000 0.000 {built-in method builtins.hasattr}
1 0.000 0.000 0.000 0.000 {built-in method builtins.len}
Con memoization:
9227465
45 function calls (10 primitive calls) in 0.000 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
36/1 0.000 0.000 0.000 0.000 /home/cardel/repositorios/infra/clase4/ejemplo3.py:5(fibonacci)
1 0.000 0.000 0.000 0.000 /usr/lib/python3.13/pstats.py:108(__init__)
1 0.000 0.000 0.000 0.000 /usr/lib/python3.13/pstats.py:118(init)
1 0.000 0.000 0.000 0.000 /usr/lib/python3.13/pstats.py:137(load_stats)
1 0.000 0.000 0.000 0.000 /usr/lib/python3.13/cProfile.py:54(create_stats)
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
1 0.000 0.000 0.000 0.000 {built-in method builtins.print}
1 0.000 0.000 0.000 0.000 {built-in method builtins.hasattr}
1 0.000 0.000 0.000 0.000 {built-in method builtins.isinstance}
1 0.000 1.000 0.000 0.000 {built-in method builtins.len}
Sin memoización (implementación recursiva simple)¶
Resultado: 9,227,465 (Fibonacci de 35)
29860713 function calls (10 primitive calls) in 12.023 seconds
Estadísticas críticas:¶
- 29,860,703 llamadas a
fibonacci_recursive - 12.023 segundos de tiempo total de ejecución
- Complejidad exponencial: \(O(2^n) \approx 2^{35}\) = 34,359,738,368 operaciones (teórico)
- 29.8 millones de llamadas reales (el profiler captura menos debido a casos base)
Interpretación:¶
- Cada llamada a Fibonacci(n) genera 2 llamadas recursivas
- Cálculos redundantes masivos: Fibonacci(3) se calcula millones de veces
- Tiempo de ejecución prohibitivo para valores mayores
- Demuestra por qué la recursión simple es inviable para Fibonacci
Con memoización (usando lru_cache)¶
Resultado: 9,227,465 (mismo resultado correcto)
45 function calls (10 primitive calls) in 0.000 seconds
Estadísticas críticas:¶
- Solo 36 llamadas a
fibonacci(35 valores + caso base) - Tiempo insignificante: < 0.001 segundos
- Complejidad lineal: O(n) - solo se calcula cada valor una vez
- 45 llamadas totales (incluyendo overhead del sistema)
Interpretación:¶
@lru_cachealmacena resultados previamente calculados- Evita recomputación redundante completamente
- Reduce las llamadas de 29.8 millones a solo 36
- Reduce el tiempo de 12 segundos a milisegundos
Comparativa directa¶
| Métrica | Sin memoización | Con memoización | Mejora |
|---|---|---|---|
| Llamadas a Fibonacci | 29,860,703 | 36 | 829,464x |
| Tiempo total | 12.023 segundos | < 0.001 segundos | > 12,000x |
| Complejidad | O(2^n) exponencial | O(n) lineal | Exponencial → Lineal |
| Escalabilidad | Inviable para n > 40 | Factible para n > 1000 | Múltiples órdenes de magnitud |
Conclusión¶
La memoización transforma un algoritmo exponencialmente costoso en uno linealmente eficiente: - Mismo resultado: Ambas implementaciones calculan correctamente Fibonacci(35) - Diferencia radical en rendimiento: de segundos a microsegundos - Impacto práctico: Hace viable el cálculo de valores grandes de Fibonacci - Demostración poderosa del valor de la optimización algorítmica
Esta comparación ilustra por qué las técnicas de optimización como memoización y programación dinámica son esenciales en desarrollo de software.
Características principales¶
1. Estadísticas del Profiler¶
El objeto Profile captura:
- ncalls: Número total de llamadas a cada función
- tottime: Tiempo total gastado en la función (excluyendo sub-llamadas)
- cumtime: Tiempo acumulado (incluyendo sub-llamadas)
- percall: Tiempo por llamada (tottime/ncalls o cumtime/ncalls)
2. Memoización con lru_cache¶
@lru_cache(maxsize=None) es un decorador que:
- Almacena resultados de llamadas anteriores
- Evita recomputación redundante en funciones recursivas
- Reduce la complejidad temporal de O(2^n) a O(n) para Fibonacci
3. Decoradores en Python¶
Las anotaciones (@decorator) son azúcar sintáctico que:
- Reciben una función como parámetro
- Retornan una función modificada o envuelta
- Permiten agregar funcionalidad sin cambiar el código original
Resultados esperados¶
Sin memoización: - Alto número de llamadas recursivas (~O(2^n)) - Tiempo de ejecución significativo - Múltiples llamadas redundantes
Con memoización: - Número lineal de llamadas (O(n)) - Tiempo de ejecución drásticamente reducido - Reutilización de resultados previamente calculados
Uso desde línea de comandos¶
# Ejecutar programa con cProfile
python -m cProfile mi_programa.py
# Ordenar por tiempo acumulado
python -m cProfile -s cumulative mi_programa.py
# Guardar resultados en archivo
python -m cProfile -o resultados.prof mi_programa.py
# Analizar archivo guardado
python -m pstats resultados.prof
Limitaciones y consideraciones¶
- Overhead: cProfile introduce overhead significativo (~10-30%)
- No mide memoria: Solo tiempos de ejecución (usar
memory_profilerpara memoria) - Granularidad: No proporciona información a nivel de línea
- Alternativas: Para profiling más detallado, considerar
line_profileropyinstrument
cProfile es ideal para identificar cuellos de botella a nivel de función y optimizar el flujo de ejecución del programa.