This repository has been archived by the owner on May 27, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathperfs.md
370 lines (242 loc) · 7.75 KB
/
perfs.md
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# Les Performances en Python
par
Julien Palard <[email protected]>
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 utiisé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
```
## 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.
## Le cas typique
```bash
$ python -m timeit -s 'container = list(range(10_000_000))' \
'10_000_001 in container'
#!bkt -- python -m timeit -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'
#!bkt -- python -m timeit -n 100 -s '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 !
# Les outils
## Les outils en ligne de commande
`time`, un outil POSIX, mais aussi une fonction native de bash :
```bash
$ time python -c 'container = set(range(10_000_000))'
#!bkt -- time -p python -c 'container = set(range(10_000_000))' 2>&1
```
Mais `time` ne teste qu'une fois, ce n'est pas fiable.
::: notes
real 0m0.719s # C'est le temps « sur le mur »
user 0m0.521s # Temps CPU passé « dans Python »
sys 0m0.195s # Temps CPU passé dans le kernel
## Hyperfine
`hyperfine` teste plusieurs fois :
```text
$ hyperfine "python -c 'container = set(range(10_000_000))'"
Benchmark 1: python -c 'container = set(range(10_000_000))'
Time (mean ± σ): 735.6 ms ± 11.2 ms
```
## Petite parenthèse
Mais attention, démarrer un processus Python n'est pas gratuit :
```python
$ hyperfine "python -c pass"
Benchmark 1: python -c pass
Time (mean ± σ): 19.4 ms ± 0.6 ms
```
## Petite parenthèse
Et puis il peut dépendre de la version de Python, des options de compilation, ... :
```text
$ hyperfine "~/.local/bin/python3.10 -c pass" # Avec pydebug
Benchmark 1: ~/.local/bin/python3.10 -c pass
Time (mean ± σ): 37.6 ms ± 0.6 ms
$ hyperfine "/usr/bin/python3.10 -c pass" # Sans pydebug
Benchmark 1: /usr/bin/python3.10 -c pass
Time (mean ± σ): 19.1 ms ± 0.8 ms
```
::: notes
Leur parler de `--enable-optimizations` (PGO).
## timeit
Timeit c'est dans la stdlib de Python, ça s'utilise en ligne de commande ou depuis Python.
## pyperf
C'est l'équivalent d'hyperfine mais exécutant du Python plutôt qu'un programme :
```bash
$ ~/.local/bin/python3.10 -m pyperf timeit pass
.....................
Mean +- std dev: 7.33 ns +- 0.18 ns
$ /usr/bin/python3.10 -m pyperf timeit pass
.....................
Mean +- std dev: 6.10 ns +- 0.11 ns
```
::: 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
prend d'exécuter `pass`.
## cProfile
time, timeit, hyperfine, pyperf c'est bien pour mesurer, comparer.
cProfile nous aider à trouver la fonction coupable dans un script plus gros.
## cProfile, exemple
```python
#! cat approx_phi_up_to_1.py | tail -n 13
```
## cProfile, exemple
Testons :
```python
#! cat approx_phi_test.py
```
```text
$ python approx_phi_test.py
#!bkt -- python approx_phi_test.py
```
C'est déjà lent, et pour `20` c'est interminable...
## cProfile, exemple
Sortons cProfile :
```text
$ python -m cProfile --sort cumulative approx_phi_test.py
#! bkt -- python -m cProfile --sort cumulative approx_phi_test.py | head
```
## cProfile, exemple
```bash
$ python -m pstats fib.prof
prof% stats 10
Mon Jun 13 10:12:29 2022 prof
30903333 function calls (133 primitive calls) in 5.381 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 5.381 5.381 {built-in method builtins.exec}
1 0.000 0.000 5.381 5.381 /tmp/fib.py:1(<module>)
33 0.000 0.000 5.381 0.163 /tmp/fib.py:8(approx_phi)
30903265/65 5.381 0.000 5.381 0.083 /tmp/fib.py:3(fib)
```
## cProfile, exemple
30903265 appels à la fonction `fib` !? On a notre coupable !
## cProfile, exemple
```python
@cache
def fib(n):
if n < 2:
return 1
return fib(n - 1) + fib(n - 2)
def approx_phi(n):
return fib(n + 1) / fib(n)
def approx_phi_up_to(n_digits):
with localcontext() as ctx:
ctx.prec = n_digits + 1
for n in count():
step1 = approx_phi(n)
step2 = approx_phi(n + 1)
if step1 == step2:
return step1
```
notes :::
On est très vite limités par les floats...
## cProfile, exemple
Dépassons la limite des floats avec le module Decimal :
```python
def approx_phi(n):
return Decimal(fib(n + 1)) / Decimal(fib(n))
```
## cProfile, exemple
Et allons plus loin :
```python
def approx_phi_up_to(n_digits):
with localcontext() as ctx:
ctx.prec = n_digits + 1
for n in count():
step1 = approx_phi(n)
step2 = approx_phi(n + 1)
if step1 == step2:
return step1
```
::: notes
Jusqu'à 5000 décimales ça marche bien, mais bon ça devient lent, c'est l'heure de cProfile !
## cProfile, exemple
```text
Ordered by: cumulative time
List reduced from 140 to 10 due to restriction <10>
ncalls tottime percall cumtime percall filename:lineno(function)
3/1 0.000 0.000 15.266 15.266 {built-in method builtins.exec}
1 0.000 0.000 15.266 15.266 /tmp/fib.py:1(<module>)
1 0.012 0.012 15.264 15.264 /tmp/fib.py:16(approx_phi_up_to)
28714 15.244 0.001 15.252 0.001 /tmp/fib.py:13(approx_phi)
14359 0.008 0.000 0.008 0.000 /tmp/fib.py:7(fib)
```
`fib` n'y est pour rien, c'est `approx_phi` qui prend près de 100% du
temps, surtout parce qu'il est appelé près de 30_000 fois !
## cProfile, exemple
Divisons par 10 le nombre d'appels, on réduira mécaniquement par 10 le temps d'exécution :
```python
[...]
step1 = approx_phi(10 * n)
step2 = approx_phi(10 * n + 1)
[...]
```
## cProfile, exemple
En rajoutant du cache sur `approx_phi` et en faisant en sorte de
réutiliser un des deux appels à chaque étape, on peut certainement
encore au moins diviser par deux le temps d'exécution :
```python
step1 = approx_phi(2 ** n)
step2 = approx_phi(2 ** (n+1))
```
`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 » :
```
@cache
def fib(n):
if n in (0, 1):
return 1
x = n // 2
return fib(x - 1) * fib(n - x - 1) + fib(x) * fib(n - x)
```
## cProfile, exemple
```bash
$ time python fib.py
real 0m0.064s
user 0m0.060s
sys 0m0.005s
```
Mieux.
## TODO
snakeviz
scalene
vprof
https://pypi.org/project/pyflame/
...
# Cython
# Numba
# mypyc
# Pythran
# Hand crafted C