5. ‹#›
Проблемы приложения.
Какие проблемы вообще бывают?
• неудачные архитектурные решения
• неудачно выбранные компоненты и фреймворки
• медленный I/O
• высокий расход памяти, утечки памяти
• медленный код
6. ‹#›
Проблемы приложения.
Как решается большинство проблем?
• добавление воркеров
• кеширование
• отложенные задания, очереди
• замена компонентов
• map/reduce
• изменение архитектуры
• …
7. ‹#›
Когда это критично и не решаемо «привычными» способами?
Обработка потоковых данных
пример: процессинг датчиков (акселерометры, гироскопы)
Десериализация
пример: JSON, pickle, ..
Авторегрессия
пример: EMA (скользящая средняя), численное интегрирование,
ряды
Стейт-машины
пример: AI, синтаксические анализаторы текста
Медленный код.
8. ‹#›
Профилирование специальными утилитами
• ручной профайлинг (тайминг)
• статистический профайлинг (сэмплинг)
• событийный профайлинг (граф вызовов)
Логгирование и сбор статистики
• настройка конфигов apache/nginx/…
• логи приложения
Как найти критические участки кода?
10. ‹#›
Выбор огромен
• line_profiler
• hotshot
• gprof2dot
• memory_profiler
• objgraph
• memprof
• для django есть миддлвары с картинками и
графиками
• django debug toolbar
• django live profiler
• …
Profiling.
11. ‹#›
Задача: профилирование живого WEB-сервера
• мы не хотим чтобы профилировщик значительно снижал
производительность
• мы хотим получить более-менее репрезентативные данные
Решение:
1. поднять апстрим на ~1% и собирать статистику с него (*)
2. воспроизвести на стейджинге/тестовом окружении
Альтернатива:
• настраиваем access logs
• смотрим, где медленно
• разбираемся почему
Итого.
12. ‹#›
• проводим серию испытаний
• замеряем среднее время
• исключаем I/O, профилировщик и тп
• помним про погрешность
• разогреваем JIT (* PyPy ~ 0.2c — см. доки)
• как-то используем результаты теста, иначе JIT может
его «вырезать»
• целевой пробег сопоставим по производительности с
разогревочным
• целевой пробег на JIT должен работать быстрее
Как правильно писать тесты на производительность?
13. ‹#›
• Регрессионные тесты
• Не нужно делать гипотез и предположений: только
цифры
• Проблему с I/O исключили
• Первое что стоит оптимизировать — алгоритм
• Проблема скорее всего в каком-то из циклов
• Все статические переменные должны быть вынесены
из цикла
• eval, exec — плохо
• Не увлекаться!
О чем всегда помнить
14. ‹#›
CPython — интерпретатор.
Он честно интерпретирует каждую строку кода.
• Lookup-ы — очень дороги
• атрибуты и методы
• локальные/глобальные переменные
• замыкание
• Запоминание переменных дорого
• Создание объектов — дорого
• Изменение размеров объектов в памяти — дорого
• eval, exec — плохо
Особенности присущие CPython
15. ‹#›
PyPy использует JIT.
PyPy пытается исполнить то, что вы имели в виду
Исполняется совсем не тот код, который вы пишите.
• JIT scope != trace: locals(), globals(), sys._getframe(),
sys.exc_info(), sys.settrace, …
• На JIT компиляцию требуется время (>0.2s)
• => то, что «гоняется редко» — оптимизировано не
будет
• C-модули поддерживаются плохо: используем Python-
версию
• eval, exec — плохо
Особенности присущие PyPy
17. ‹#›
FizzBuzz
Для данного списка натуральный чисел (int) вернуть
строку со значениями через запятую, где
• числа, делящиеся на 3 заменены на "Fizz";
• числа, делящиеся на 5 заменены на "Buzz";
• числа, делящиеся одновременно и на 3, и на 5
заменены на "FizzBuzz";
• остальные числа выведены как есть.
Например:
[1, 2, 5, 15, 3, 1, 1, 4] => "1,2,Buzz,FizzBuzz,Fizz,1,1,4"
http://rosettacode.org/wiki/FizzBuzz
18. ‹#›
FizzBuzz. Самое простое решение (Гуглим).
for i in xrange(1, 101):
if i % 15 == 0:
print "FizzBuzz"
elif i % 3 == 0:
print "Fizz"
elif i % 5 == 0:
print "Buzz"
else:
print i
19. ‹#›
FizzBuzz. Самое простое решение.
def fizzbuzz_simple(arr):
output_array = []
for i in arr:
if i % 15 == 0:
output_array.append("FizzBuzz")
elif i % 3 == 0:
output_array.append("Fizz")
elif i % 5 == 0:
output_array.append("Buzz")
else:
output_array.append(str(i))
return ",".join(output_array)
21. ‹#›
FizzBuzz: Тайминг
import gc
import hashlib
import time
from random import shuffle
def _timetest(fn, n):
gc.disable()
gc.collect()
setup = [range(1, 101) for _ in xrange(n)]
map(shuffle, setup)
ts = time.clock()
output = map(fn, setup)
tt = time.clock() - ts
print '.. took {:.5f}s, for {} runs, avg={}ms hash={}'.format(
tt, n, tt * 1000 / n, hashlib.md5(''.join(output)).hexdigest())
gc.enable()
def check_time_taken(fn, n_warming=10000, n_executing=1000):
print 'checking function {fn.__name__} for speed'.format(**locals())
print 'warming up',
_timetest(fn, n_warming)
print 'executing',
_timetest(fn, n_executing)
22. ‹#›
Инструменты
• Юнит-тесты или иной способ проверки правильности алгоритма
check_correct_100(fizzbuzz_simple)
• Замеры времени
check_time_taken(fizzbuzz_simple)
• Модуль dis
from dis import dis
dis(fizzbuzz_simple)
• Модуль Profile
from profile import run
run('fizzbuzz_simple(range(100000))')
• Утилита Pycallgraph
from pycallgraph import PyCallGraph
from pycallgraph.output import GraphvizOutput
with PyCallGraph(output=GraphvizOutput()):
fizzbuzz_simple(range(100000))
50. ‹#›
Оптимизация алгоритма
Для данного списка натуральный чисел (int) вернуть
строку со значениями через запятую, где
• числа, делящиеся на 3 заменены на "Fizz";
• числа, делящиеся на 5 заменены на "Buzz";
• числа, делящиеся одновременно и на 3, и на 5
заменены на "FizzBuzz";
• остальные числа выведены как есть.
Например:
[1, 2, 5, 15, 3, 1, 1, 4] => "1,2,Buzz,FizzBuzz,Fizz,1,1,4"
http://rosettacode.org/wiki/FizzBuzz
51. ‹#›
Оптимизация алгоритма
def fizzbuzz_simple(arr):
output_array = []
for i in arr:
if i % 15 == 0:
output_array.append("FizzBuzz")
elif i % 3 == 0:
output_array.append("Fizz")
elif i % 5 == 0:
output_array.append("Buzz")
else:
output_array.append(str(i))
return ",".join(output_array)
15?
52. ‹#›
Оптимизация алгоритма
def fizzbuzz_simple(arr):
output_array = []
for i in arr:
if i % 3 == 0 and i % 5 == 0:
output_array.append("FizzBuzz")
elif i % 3 == 0:
output_array.append("Fizz")
elif i % 5 == 0:
output_array.append("Buzz")
else:
output_array.append(str(i))
return ",".join(output_array)
53. ‹#›
Оптимизация алгоритма
def fizzbuzz_simple(arr):
output_array = []
for i in arr:
if i % 3 == 0 and i % 5 == 0:
output_array.append("FizzBuzz")
elif i % 3 == 0:
output_array.append("Fizz")
elif i % 5 == 0:
output_array.append("Buzz")
else:
output_array.append(str(i))
return ",".join(output_array)
54. ‹#›
Оптимизация алгоритма
def fizzbuzz_simple(arr):
output_array = []
for i in arr:
if i % 3 == 0:
if i % 5 == 0:
output_array.append("FizzBuzz")
else:
output_array.append("Fizz")
elif i % 5 == 0:
output_array.append("Buzz")
else:
output_array.append(str(i))
return ",".join(output_array)
55. ‹#›
Оптимизация алгоритма
Количество сравнений для списка значений 1 .. 15
До … 39
После … 30
По времени ~ 3% разницы
По количеству операций ~ 30%
А что если переставить порядок сравнений?
56. ‹#›
Оптимизация алгоритма. Перестановка операций
def fizzbuzz_simple(arr):
output_array = []
for i in arr:
if i % 15 == 0:
output_array.append("FizzBuzz")
elif i % 5 == 0:
output_array.append("Buzz")
elif i % 3 == 0:
output_array.append("Fizz")
else:
output_array.append(str(i))
return ",".join(output_array)
57. ‹#›
Оптимизация алгоритма. Перестановка операций
def fizzbuzz_simple(arr):
output_array = []
for i in arr:
if i % 5 == 0:
if i % 3 == 0:
output_array.append("FizzBuzz")
else:
output_array.append("Buzz")
elif i % 3 == 0:
output_array.append("Fizz")
else:
output_array.append(str(i))
return ",".join(output_array)
58. ‹#›
Оптимизация алгоритма. Перестановка операций
Количество сравнений для списка значений 1 .. 15
Плохой вариант
До … 39
После … 41 (хуже)
Улучшенный вариант
До … 30
После … 30 (не изменилось)
От лучшего до худшего ~ 30%
61. ‹#›
Оптимизируем CPython. Lookup
def fizzbuzz_simple(arr):
output_array = []
for i in arr:
if i % 5 == 0:
if i % 3 == 0:
output_array.append("FizzBuzz")
else:
output_array.append("Buzz")
elif i % 3 == 0:
output_array.append("Fizz")
else:
output_array.append(str(i))
return ",".join(output_array)
62. ‹#›
Оптимизируем CPython. Lookup
def fizzbuzz_simple(arr):
output_array = []
_append = output_array.append
for i in arr:
if i % 5 == 0:
if i % 3 == 0:
_append(«FizzBuzz")
else:
_append(«Buzz")
elif i % 3 == 0:
_append(«Fizz")
else:
_append(str(i))
return ",".join(output_array)
63. ‹#›
Оптимизируем CPython. Lookup
def fizzbuzz_simple(arr):
output_array = []
_append = output_array.append
for i in arr:
if i % 5 == 0:
if i % 3 == 0:
_append(«FizzBuzz")
else:
_append(«Buzz")
elif i % 3 == 0:
_append(«Fizz")
else:
_append(str(i))
return ",".join(output_array) 1.3x
76. ‹#›
def co():
. . .
x = yield y
[return None]
c = co()
out = c.send(Z)
Coroutines
Как это работает
• def + yield = ключевые слова
• создаем «конструктор» генератора
• вызов c = co() создает генератор c
• c.next()
• выполнит все до первого yield,
• вернет результат выражения y,
• «встанет на паузу»
• c.send(Z)
• x = Z
• продолжит выполнение до yield/return
• out = y
• return завершает выполнение (StopIteration)
78. ‹#›
Coroutines
… и поместить все внутрь (до первого yield)
@coroutine
def fizzbuzz_co():
def fizzbuzz_samples_helper(arr):
for i in arr:
if i % 3 == 0:
if i % 5 == 0:
yield "FizzBuzz"
else:
yield "Fizz"
elif i % 5 == 0:
yield "Buzz"
else:
yield False
__join = ",".join
__str = str
samples = tuple(fizzbuzz_samples_helper(xrange(15)))
arr = ()
while True:
arr = yield __join(samples[i % 15] or __str(i) for i in arr)
80. ‹#›
Быстрый FizzBuzz, кэширующая функция
Кэширующая функция
• вычисления ресурсоемки
• значения аргументов часто повторяются
def cached(fn):
cache = {}
@wraps(fn)
def decorated(arg):
value = cache.get(arg)
if not value:
cache[arg] = value = fn(arg)
return value
return decorated
87. ‹#›
Coroutine based class
coroutine class coroutine vs
class
send
main method
4,23 6,93 1,63x faster
throw MakeSum
make_sum
21,85 7,30 3x slower
88. ‹#›
Coroutine based class
Плюсы
• Основной метод работает быстрее
• «Наследование»
Минусы
• Интерфейс «заморожен»
• Основной метод «заморожен»
• Код «специфичен»
90. ‹#›
Cython, numpy, weave, etc..
«Числодробилки»
Travis Oliphant
from numpy import zeros
from scipy import weave
dx = 0.1
dy = 0.1
dx2 = dx*dx
dy2 = dy*dy
def py_update(u):
nx, ny = u.shape
for i in xrange(1,nx-1):
for j in xrange(1, ny-1):
u[i,j] = ((u[i+1, j] + u[i-1, j]) * dy2 +
(u[i, j+1] + u[i, j-1]) * dx2) / (2*(dx2+dy2))
def calc(N, Niter=100, func=py_update, args=()):
u = zeros([N, N])
u[0] = 1
for i in range(Niter):
func(u,*args)
return u
91. ‹#›
Почти тот же Python!
cimport numpy as np
def cy_update(np.ndarray[double, ndim=2] u, double dx2, double dy2):
cdef unsigned int i, j
for i in xrange(1,u.shape[0]-1):
for j in xrange(1, u.shape[1]-1):
u[i,j] = ((u[i+1, j] + u[i-1, j]) * dy2 +
(u[i, j+1] + u[i, j-1]) * dx2) / (2*(dx2+dy2))
Cython, numpy, weave, etc..
92. ‹#›
Cython, numpy, weave, etc..
Почти «чистый С»
def weave_update(u):
code = """
int i, j;
for (i=1; i<Nu[0]-1; i++) {
for (j=1; j<Nu[1]-1; j++) {
U2(i,j) = ((U2(i+1, j) + U2(i-1, j))*dy2 +
(U2(i, j+1) + U2(i, j-1))*dx2) / (2*(dx2+dy2));
}
}
"""
weave.inline(code, ['u', 'dx2', 'dy2'])
95. ‹#›
Рецепт
• найти слабое место
• убедиться что все упирается в производительность кода, а не в
дисковое/сетевое IO
• упростить ООП до простых функций и процедур
• оптимизировать алгоритм
• избавиться от лишних переменных
• избавиться от конструкций object.method()
• использовать итераторы/генераторы вместо списков
• завернуть все в сопроцессы
• постоянно замерять производительность на данных, схожих с
реальными
• тестировать
• знать когда остановиться
96. ‹#›
• Ссылки, литература:
• Дэвид Бизли: генераторы/сопроцессы http://www.dabeaz.com/generators/
• Python и память http://www.slideshare.net/PiotrPrzymus/pprzymus-europython-2014
• Другой пример о профилировали — числа фибоначчи http://pymotw.com/2/profile/
• Про объекты, ссылки и утечки памяти http://mg.pov.lt/objgraph/
• line_profiler, memory_profiler http://www.huyng.com/posts/python-
performance-analysis/
• numpy, cython, weave http://technicaldiscovery.blogspot.ru/2011/06/speeding-up-
python-numpy-cython-and.html
• google
• Контакты:
• email: iremizov@parallels.com #CodeFest
• twitter: @iremizov