# Les performances en Python

par

<!-- .slide: data-background="static/background.jpg" -->

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

Les notations les plus utilisées :

```text
O(1)        Constant
O(log n)    Logarithmique
O(n)        Linéaire
O(n log n)  Parfois appelée « linéarithmique »
O(n²)       Quadratique
O(nᶜ)       Polynomiale
O(cⁿ)       Exponentielle
O(n!)       Factorielle
```

notes:

Il faut les grapher pour s'en rendre compte : cf. examples/big.o.py


## 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
```

notes:

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.

notes:

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
#!sed -n '/def index/,/raise ValueError/p' examples/find_in_list.py
```

## O(n)

```python
#!sed -n '/def dumb_index/,/raise ValueError/p' examples/find_in_list.py
```

## O(n log n)

C'est `n` fois un `log n`, par exemple rayer `n` personnes dans un annuaire.

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


## 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.


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

```shell
#!cache python -m pyperf timeit --setup 'container = list(range(10_000_000))' '10_000_001 in container'

#!cache python -m pyperf timeit --setup 'container = set(range(10_000_000))' '10_000_001 in container'
```

Pourquoi une si grande différence !?


notes:

C'est l'heure du live coding !


# À vous !

Simulez un tas de sable, moi je calcule le nombre l'or.

Ne vous souciez pas des perfs, on s'en occupera.

notes:

Leur laisser ~15mn.

voir sandpile.py


# Les outils

notes:

Je mesure mes perfs, puis ils mesurent leurs perfs.


## `pyperf command`

```shell
#!cache pyperf command python examples/phi1.py 3

#!cache pyperf command python examples/phi1.py 6

#!cache pyperf command python examples/phi1.py 9
```

## Petite parenthèse

Mais attention, démarrer un processus Python n'est pas gratuit :

```shell
#!cache pyperf command python -c pass
```

notes:

N'essayez pas de retenir les chiffres, retenez les faits.


## Petite parenthèse

Et puis il peut dépendre de la version de Python, des options de compilation, ... :

```shell
$ pyperf command ~/.local/bin/python3.10 -c pass
.....................
command: Mean +- std dev: 37.6 ms +- 0.6 ms

$ pyperf command /usr/bin/python3.10 -c pass
.....................
command: Mean +- std dev: 14.4 ms +- 0.4 ms
```

notes:

Leur parler de `--enable-optimizations` (PGO).


## `pyperf timeit`

Il existe aussi `timeit` dans la stdlib, mais je préfère `pyperf timeit` :

```shell
#!cache pyperf timeit --setup 'from examples.phi1  import approx_phi_up_to' 'approx_phi_up_to(3)'
```


## Les outils — À vous !

Effectuez quelques mesures sur votre implémentation.

Tentez d'en déterminer la complexité en fonction du nombre de grains.

Explorez les limites de vos implémentations.


# Profilage

`pyperf` c'est bien pour mesurer, comparer.

Le profilage peut nous aider à trouver la fonction coupable.


## cProfile, exemple

```python
#!sed -n '/def fib/,/return approx/p' examples/phi1.py
```


## cProfile, exemple

Sortons cProfile :

```shell
$ python -m cProfile --sort cumulative phi1.py 10
...
#!cache python -m cProfile --sort cumulative examples/phi1.py 10 | sed -n '/fib\|function calls/{s/ \+/ /g;s/^ *//;p}'
...
```

C'est donc `fib` la coupable :
- C'est ~100% du temps (`cumtime`).
- C'est ~100% des appels de fonctions.


## cProfile, exemple

Cachons les résultats de `fib` :
```python
#!sed -n '/import cache/,/return fib/p' examples/phi2.py
```

## cProfile, exemple

Et on repasse dans cProfile !

```shell
$ python -m cProfile --sort cumulative phi2.py 10
...
#!cache python -m cProfile --sort cumulative examples/phi2.py 10 | sed -n '/fib\|function calls/{s/ \+/ /g;s/^ *//;p}'
...
```

C'est mieux !

## cProfile, exemple

On essaye d'aller plus loin ?

```shell
#!cache python -m cProfile --sort cumulative examples/phi2.py 2000 | head -n 3 | sed 's/^ *//g;s/seconds/s/g'
```

Ça tient, mais peut-on faire mieux ?


## cProfile, exemple

Divisons par 10 le nombre d'appels, on réduira mécaniquement par 10 le
temps d'exécution ?

```python
#!sed -n '/def approx_phi_up_to/,/return step1/p' examples/phi3.py
```

