diff --git a/python-perfs/.gitignore b/python-perfs/.gitignore index ba31f92..9829665 100644 --- a/python-perfs/.gitignore +++ b/python-perfs/.gitignore @@ -1,2 +1,5 @@ .cache/ .hypothesis/ +*.so +*.c +*.html diff --git a/python-perfs/bin/cache b/python-perfs/bin/cache index 8bd4b45..0f39a62 100755 --- a/python-perfs/bin/cache +++ b/python-perfs/bin/cache @@ -1,3 +1,17 @@ #!/bin/sh -printf "%s\n" "$*" >&2 -bkt --ttl 1y --cache-dir .cache "$@" 2>&1 + +ARGS="$*" + +args="--ttl 1y --cache-dir .cache" + +while [ "$1" != "--" ] +do + args="$args $1" + shift +done + +before="$(date +"%s.%N")" +bkt $args "$@" 2>&1 +after="$(date +"%s.%N")" + +printf "%s: %.2fs\n\n" "$ARGS" "$(echo "$after - $before"|bc)" >&2 diff --git a/python-perfs/include/collatz_length.py b/python-perfs/include/collatz_length.py new file mode 100644 index 0000000..a265716 --- /dev/null +++ b/python-perfs/include/collatz_length.py @@ -0,0 +1,7 @@ +def collatz_length(n): + if n == 1: + return 0 + if n % 2 == 0: + return 1 + collatz_length(n // 2) + elif n % 2 == 1: + return 1 + collatz_length(n * 3 + 1) diff --git a/python-perfs/include/collatz_length_annotated.py b/python-perfs/include/collatz_length_annotated.py new file mode 100644 index 0000000..89b1ec1 --- /dev/null +++ b/python-perfs/include/collatz_length_annotated.py @@ -0,0 +1,10 @@ +import cython + +@cython.ccall +def collatz_length(n: cython.uint) -> cython.uint: + if n == 1: + return 0 + if n % 2 == 0: + return 1 + collatz_length(n // 2) + elif n % 2 == 1: + return 1 + collatz_length(n * 3 + 1) diff --git a/python-perfs/perfs.md b/python-perfs/perfs.md index b1c5c8b..1011614 100644 --- a/python-perfs/perfs.md +++ b/python-perfs/perfs.md @@ -29,10 +29,11 @@ O(cⁿ) Exponentielle O(n!) Factorielle ``` -::: notes +notes: Il faut les grapher pour s'en rendre compte : cf. include/big.o.py + ## Comparaison asymptotique Exemples. @@ -52,7 +53,7 @@ def is_in(a_set: set, a_value): return a_value in a_set ``` -::: notes +notes: Peu importe la taille de la liste, accéder à un élément prend le même temps. @@ -63,7 +64,7 @@ Attention c'est toujours en base deux. Exemple typique : chercher dans un annuaire. -::: notes +notes: Un annuaire deux fois plus gros ne vous demandera pas deux fois plus de temps mais peut-être une opération de plus. @@ -90,12 +91,12 @@ Typique d'algorithmes de tris. ## Les mesures de complexité -- De temps (CPU consommé) -- D'espace (Mémoire consommée) -- Dans le meilleur des cas -- Dans le pire des cas -- Dans le cas moyen -- Amorti +- De temps (CPU consommé). +- D'espace (Mémoire consommée). +- Dans le meilleur des cas. +- Dans le pire des cas. +- Dans le cas moyen. +- Amorti. - ... @@ -119,22 +120,31 @@ Mais retenir par cœur la complexité de quelques structures - 1 nanoseconde (1 ns) c'est un milliardième de seconde. +## Rappel des unités de temps + +- milli c'est `10 ** -3`, c'est `0.001`. +- micro c'est `10 ** -6`, c'est `0.000_001`. +- nano c'est `10 ** -9`, c'est `0.000_000_001`. + + ## Le cas typique -```bash -$ python -m timeit -s 'container = list(range(10_000_000))' \ - '10_000_001 in container' -#!cache -- python -m timeit -s 'container = list(range(10_000_000))' '10_000_001 in container' +```shell +$ python -m pyperf timeit \ +> --setup 'container = list(range(10_000_000))' \ +> '10_000_001 in container' +#!cache -- python -m pyperf timeit --fast -s 'container = list(range(10_000_000))' '10_000_001 in container' -$ python -m timeit -s 'container = set(range(10_000_000))' \ - '10_000_001 in container' -#!cache -- python -m timeit -n 100 -s 'container = set(range(10_000_000))' '10_000_001 in container' +$ python -m pyperf timeit \ +> --setup 'container = set(range(10_000_000))' \ +> '10_000_001 in container' +#!cache -- python -m pyperf timeit --fast -s 'container = set(range(10_000_000))' '10_000_001 in container' ``` Pourquoi une si grande différence !? -::: notes +notes: C'est l'heure du live coding ! @@ -145,14 +155,14 @@ C'est l'heure du live coding ! `time`, un outil POSIX, mais aussi une fonction native de bash : -```bash +```shell $ time python -c 'container = set(range(10_000_000))' -#!cache -- time -p python -c 'container = set(range(10_000_000))' 2>&1 +#!cache -- time -p python -c 'container = set(range(10_000_000))' ``` Mais `time` ne teste qu'une fois. -::: notes +notes: real 0m0.719s # C'est le temps « sur le mur » user 0m0.521s # Temps CPU passé « dans Python » @@ -180,7 +190,7 @@ Benchmark 1: python -c pass Time (mean ± σ): 19.4 ms ± 0.6 ms ``` -::: notes +notes: N'essayez pas de retenir les chiffres, retenez les faits. @@ -199,7 +209,7 @@ Benchmark 1: /usr/bin/python3.10 -c pass Time (mean ± σ): 19.1 ms ± 0.8 ms ``` -::: notes +notes: Leur parler de `--enable-optimizations` (PGO). @@ -213,7 +223,7 @@ Timeit c'est dans la stdlib de Python, ça s'utilise en ligne de commande ou dep C'est l'équivalent d'hyperfine mais exécutant du Python plutôt qu'un programme : -```bash +```shell $ ~/.local/bin/python3.10 -m pyperf timeit pass ..................... Mean +- std dev: 7.33 ns +- 0.18 ns @@ -223,7 +233,7 @@ $ /usr/bin/python3.10 -m pyperf timeit pass Mean +- std dev: 6.10 ns +- 0.11 ns ``` -::: notes +notes: Avec hyperfine on teste combien de temps ça prend à Python **de démarrer** puis d'exécuter `pass`, ici on teste combien de temps ça @@ -234,7 +244,7 @@ prend d'exécuter `pass`. time, timeit, hyperfine, pyperf c'est bien pour mesurer, comparer. -cProfile nous aider à trouver la fonction coupable dans un script plus gros. +cProfile nous aider à trouver la fonction coupable. ## cProfile, exemple @@ -331,7 +341,7 @@ En cachant `approx_phi` ? #!sed -n '10,/return step1/p' include/phi4.py ``` -::: notes +notes: Notez l'astuce pour que le `step2` d'un tour soit le `step1` du suivant... @@ -346,6 +356,7 @@ $ python -m cProfile --sort cumulative phi4.py 2000 de `fib` n'est pas chaud, et il peut vite devoir descendre profondément en récursion... + ## cProfile, exemple Il est temps de sortir une implémentation de `fib` plus robuste, basée @@ -363,35 +374,37 @@ $ python -m cProfile --sort cumulative phi5.py 2000 #!cache -- python -m cProfile --sort cumulative include/phi5.py 2000 | head -n 2 | sed 's/^ *//g;s/seconds/s/g' ``` -::: notes +notes: Mieux. + ## Snakeviz ```text python -m pip install snakeviz -#!python -m pip install snakeviz >/dev/null 2>&1 python -m cProfile -o phi5.prof phi5.py 2000 -#!if [ ! -f /tmp/phi5.prof ]; then python -m cProfile -o /tmp/phi5.prof include/phi5.py 2000 >/dev/null 2>&1; fi +#!if [ ! -f .cache/phi5.prof ]; then python -m cProfile -o .cache/phi5.prof include/phi5.py 2000 >/dev/null 2>&1; fi python -m snakeviz phi5.prof -#!if [ ! -f .cache/phi5-snakeviz.png ]; then python -m snakeviz -s /tmp/phi5.prof & TOKILL=$!; sleep 1; cutycapt --min-width=1024 --delay=500 --url=http://127.0.0.1:8080/snakeviz/%2Ftmp%2Fphi5.prof --out=.cache/phi5-snakeviz.png ; kill $TOKILL; fi +#!if [ ! -f output/phi5-snakeviz.png ]; then python -m snakeviz -s .cache/phi5.prof & TOKILL=$!; sleep 1; cutycapt --min-width=1024 --delay=500 --url=http://127.0.0.1:8080/snakeviz/%2Ftmp%2Fphi5.prof --out=output/phi5-snakeviz.png ; kill $TOKILL; fi ``` + ## Snakeviz ![](phi5-snakeviz.png) + ## Scalene -```bash +```shell $ python -m pip install scalene -#!python -m pip install scalene >/dev/null 2>&1 $ scalene phi5.py 100000 -#!if [ ! -f .cache/phi5.html ]; then scalene include/phi5.py 100000 --html --outfile .cache/phi5.html --cli >&2; fi -#!if [ ! -f .cache/phi5-scalene.png ]; then cutycapt --min-width=1024 --delay=100 --url=file://$(pwd)/.cache/phi5.html --out=.cache/phi5-scalene.png; fi +##!if [ ! -f output/phi5.html ]; then scalene include/phi5.py 100000 --html --outfile output/phi5.html --cli >&2; fi +##!if [ ! -f output/phi5-scalene.png ]; then cutycapt --min-width=1024 --delay=100 --url=file://$(pwd)/output/phi5.html --out=output/phi5-scalene.png; fi ``` + ## Scalene ![](phi5-scalene.png) @@ -401,18 +414,91 @@ $ scalene phi5.py 100000 Générateur de prénoms français. -::: notes - -See includes/prenom-*.py +Notes: voir includes/prenom-*.py ## TODO - vprof -- https://pypi.org/project/pyflame/ +- https://pypi.org/project/pp3yflame/ # Cython +Cython est un dialecte de Python transpilable en C. + +## Cython démo + +```python +#!cat include/collatz_length.py +``` + +## Cython démo + +#!rm -f include/*.so # Ensure we're not hitting the cythonized one here... +```shell +$ python -m pyperf timeit \ +> -s 'from collatz_length import collatz_length' +> 'collatz_length(837799)' +#!cache -- python -m pyperf timeit --fast --setup 'from include.collatz_length import collatz_length' 'collatz_length(837799)' +``` + +```shell +$ cythonize --inplace collatz_length.py +``` + +#!cythonize --inplace include/collatz_length.py + +```shell +$ python -m pyperf timeit \ +> -s 'from collatz_length import collatz_length' +> 'collatz_length(837799)' +#!cache -- python -m pyperf timeit --fast -s 'from include.collatz_length import collatz_length' 'collatz_length(837799)' # faster +#!# Beware, the cythonized use `-s` while the non cythonized uses `--setup` just to have two cache buckets :D +``` + + +## Cython annotate + +```shell +$ cython -a collatz_length.py +#!cython -a include/collatz_length.py +#!cutycapt --min-width=1024 --delay=500 --url=file://$(pwd)/include/collatz_length.html --out=output/collatz_length.png +``` + +![](collatz_length.png) + + +## Cython annotated + +```python +#!cat include/collatz_length_annotated.py +``` + +```shell +$ cythonize --inplace collatz_length_annotated.py +``` + +#!cythonize --inplace include/collatz_length_annotated.py + +```shell +$ python -m pyperf timeit \ +> -s 'from collatz_length_annotated import collatz_length' +> 'collatz_length(837799)' +#!cache -- python -m pyperf timeit --fast -s 'from include.collatz_length_annotated import collatz_length' 'collatz_length(837799)' +``` + + +## Cython annotate again + +```shell +$ cython -a include/collatz_length_annotated.py +#!cython -a include/collatz_length_annotated.py +#!cutycapt --min-width=1024 --delay=500 --url=file://$(pwd)/include/collatz_length_annotated.html --out=output/collatz_length_annotated.png +``` + +![](collatz_length_annotated.png) + + # Numba # mypyc diff --git a/python-perfs/requirements.txt b/python-perfs/requirements.txt new file mode 100644 index 0000000..db33d9c --- /dev/null +++ b/python-perfs/requirements.txt @@ -0,0 +1,4 @@ +pyperf +cython +snakeviz +scalene