formations/python-perfs/perfs.md

604 lines
12 KiB
Markdown
Raw Normal View History

2023-09-22 13:33:14 +00:00
# Les performances en Python
2022-06-23 14:10:37 +00:00
par
2023-06-15 12:42:33 +00:00
<!-- .slide: data-background="static/background.jpg" -->
2022-06-23 14:10:37 +00:00
Julien Palard <julien@palard.fr>
https://mdk.fr
# Bien choisir sa structure de donnée
C'est bien choisir l'algorihtme qu'on va utiliser.
## Comparaison asymptotique
2022-09-15 14:16:10 +00:00
Les notations les plus utilisées :
2022-06-23 14:10:37 +00:00
```text
O(1) Constant
2022-09-15 14:16:10 +00:00
O(log n) Logarithmique
2022-06-23 14:10:37 +00:00
O(n) Linéaire
2022-09-15 14:16:10 +00:00
O(n log n) Parfois appelée « linéarithmique »
2022-06-23 14:10:37 +00:00
O(n²) Quadratique
O(nᶜ) Polynomiale
O(cⁿ) Exponentielle
O(n!) Factorielle
```
2023-06-22 09:50:58 +00:00
notes:
2022-09-15 14:16:10 +00:00
2023-09-22 13:33:14 +00:00
Il faut les grapher pour s'en rendre compte : cf. examples/big.o.py
2022-09-15 14:16:10 +00:00
2023-06-22 09:50:58 +00:00
2022-09-15 14:16:10 +00:00
## Comparaison asymptotique
Exemples.
## O(1)
```python
def get_item(a_list: list, an_idx):
return a_list[an_idx]
```
ou
```python
def is_in(a_set: set, a_value):
return a_value in a_set
```
2023-06-22 09:50:58 +00:00
notes:
2022-09-15 14:16:10 +00:00
Peu importe la taille de la liste, accéder à un élément prend le même temps.
## O(log n)
Attention c'est toujours en base deux.
Exemple typique : chercher dans un annuaire.
2023-06-22 09:50:58 +00:00
notes:
2022-09-15 14:16:10 +00:00
Un annuaire deux fois plus gros ne vous demandera pas deux fois plus
de temps mais peut-être une opération de plus.
## O(log n)
```python
2023-09-22 13:33:14 +00:00
#!sed -n '/def index/,/raise ValueError/p' examples/find_in_list.py
2022-09-15 14:16:10 +00:00
```
## O(n)
```python
2023-09-22 13:33:14 +00:00
#!sed -n '/def dumb_index/,/raise ValueError/p' examples/find_in_list.py
2022-09-15 14:16:10 +00:00
```
## O(n log n)
C'est `n` fois un `log n`, par exemple rayer `n` personnes dans un annuaire.
Typique d'algorithmes de tris.
2022-06-23 14:10:37 +00:00
## Les mesures de complexité
2023-06-22 09:50:58 +00:00
- De temps (CPU consommé).
2023-09-22 13:33:14 +00:00
- D'espace (mémoire consommée).
2023-06-22 09:50:58 +00:00
- Dans le meilleur des cas.
- Dans le pire des cas.
- Dans le cas moyen.
- Amorti.
2022-06-23 14:10:37 +00:00
- ...
## Les mesures de complexité
Il n'est pas forcément nécessaire d'apprendre par cœur toutes les complexités de chaque opération.
Pas toute suite.
## Les bases
Mais retenir par cœur la complexité de quelques structures
élémentaires permet d'éviter les « erreurs de débutants ».
## Rappel des unités de temps
- 1 milliseconde (1 ms) c'est un millième de seconde.
- 1 microseconde (1 μs) c'est un millionième de seconde.
- 1 nanoseconde (1 ns) c'est un milliardième de seconde.
2023-06-22 09:50:58 +00:00
## 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`.
2022-06-23 14:10:37 +00:00
## Le cas typique
2023-06-22 09:50:58 +00:00
```shell
2023-09-22 13:33:14 +00:00
#!cache python -m pyperf timeit --setup 'container = list(range(10_000_000))' '10_000_001 in container'
2022-06-23 14:10:37 +00:00
2023-09-22 13:33:14 +00:00
#!cache python -m pyperf timeit --setup 'container = set(range(10_000_000))' '10_000_001 in container'
2022-06-23 14:10:37 +00:00
```
Pourquoi une si grande différence !?
2023-06-22 09:50:58 +00:00
notes:
2022-06-23 14:10:37 +00:00
C'est l'heure du live coding !
2023-09-22 13:33:14 +00:00
# À vous !
2023-10-09 12:09:02 +00:00
Simulez un tas de sable, moi je calcule le nombre l'or.
2023-09-22 13:33:14 +00:00
Ne vous souciez pas des perfs, on s'en occupera.
notes:
2023-10-09 12:09:02 +00:00
Leur laisser ~15mn.
2023-09-22 13:33:14 +00:00
voir sandpile.py
2022-06-23 14:10:37 +00:00
# Les outils
2023-06-22 09:50:58 +00:00
notes:
2022-06-23 14:10:37 +00:00
2023-10-09 12:09:02 +00:00
Je mesure mes perfs, puis ils mesurent leurs perfs.
2022-06-23 14:10:37 +00:00
2023-10-09 12:09:02 +00:00
## `pyperf command`
2022-06-23 14:10:37 +00:00
2023-09-22 13:33:14 +00:00
```shell
2023-10-09 12:09:02 +00:00
#!cache pyperf command python examples/phi1.py 3
#!cache pyperf command python examples/phi1.py 6
2022-06-23 14:10:37 +00:00
2023-10-09 12:09:02 +00:00
#!cache pyperf command python examples/phi1.py 9
```
2022-06-23 14:10:37 +00:00
## Petite parenthèse
Mais attention, démarrer un processus Python n'est pas gratuit :
2023-09-22 13:33:14 +00:00
```shell
2023-10-09 12:09:02 +00:00
#!cache pyperf command python -c pass
2022-06-23 14:10:37 +00:00
```
2023-06-22 09:50:58 +00:00
notes:
2022-09-15 14:16:10 +00:00
N'essayez pas de retenir les chiffres, retenez les faits.
2022-06-23 14:10:37 +00:00
## Petite parenthèse
Et puis il peut dépendre de la version de Python, des options de compilation, ... :
2023-09-22 13:33:14 +00:00
```shell
2023-10-09 12:09:02 +00:00
$ pyperf command ~/.local/bin/python3.10 -c pass
.....................
command: Mean +- std dev: 37.6 ms +- 0.6 ms
2022-06-23 14:10:37 +00:00
2023-10-09 12:09:02 +00:00
$ pyperf command /usr/bin/python3.10 -c pass
.....................
command: Mean +- std dev: 14.4 ms +- 0.4 ms
2022-06-23 14:10:37 +00:00
```
2023-06-22 09:50:58 +00:00
notes:
2022-06-23 14:10:37 +00:00
Leur parler de `--enable-optimizations` (PGO).
2023-10-09 12:09:02 +00:00
## `pyperf timeit`
2022-06-23 14:10:37 +00:00
2023-10-09 12:09:02 +00:00
Il existe aussi `timeit` dans la stdlib, mais je préfère `pyperf timeit` :
2022-06-23 14:10:37 +00:00
2023-09-22 13:33:14 +00:00
```shell
2023-10-09 12:09:02 +00:00
#!cache pyperf timeit --setup 'from examples.phi1 import approx_phi_up_to' 'approx_phi_up_to(3)'
2023-09-22 13:33:14 +00:00
```
2022-06-23 14:10:37 +00:00
2023-09-22 13:33:14 +00:00
## Les outils — À vous !
Effectuez quelques mesures sur votre implémentation.
Tentez d'en déterminer la complexité en fonction du nombre de grains.
2023-10-09 12:09:02 +00:00
Explorez les limites de vos implémentations.
2022-06-23 14:10:37 +00:00
2023-09-22 13:33:14 +00:00
# Profilage
2022-06-23 14:10:37 +00:00
2023-10-09 12:09:02 +00:00
`pyperf` c'est bien pour mesurer, comparer.
2023-09-22 13:33:14 +00:00
2023-10-09 12:09:02 +00:00
Le profilage peut nous aider à trouver la fonction coupable.
2022-06-23 14:10:37 +00:00
## cProfile, exemple
```python
2023-09-22 13:33:14 +00:00
#!sed -n '/def fib/,/return approx/p' examples/phi1.py
2022-06-23 14:10:37 +00:00
```
## cProfile, exemple
Sortons cProfile :
2023-09-22 13:33:14 +00:00
```shell
2022-06-29 21:26:20 +00:00
$ python -m cProfile --sort cumulative phi1.py 10
2022-09-15 14:16:10 +00:00
...
2023-09-22 13:33:14 +00:00
#!cache python -m cProfile --sort cumulative examples/phi1.py 10 | sed -n '/fib\|function calls/{s/ \+/ /g;s/^ *//;p}'
2022-06-29 21:26:20 +00:00
...
2022-06-23 14:10:37 +00:00
```
2022-06-29 21:26:20 +00:00
C'est donc `fib` la coupable :
- C'est ~100% du temps (`cumtime`).
- C'est ~100% des appels de fonctions.
2022-06-23 14:10:37 +00:00
## cProfile, exemple
2022-06-29 21:26:20 +00:00
Cachons les résultats de `fib` :
```python
2023-09-22 13:33:14 +00:00
#!sed -n '/import cache/,/return fib/p' examples/phi2.py
2022-06-29 21:26:20 +00:00
```
2022-06-23 14:10:37 +00:00
## cProfile, exemple
2022-06-29 21:26:20 +00:00
Et on repasse dans cProfile !
2022-06-23 14:10:37 +00:00
2023-09-22 13:33:14 +00:00
```shell
2022-06-29 21:26:20 +00:00
$ python -m cProfile --sort cumulative phi2.py 10
2023-09-22 13:33:14 +00:00
...
#!cache python -m cProfile --sort cumulative examples/phi2.py 10 | sed -n '/fib\|function calls/{s/ \+/ /g;s/^ *//;p}'
...
2022-06-23 14:10:37 +00:00
```
2022-06-29 21:26:20 +00:00
C'est mieux !
2022-06-23 14:10:37 +00:00
## cProfile, exemple
2022-06-29 21:26:20 +00:00
On essaye d'aller plus loin ?
2022-06-23 14:10:37 +00:00
2023-09-22 13:33:14 +00:00
```shell
#!cache python -m cProfile --sort cumulative examples/phi2.py 2000 | head -n 3 | sed 's/^ *//g;s/seconds/s/g'
2022-06-23 14:10:37 +00:00
```
2022-06-29 21:26:20 +00:00
Ça tient, mais peut-on faire mieux ?
2022-06-23 14:10:37 +00:00
## cProfile, exemple
2022-06-29 21:26:20 +00:00
Divisons par 10 le nombre d'appels, on réduira mécaniquement par 10 le
temps d'exécution ?
2022-06-23 14:10:37 +00:00
```python
2023-09-22 13:33:14 +00:00
#!sed -n '/def approx_phi_up_to/,/return step1/p' examples/phi3.py
2022-06-23 14:10:37 +00:00
```
## cProfile, exemple
2023-09-22 13:33:14 +00:00
```shell
#!cache python -m cProfile --sort cumulative examples/phi3.py 2000 | head -n 3 | sed 's/^ *//g;s/seconds/s/g'
2022-06-23 14:10:37 +00:00
```
2023-09-22 13:33:14 +00:00
2022-06-23 14:10:37 +00:00
## cProfile, exemple
2022-06-29 21:26:20 +00:00
En cachant `approx_phi` ?
2022-06-23 14:10:37 +00:00
```python
2023-09-22 13:33:14 +00:00
#!sed -n '10,/return step1/p' examples/phi4.py
2022-06-23 14:10:37 +00:00
```
2023-06-22 09:50:58 +00:00
notes:
2022-06-23 14:10:37 +00:00
2022-06-29 21:26:20 +00:00
Notez l'astuce pour que le `step2` d'un
tour soit le `step1` du suivant...
2023-09-22 13:33:14 +00:00
2022-06-29 21:26:20 +00:00
## cProfile, exemple
2022-06-23 14:10:37 +00:00
2023-09-22 13:33:14 +00:00
```shell
$ python -m cProfile --sort cumulative examples/phi4.py 2000
2022-09-15 14:16:10 +00:00
```
2022-06-23 14:10:37 +00:00
`RecursionError` !? En effet, en avançant par si grands pas, le cache
de `fib` n'est pas chaud, et il peut vite devoir descendre
profondément en récursion...
2023-06-22 09:50:58 +00:00
2022-06-23 14:10:37 +00:00
## cProfile, exemple
Il est temps de sortir une implémentation de `fib` plus robuste, basée
sur l'algorithme « matrix exponentiation » :
2022-06-29 21:26:20 +00:00
```python
2023-09-22 13:33:14 +00:00
#!sed -n '/def fib/,/return fib/p' examples/phi5.py
2022-06-23 14:10:37 +00:00
```
## cProfile, exemple
2022-06-29 21:26:20 +00:00
```text
2023-09-22 13:33:14 +00:00
#!cache python -m cProfile --sort cumulative examples/phi5.py 2000 | head -n 3 | sed 's/^ *//g;s/seconds/s/g'
2022-06-23 14:10:37 +00:00
```
2023-06-22 09:50:58 +00:00
notes:
2022-06-29 21:26:20 +00:00
2022-06-23 14:10:37 +00:00
Mieux.
2023-06-22 09:50:58 +00:00
2022-06-29 21:26:20 +00:00
## Snakeviz
2023-09-22 13:33:14 +00:00
```shell
$ python -m pip install snakeviz
$ python -m cProfile -o phi5.prof phi5.py 2000
$ python -m snakeviz phi5.prof
2022-06-29 21:26:20 +00:00
```
2023-09-22 13:33:14 +00:00
#!if [ ! -f .cache/phi5.prof ]; then python -m cProfile -o .cache/phi5.prof examples/phi5.py 2000 >/dev/null 2>&1; fi
2023-10-09 12:09:02 +00:00
#!if [ ! -f output/phi5-snakeviz.png ]; then python -m snakeviz -s .cache/phi5.prof & TOKILL=$!; sleep 1; cutycapt --min-width=1024 --delay=500 --url=file://$(pwd)/.cache/phi5.prof --out=output/phi5-snakeviz.png ; kill $TOKILL; fi
2023-09-22 13:33:14 +00:00
2023-06-22 09:50:58 +00:00
2022-06-29 21:26:20 +00:00
## Snakeviz
2022-06-23 14:10:37 +00:00
2022-09-15 14:16:10 +00:00
![](phi5-snakeviz.png)
2023-06-22 09:50:58 +00:00
2022-09-15 14:16:10 +00:00
## Scalene
2023-06-22 09:50:58 +00:00
```shell
2022-09-15 14:16:10 +00:00
$ python -m pip install scalene
$ scalene phi5.py 100000
```
2023-09-22 13:33:14 +00:00
#!if [ ! -f output/phi5.html ]; then ( cd examples; scalene 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
2022-09-15 14:16:10 +00:00
## Scalene
![](phi5-scalene.png)
2023-10-09 12:09:02 +00:00
## line_profiler
```shell
$ python -m pip install line_profiler
#!cache python -m kernprof --view --prof-mod examples/phi5.py --line-by-line examples/phi5.py 100000
```
2023-09-22 13:33:14 +00:00
## Aussi
2022-09-15 14:16:10 +00:00
2023-09-22 13:33:14 +00:00
- https://github.com/gaogaotiantian/viztracer
- https://github.com/joerick/pyinstrument
- https://github.com/benfred/py-spy
- https://github.com/sumerc/yappi
- https://github.com/vmprof/vmprof-python
- https://github.com/bloomberg/memray
2023-10-09 12:09:02 +00:00
- https://github.com/pythonprofilers/memory_profiler
2022-09-15 14:16:10 +00:00
2023-09-22 13:33:14 +00:00
## Profilage — À vous !
2022-06-29 21:26:20 +00:00
2023-09-22 13:33:14 +00:00
Profilez votre implémentation et tentez quelques améliorations.
2022-09-15 14:16:10 +00:00
2022-06-23 14:10:37 +00:00
# Cython
2023-06-22 09:50:58 +00:00
Cython est un dialecte de Python transpilable en C.
2023-09-22 13:33:14 +00:00
2023-06-22 09:50:58 +00:00
## Cython démo
2023-10-09 12:09:02 +00:00
Sans modifier le code :
2023-06-22 09:50:58 +00:00
```python
2023-10-09 12:09:02 +00:00
$ pip install cython
$ cythonize --inplace examples/phi5cython.py
#!cythonize --inplace examples/phi5cython.py
2023-06-22 09:50:58 +00:00
```
## Cython démo
```
2023-10-09 12:09:02 +00:00
#!cache pyperf timeit --setup 'from examples.phi5 import approx_phi_up_to' 'approx_phi_up_to(100_000)'
2023-06-22 09:50:58 +00:00
2023-10-09 12:09:02 +00:00
#!cache pyperf timeit --setup 'from examples.phi5cython import approx_phi_up_to' 'approx_phi_up_to(100_000)'
2023-06-22 09:50:58 +00:00
```
2023-10-09 12:09:02 +00:00
## Cython démo
2023-06-22 09:50:58 +00:00
2023-10-09 12:09:02 +00:00
En annotant le fichier on permet à cython d'utiliser des types natifs.
2023-06-22 09:50:58 +00:00
2023-10-09 12:09:02 +00:00
Et ainsi réduire les aller-retour coûteux entre le C et Python.
2023-06-22 09:50:58 +00:00
2023-10-09 12:09:02 +00:00
## Cython annotate
2023-06-22 09:50:58 +00:00
```shell
2023-10-09 12:09:02 +00:00
#!cache cython --annotate examples/phi5cython.py
#!if ! [ -f output/phi5cython.png ] ; then cutycapt --min-width=1024 --delay=500 --url=file://$(pwd)/examples/phi5cython.html --out=output/phi5cython.png; fi
2023-06-22 09:50:58 +00:00
```
2023-10-09 12:09:02 +00:00
![](phi5cython.png)
2023-06-22 09:50:58 +00:00
2023-09-22 13:33:14 +00:00
## Cython — À vous !
2022-06-23 14:10:37 +00:00
# Numba
2023-06-22 09:57:41 +00:00
Numba est un `JIT` : « Just In Time compiler ».
```python
2023-10-09 12:09:02 +00:00
#!
2023-09-22 13:33:14 +00:00
#!cat examples/collatz_length_numba.py
2023-06-22 09:57:41 +00:00
```
## Numba démo
```shell
2023-10-09 12:09:02 +00:00
#!cache pyperf timeit --setup 'from examples.collatz_length import collatz_length' 'collatz_length(837799)'
#!cache pyperf timeit --setup 'from examples.collatz_length_numba import collatz_length' 'collatz_length(837799)'
2023-06-22 09:57:41 +00:00
```
2023-09-22 13:33:14 +00:00
## numba — À vous !
# pypy
pypy est un interpréteur Python écrit en Python.
```shell
#!cache pyperf timeit --setup 'from examples.collatz_length import collatz_length' 'collatz_length(837799)'
#!cache pypy3 -m pyperf timeit --setup 'from examples.collatz_length import collatz_length' 'collatz_length(837799)'
```
2022-06-23 14:10:37 +00:00
# mypyc
mypyc est un compilateur qui s'appuie sur les annotationes de type mypy :
```python
2023-09-22 13:33:14 +00:00
#!cat examples/collatz_length_mypy.py
```
## mypyc demo
```shell
2023-10-09 12:09:02 +00:00
$ pip install mypy
#!cd examples; mypyc collatz_length_mypy.py
$ mypyc collatz_length_mypy.py
```
2023-10-09 12:09:02 +00:00
## mypyc demo
```shell
2023-10-09 12:09:02 +00:00
#!cache pyperf timeit --setup 'from examples.collatz_length import collatz_length' 'collatz_length(837799)'
#!cache pyperf timeit --setup 'from examples.collatz_length_mypy import collatz_length' 'collatz_length(837799)' 2>/dev/null
```
2023-09-22 13:33:14 +00:00
## mypyc — À vous !
2022-06-23 14:10:37 +00:00
# Pythran
pythran est un compilateur pour du code scientifique :
```python
2023-09-22 13:33:14 +00:00
#!cat examples/collatz_length_pythran.py
```
## Pythran demo
```shell
2023-10-09 12:09:02 +00:00
$ pip install pythran
2023-09-22 13:33:14 +00:00
$ pythran examples/collatz_length_pythran.py
2023-10-09 12:09:02 +00:00
#!if ! [ -f examples/collatz_length_pythran.*.so ]; then cd examples; pythran collatz_length_pythran.py; fi
```
2023-10-09 12:09:02 +00:00
## Pythran demo
```shell
2023-10-09 12:09:02 +00:00
#!cache pyperf timeit --setup 'from examples.collatz_length import collatz_length' 'collatz_length(837799)'
#!cache pyperf timeit --setup 'from examples.collatz_length_pythran import collatz_length' 'collatz_length(837799)'
```
2023-10-09 12:09:02 +00:00
2023-09-22 13:33:14 +00:00
## pythran — À vous !
2022-09-23 13:05:20 +00:00
# Nuitka
Aussi un compilateur, aussi utilisable pour distribuer une application.
```shell
2023-10-09 12:09:02 +00:00
$ pip install nuitka
$ python -m nuitka --module collatz_length_nuitka.py
2023-10-09 12:09:02 +00:00
#!if ! [ -f examples/collatz_length_nuitka.*.so ]; then (cd examples/; python -m nuitka --module collatz_length_nuitka.py >/dev/null); fi
```
```shell
2023-10-09 12:09:02 +00:00
#!cache pyperf timeit --setup 'from examples.collatz_length import collatz_length' 'collatz_length(837799)'
#!cache pyperf timeit --setup 'from examples.collatz_length_nuitka import collatz_length' 'collatz_length(837799)'
```
2022-06-23 14:10:37 +00:00
# Hand crafted C
2023-06-23 08:54:12 +00:00
```c
2023-10-09 12:09:02 +00:00
#!sed -n '/int collatz_length/,$p' examples/my_collatz_length.c
2023-06-23 08:54:12 +00:00
```
Mais comment l'utiliser ?
2023-09-22 13:33:14 +00:00
2023-10-09 12:09:02 +00:00
## Avec Cython
2023-06-23 08:54:12 +00:00
```cpython
2023-09-22 13:33:14 +00:00
#!cat examples/collatz_length_cython_to_c.pyx
2023-06-23 08:54:12 +00:00
```
```shell
2023-09-22 13:33:14 +00:00
$ cythonize -i examples/collatz_length_cython_to_c.pyx
#!if ! [ -f examples/collatz_length_cython_to_c.*.so ] ; then cythonize -i examples/collatz_length_cython_to_c.pyx; fi
2023-06-23 08:54:12 +00:00
```
2023-10-09 12:09:02 +00:00
## Avec Cython
2023-06-23 08:54:12 +00:00
```shell
2023-10-09 12:09:02 +00:00
#!cache pyperf timeit --setup 'from examples.collatz_length import collatz_length' 'collatz_length(837799)'
#!cache pyperf timeit --setup 'from examples.collatz_length_cython_to_c import collatz_length' 'collatz_length(837799)'
2023-06-23 08:54:12 +00:00
```
2023-10-09 12:09:02 +00:00
2023-10-09 14:00:49 +00:00
# Hand crafted rust
2023-10-09 12:09:02 +00:00
2023-10-09 14:00:49 +00:00
```rust
#!sed -n '/pyfunction/,$p' examples/collatz_length_rs.rs
```
## with rustimport
```bash
$ pip install rustimport
```
## with rustimport
```bash
#!cache pyperf timeit --setup 'from examples.collatz_length import collatz_length' 'collatz_length(837799)'
#!cache pyperf timeit --setup 'import rustimport.import_hook; import rustimport.settings; rustimport.settings.compile_release_binaries = True; from examples.collatz_length_rs import collatz_length' 'collatz_length(837799)'
2023-10-09 14:00:49 +00:00
```