## cProfile, exemple

```shell
#!cache python -m cProfile --sort cumulative examples/phi3.py 2000 | head -n 3 | sed 's/^ *//g;s/seconds/s/g'
```


## cProfile, exemple

En cachant `approx_phi` ?

```python
#!sed -n '10,/return step1/p' examples/phi4.py
```

notes:

Notez l'astuce pour que le `step2` d'un
tour soit le `step1` du suivant...


## cProfile, exemple

```shell
$ python -m cProfile --sort cumulative examples/phi4.py 2000
```

`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...


## cProfile, exemple

Il est temps de sortir une implémentation de `fib` plus robuste, basée
sur l'algorithme « matrix exponentiation » :

```python
#!sed -n '/def fib/,/return fib/p' examples/phi5.py
```


## cProfile, exemple

```text
#!cache python -m cProfile --sort cumulative examples/phi5.py 2000 | head -n 3 | sed 's/^ *//g;s/seconds/s/g'
```

notes:

Mieux.


## Snakeviz

```shell
$ python -m pip install snakeviz
$ python -m cProfile -o phi5.prof phi5.py 2000
$ python -m snakeviz phi5.prof
```

#!if [ ! -f .cache/phi5.prof ]; then python -m cProfile -o .cache/phi5.prof examples/phi5.py 2000 >/dev/null 2>&1; 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=file://$(pwd)/.cache/phi5.prof --out=output/phi5-snakeviz.png ; kill $TOKILL; fi


## Snakeviz

![](phi5-snakeviz.png)


## Scalene

```shell
$ python -m pip install scalene
$ scalene phi5.py 100000
```

#!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


## Scalene

![](phi5-scalene.png)


## 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
```


## Aussi

- 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
- https://github.com/pythonprofilers/memory_profiler


## Profilage — À vous !

Profilez votre implémentation et tentez quelques améliorations.


# Cython

Cython est un dialecte de Python transpilable en C.


## Cython démo

Sans modifier le code :

```python
$ pip install cython
$ cythonize --inplace examples/phi5cython.py
#!cythonize --inplace examples/phi5cython.py
```

## Cython démo

```
#!cache pyperf timeit --setup 'from examples.phi5 import approx_phi_up_to' 'approx_phi_up_to(100_000)'

#!cache pyperf timeit --setup 'from examples.phi5cython import approx_phi_up_to' 'approx_phi_up_to(100_000)'
```


## Cython démo

En annotant le fichier on permet à cython d'utiliser des types natifs.

Et ainsi réduire les aller-retour coûteux entre le C et Python.


## Cython annotate

```shell
#!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
```

![](phi5cython.png)


## Cython — À vous !


# Numba

Numba est un `JIT` : « Just In Time compiler ».

```python
#!
#!cat examples/collatz_length_numba.py
```

## Numba démo

```shell
#!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)'
```

## 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)'
```

# mypyc

mypyc est un compilateur qui s'appuie sur les annotationes de type mypy :

```python
#!cat examples/collatz_length_mypy.py
```

## mypyc demo

```shell
$ pip install mypy
#!cd examples; mypyc collatz_length_mypy.py
$ mypyc collatz_length_mypy.py
```


## mypyc demo

```shell
#!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
```

## mypyc — À vous !


# Pythran

pythran est un compilateur pour du code scientifique :

```python
#!cat examples/collatz_length_pythran.py
```

## Pythran demo

```shell
$ pip install pythran
$ pythran examples/collatz_length_pythran.py
#!if ! [ -f examples/collatz_length_pythran.*.so ]; then cd examples; pythran collatz_length_pythran.py; fi
```


## Pythran demo

```shell
#!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)'
```


## pythran — À vous !


# Nuitka

Aussi un compilateur, aussi utilisable pour distribuer une application.

```shell
$ pip install nuitka
$ python -m nuitka --module collatz_length_nuitka.py
#!if ! [ -f examples/collatz_length_nuitka.*.so ]; then (cd examples/; python -m nuitka --module collatz_length_nuitka.py >/dev/null); fi
```

```shell
#!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)'
```


# Hand crafted C

```c
#!sed -n '/int collatz_length/,$p' examples/my_collatz_length.c
```

Mais comment l'utiliser ?


## Avec Cython

```cpython
#!cat examples/collatz_length_cython_to_c.pyx
```

```shell
$ 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
```

## Avec Cython

```shell
#!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)'
```


# Hand crafted rust

```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)'
```
