Python performance: Hello Cython

This commit is contained in:
Julien Palard 2023-06-22 11:50:58 +02:00
parent f8818bff93
commit 1a2c0816c3
Signed by: mdk
GPG Key ID: 0EFC1AC1006886F8
6 changed files with 164 additions and 40 deletions

View File

@ -1,2 +1,5 @@
.cache/
.hypothesis/
*.so
*.c
*.html

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -0,0 +1,4 @@
pyperf
cython
snakeviz
scalene