FizzBuzz: OOP.

реклама
Оптимизация
производительности Python
Ремизов Иван
Cloud Architect
Картинка для привлечения внимания
30x
* CPython, без привлечения внешних зависимостей, компиляции и тп
‹#›
Python
Рассматриваем язык:
Python 2.x
Конкретные реализации:
CPython (*nix default)
PyPy (JIT)
‹#›
Проблемы приложения.
Какие проблемы вообще бывают?
• неудачные архитектурные решения
• неудачно выбранные компоненты и фреймворки
• медленный I/O
• высокий расход памяти, утечки памяти
• медленный код
‹#›
Проблемы приложения.
Как решается большинство проблем?
• добавление воркеров
• кеширование
• отложенные задания, очереди
• замена компонентов
• map/reduce
• изменение архитектуры
•…
‹#›
Медленный код.
Когда это критично и не решаемо «привычными» способами?
Обработка потоковых данных
пример: процессинг датчиков (акселерометры, гироскопы)
Десериализация
пример: JSON, pickle, ..
Авторегрессия
пример: EMA (скользящая средняя), численное интегрирование, ряды
Стейт-машины
пример: AI, синтаксические анализаторы текста
‹#›
Как найти критические участки кода?
Профилирование специальными утилитами
• ручной профайлинг (тайминг)
• статистический профайлинг (сэмплинг)
• событийный профайлинг (граф вызовов)
Логгирование и сбор статистики
• настройка конфигов apache/nginx/…
• логи приложения
‹#›
Profiling.
Утилиты
• profile/cprofile
• pycallgraph
• dis (иногда бывает полезно)
Выбор огромен
• line_profiler
• hotshot
• gprof2dot
• memory_profiler
• objgraph
• memprof
• для django есть миддлвары с картинками и графиками
• django debug toolbar
• django live profiler
• …
‹#›
Итого.
Задача: профилирование живого WEB-сервера
• мы не хотим чтобы профилировщик значительно снижал производительность
• мы хотим получить более-менее репрезентативные данные
Решение:
1.поднять апстрим на ~1% и собирать статистику с него (*)
2.воспроизвести на стейджинге/тестовом окружении
Альтернатива:
• настраиваем access logs
• смотрим, где медленно
• разбираемся почему
‹#›
Как правильно писать тесты на производительность?
• проводить серию испытаний и замерять среднее время
• по возможности необходимо снизить влияние наведенных эффектов:
• сборщик мусора (если мы не хотим его учитывать),
• I/O блокировки,
• профилировщик и тп
• сравнивая различные варианты кода нужно учитывать, что разница
должна быть больше погрешности измерений
• имея дело с JIT, всегда сначала проводить «разогревочные итерации» (*
PyPy на JIT компилляцию нужно не менее 0.2c — см. доки)
• тест не должен молотить впустую, иначе JIT может его "вырезать"
• разогревочный пробег и целевой
• не должны значительно различаться
• код, оптимизированный JIT-ом, должен работать быстрее, если нет,
надо разбираться что не так
‹#›
Что всегда надо держать в голове
• Регрессионные тесты должны быть
• Не нужно делать гипотез и предположений о ботлнэке. Замерять и
профайлить!
• Проблема в I/O или нет?
• Первое что стоит оптимизировать — алгоритм
• снижать сложность, упрощать логику
• уменьшать количество ветвлений
• увеличивать избератильность
• уменьшать размеры циклов
• Проблема скорее всего в каком-то из циклов
• Все что не меняется, не нужно пересчитывать много раз
• регулярки, конфигурации
• eval, exec — плохо
• Не увлекаться!
‹#›
Особенности присущие CPython
CPython — интерпретатор.
Он честно интерпретирует каждую строку кода.
• Lookup-ы — очень дороги
• локальные/глобальные переменные
• замыкание
• атрибуты и методы
• Запоминание переменных дорого
• Создание объектов — дорого
• Изменение размеров объектов в памяти — дорого
‹#›
Особенности присущие PyPy
PyPy использует JIT.
PyPy пытается исполнить то, что вы имели в виду
Исполняется совсем не тот код, который вы пишите.
• JIT scope != trace: locals(), globals(), sys._getframe(),
sys.exc_info(), sys.settrace, …— сильно замедляют PyPy
• На JIT компиляцию требуется время (>0.2s)
• то, что «гоняется редко» — оптимизировано не будет
• eval, exec — сильно замедляют PyPy
• Модули написанные на C не оптимизируются и
рекомендуется использовать их Python-версию.
‹#›
ПРИМЕРЫ
‹#›
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
‹#›
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
‹#›
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)
‹#›
FizzBuzz: Тесты
CORRECT_100 = (
"1,2,Fizz,4,Buzz,Fizz,7,8,Fizz,Buzz,11,Fizz,13,14,FizzBuzz,"
"16,17,Fizz,19,Buzz,Fizz,22,23,Fizz,Buzz,26,Fizz,28,29,FizzBuzz,"
"31,32,Fizz,34,Buzz,Fizz,37,38,Fizz,Buzz,41,Fizz,43,44,FizzBuzz,"
"46,47,Fizz,49,Buzz,Fizz,52,53,Fizz,Buzz,56,Fizz,58,59,FizzBuzz,"
"61,62,Fizz,64,Buzz,Fizz,67,68,Fizz,Buzz,71,Fizz,73,74,FizzBuzz,"
"76,77,Fizz,79,Buzz,Fizz,82,83,Fizz,Buzz,86,Fizz,88,89,FizzBuzz,"
"91,92,Fizz,94,Buzz,Fizz,97,98,Fizz,Buzz"
)
def check_correct_100(fn):
print 'checking function {fn.__name__}'.format(**locals()),
output = fn(range(1, 101))
if output == CORRECT_100:
print '.. ok'
else:
print ‘.. failed'
‹#›
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)
‹#›
Инструменты
• Юнит-тесты или иной способ проверки правильности алгоритма
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))
‹#›
Как выглядит вывод dis
4
5
0 BUILD_LIST
3 STORE_FAST
6 SETUP_LOOP
9 LOAD_FAST
12 GET_ITER
>> 13 FOR_ITER
16 STORE_FAST
0
1 (output_array)
121 (to 137)
2 (i)
6
19 LOAD_FAST
2 (i)
22 LOAD_CONST
1 (15)
25 BINARY_MODULO
26 LOAD_CONST
2 (0)
29 COMPARE_OP
2 (==)
32 POP_JUMP_IF_FALSE
51
7
35 LOAD_FAST
38 LOAD_ATTR
41 LOAD_CONST
44 CALL_FUNCTION
47 POP_TOP
48 JUMP_ABSOLUTE
8
9
10
99 LOAD_FAST
102 LOAD_ATTR
105 LOAD_CONST
108 CALL_FUNCTION
111 POP_TOP
112 JUMP_ABSOLUTE
13
>> 115 LOAD_FAST
118 LOAD_ATTR
121 LOAD_GLOBAL
124 LOAD_FAST
127 CALL_FUNCTION
130 CALL_FUNCTION
133 POP_TOP
134 JUMP_ABSOLUTE
>> 137 POP_BLOCK
14
>> 138 LOAD_CONST
141 LOAD_ATTR
144 LOAD_FAST
147 CALL_FUNCTION
150 RETURN_VALUE
0 BUILD_LIST
3 STORE_FAST
6 SETUP_LOOP
9 LOAD_FAST
12 GET_ITER
>> 13 FOR_ITER
16 STORE_FAST
0
1 (output_array)
129 (to 138)
0 (arr)
121 (to 137)
2 (i)
13
6
19 LOAD_FAST
2 (i)
22 LOAD_CONST
1 (15)
25 BINARY_MODULO
26 LOAD_CONST
2 (0)
29 COMPARE_OP
2 (==)
32 POP_JUMP_IF_FALSE
51
7
35 LOAD_FAST
38 LOAD_ATTR
41 LOAD_CONST
44 CALL_FUNCTION
47 POP_TOP
48 JUMP_ABSOLUTE
1 (output_array)
0 (append)
5 ('Fizz')
1
13
>> 83 LOAD_FAST
2 (i)
86 LOAD_CONST
6 (5)
89 BINARY_MODULO
90 LOAD_CONST
2 (0)
93 COMPARE_OP
2 (==)
96 POP_JUMP_IF_FALSE
115
11
5
1 (output_array)
0 (append)
3 ('FizzBuzz')
1
>> 51 LOAD_FAST
2 (i)
54 LOAD_CONST
4 (3)
57 BINARY_MODULO
58 LOAD_CONST
2 (0)
61 COMPARE_OP
2 (==)
64 POP_JUMP_IF_FALSE
83
67 LOAD_FAST
70 LOAD_ATTR
73 LOAD_CONST
76 CALL_FUNCTION
79 POP_TOP
80 JUMP_ABSOLUTE
4
129 (to 138)
0 (arr)
1 (output_array)
0 (append)
7 ('Buzz')
1
13
1 (output_array)
0 (append)
1 (str)
2 (i)
1
1
...
13
8 (',')
2 (join)
1 (output_array)
1
‹#›
1 (output_array)
0 (append)
3 ('FizzBuzz')
1
13
Как выглядит вывод профайлера
100006 function calls in 0.699 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
100000 0.302 0.000 0.302 0.000 :0(append)
1 0.003 0.003 0.003 0.003 :0(join)
1 0.003 0.003 0.003 0.003 :0(range)
1 0.002 0.002 0.002 0.002 :0(setprofile)
1 0.002 0.002 0.697 0.697 <string>:1(<module>)
1 0.388 0.388 0.692 0.692 example_1_profile.py:3(fizzbuzz_simple)
1 0.000 0.000 0.699 0.699 profile:0(fizzbuzz_simple(range(100000)))
0 0.000
0.000
profile:0(profiler)
‹#›
Как выглядит вывод профайлера
100006 function calls in 0.699 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
100000 0.302 0.000 0.302 0.000 :0(append)
1 0.003 0.003 0.003 0.003 :0(join)
1 0.003 0.003 0.003 0.003 :0(range)
1 0.002 0.002 0.002 0.002 :0(setprofile)
1 0.002 0.002 0.697 0.697 <string>:1(<module>)
1 0.388 0.388 0.692 0.692 example_1_profile.py:3(fizzbuzz_simple)
1 0.000 0.000 0.699 0.699 profile:0(fizzbuzz_simple(range(100000)))
0 0.000
0.000
profile:0(profiler)
‹#›
Проблемный
участок
Как выглядит вывод профайлера
100006 function calls in 0.699 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
100000 0.302 0.000 0.302 0.000 :0(append)
1 0.003 0.003 0.003 0.003 :0(join)
1 0.003 0.003 0.003 0.003 :0(range)
1 0.002 0.002 0.002 0.002 :0(setprofile)
1 0.002 0.002 0.697 0.697 <string>:1(<module>)
1 0.388 0.388 0.692 0.692 example_1_profile.py:3(fizzbuzz_simple)
1 0.000 0.000 0.699 0.699 profile:0(fizzbuzz_simple(range(100000)))
0 0.000
0.000
profile:0(profiler)
‹#›
Артефакт
Как выглядит вывод PyCallGraph
‹#›
FizzBuzz. eval.
def fizzbuzz_simple(arr):
output_array = []
for i in arr:
if i % 15 == 0:
eval(
'output_array.append("FizzBuzz")',
globals(), locals())
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)
‹#›
FizzBuzz. exec.
def fizzbuzz_simple(arr):
output_array = []
for i in arr:
if i % 15 == 0:
(exec
‘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)
‹#›
FizzBuzz: OOP.
Array of items
processed as string
ArrayProcessor
ItemProcessor
Replacer
Replacer
Replacer
‹#›
FizzBuzz: OOP.
class AbstractReplacer(object):
__metaclass__ = ABCMeta
__slots__ = 'value', 'output'
return_value = NotImplemented
def __init__(self, value):
pass
@abstractmethod
def validate_input(self):
raise NotImplementedError
@abstractmethod
def check_match(self):
raise NotImplementedError
@abstractmethod
def process(self):
raise NotImplementedError
@abstractmethod
def get_output_value(self):
raise NotImplementedError
‹#›
FizzBuzz: OOP.
class AbstractItemProcessor(object):
__metaclass__ = ABCMeta
__slots__ = 'value', 'output'
replacer_classes = NotImplemented
def __init__(self, value):
pass
@abstractmethod
def validate_input(self):
raise NotImplementedError
@abstractmethod
def validate_processed_value(self):
raise NotImplementedError
@abstractmethod
def process(self):
raise NotImplementedError
@abstractmethod
def get_replacer_classes(self):
raise NotImplementedError
@abstractmethod
def get_output_value(self):
raise NotImplementedError
‹#›
FizzBuzz: OOP.
class AbstractArrayProcessor(object):
__metaclass__ = ABCMeta
__slots__ = 'array', 'output'
item_processer_class = NotImplemented
def __init__(self, array):
pass
@abstractmethod
def validate_input(self):
raise NotImplementedError
@abstractmethod
def process(self):
raise NotImplementedError
@abstractmethod
def get_item_processer_class(self):
raise NotImplementedError
@abstractmethod
def get_output_value(self):
raise NotImplementedError
‹#›
FizzBuzz: OOP.
class ImproperInputValue(Exception):
pass
class ImproperOutputValue(Exception):
pass
‹#›
FizzBuzz: OOP.
class BaseReplacer(AbstractReplacer):
return_value = None
divider = 1
def __init__(self, value):
super(BaseReplacer, self).__init__(value)
self.value = value
self.validate_input()
self.output = None
def validate_input(self):
if not isinstance(self.value, int):
raise ImproperInputValue(self.value)
def check_match(self):
return self.value % self.divider == 0
def process(self):
if self.check_match():
self.output = self.return_value
def get_output_value(self):
return self.output
‹#›
FizzBuzz: OOP.
class BaseItemProcessor(AbstractItemProcessor):
replacer_classes = BaseReplacer,
def __init__(self, value):
super(BaseItemProcesser, self).__init__(value)
self.value = value
self.validate_input()
self.output = None
def validate_input(self):
if not isinstance(self.value, int):
raise ImproperInputValue(self.value)
def validate_processed_value(self):
if not isinstance(self.output, basestring):
raise ImproperOutputValue
def process(self):
for replacer_class in self.get_replacer_classes():
replacer = replacer_class(self.value)
replacer.process()
processed_value = replacer.get_output_value()
if processed_value is not None:
self.output = processed_value
break
def get_replacer_classes(self):
return self.replacer_classes
def get_output_value(self):
return self.output
‹#›
FizzBuzz: OOP.
class BaseArrayProcessor(AbstractArrayProcessor):
item_processor_class = BaseItemProcessor
def __init__(self, array):
super(BaseArrayProcessor, self).__init__(array)
self.array = array
self.validate_input()
self.output = ''
def validate_input(self):
if not isinstance(self.array, (list, tuple, set)):
raise ImproperInputValue(self.array)
def process(self):
output_array = []
for item in self.array:
item_processor_class = self.get_item_processor_class()
item_processor = item_processor_class(item)
item_processor.process()
processed_item = item_processor.get_output_value()
if processed_item:
output_array.append(processed_item)
self.output = ','.join(output_array)
def get_item_processor_class(self):
return self.item_processor_class
def get_output_value(self):
return self.output
‹#›
FizzBuzz: OOP.
FIZZ = "Fizz"
BUZZ = "Buzz"
FIZZBUZZ = FIZZ + BUZZ
class MultiplesOfThreeReplacer(BaseReplacer):
return_value = FIZZ
divider = 3
class MultiplesOfFiveReplacer(BaseReplacer):
return_value = BUZZ
divider = 5
class MultiplesOfThreeAndFiveReplacer(BaseReplacer):
return_value = FIZZBUZZ
divider = 15
class IntToStrReplacer(BaseReplacer):
def check_match(self):
return True
def process(self):
self.output = str(self.value)
‹#›
FizzBuzz: OOP.
class FizzBuzzItemProcessor(BaseItemProcessor):
replacer_classes = (
MultiplesOfThreeAndFiveReplacer,
MultiplesOfThreeReplacer,
MultiplesOfFiveReplacer,
IntToStrReplacer,
)
class FizzBuzzProcessor(BaseArrayProcessor):
item_processor_class = FizzBuzzItemProcessor
def fizzbuzz_oop(arr):
fbp = FizzBuzzProcessor(arr)
fbp.process()
return fbp.get_output_value()
‹#›
ЗАМЕРЫ
‹#›
FizzBuzz: Результаты
cpython
FizzBuzz OOP
pypy
cpython
pypy
to FizzBuzz OOP to FizzBuzz OOP
cpython
cpython
1х
24,11218
FizzBuzz simple
Adding eval
Adding exec
FizzBuzz
optimized
‹#›
FizzBuzz: Результаты
FizzBuzz OOP
cpython
pypy
cpython
pypy
to FizzBuzz OOP to FizzBuzz OOP
cpython
cpython
24,11218
0,72933
1х
FizzBuzz simple
Adding eval
Adding exec
FizzBuzz
optimized
‹#›
33x
FizzBuzz: Результаты
cpython
pypy
cpython
pypy
to FizzBuzz OOP to FizzBuzz OOP
cpython
cpython
FizzBuzz OOP
24,11218
0,72933
1х
33x
FizzBuzz simple
1,23326
0,23751
19,5х
101х
Adding eval
Adding exec
FizzBuzz
optimized
‹#›
FizzBuzz: Результаты
cpython
pypy
cpython
pypy
to FizzBuzz OOP to FizzBuzz OOP
cpython
cpython
FizzBuzz OOP
24,11218
0,72933
1х
33x
FizzBuzz simple
1,23326
0,23751
19,5х
101х
Adding eval
3,49037
6,34854
6,9х
3,8x
Adding exec
FizzBuzz
optimized
‹#›
FizzBuzz: Результаты
cpython
pypy
cpython
pypy
to FizzBuzz OOP to FizzBuzz OOP
cpython
cpython
FizzBuzz OOP
24,11218
0,72933
1х
33x
FizzBuzz simple
1,23326
0,23751
19,5х
101х
Adding eval
3,49037
6,34854
6,9х
3,8x
Adding exec
3,90273
—
6х
—
FizzBuzz
optimized
‹#›
FizzBuzz: Результаты
cpython
pypy
cpython
pypy
to FizzBuzz OOP to FizzBuzz OOP
cpython
cpython
FizzBuzz OOP
24,11218
0,72933
1х
33x
FizzBuzz simple
1,23326
0,23751
19,5х
101х
Adding eval
3,49037
6,34854
6,9х
3,8x
Adding exec
3,90273
—
6х
—
FizzBuzz
optimized
?
?
?
?
‹#›
FizzBuzz: OOP. PyCallGraph
‹#›
О ПРЕЖДЕВРЕМЕННОЙ ОПТИМИЗАЦИИ
‹#›
Оптимизация алгоритма
Для данного списка натуральный чисел (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
‹#›
Оптимизация алгоритма
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?
Оптимизация алгоритма
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)
‹#›
Оптимизация алгоритма
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)
‹#›
Оптимизация алгоритма
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)
‹#›
Оптимизация алгоритма
Количество сравнений для списка значений 1 .. 15
До … 39
После … 30
По времени ~ 3 % разницы
А что если переставить порядок сравнений?
‹#›
Оптимизация алгоритма. Перестановка операций
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)
‹#›
Оптимизация алгоритма. Перестановка операций
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)
‹#›
Оптимизация алгоритма. Перестановка операций
Количество сравнений для списка значений 1 .. 15
Плохой вариант
До … 39
После … 41 (хуже)
Улучшенный вариант
До … 30
После … 30 (не изменилось)
От лучшего до худшего ~ 30%
‹#›
ОПТИМИЗИРУЕМ CPYTHON
‹#›
FizzBuzz: Результаты
cpython
pypy
cpython
pypy
to FizzBuzz OOP to FizzBuzz OOP
cpython
cpython
FizzBuzz OOP
24,11218
0,72933
1х
33x
FizzBuzz simple
1,23326
0,23751
19,5х
101х
Adding eval
3,49037
6,34854
6,9х
3,8x
Adding exec
3,90273
—
6х
—
FizzBuzz
optimized
?
?
?
?
‹#›
Оптимизируем 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)
‹#›
Оптимизируем 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)
‹#›
Оптимизируем 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
FizzBuzz: Тесты
CORRECT_100 = (
"1,2,Fizz,4,Buzz,Fizz,7,8,Fizz,Buzz,11,Fizz,13,14,FizzBuzz,"
"16,17,Fizz,19,Buzz,Fizz,22,23,Fizz,Buzz,26,Fizz,28,29,FizzBuzz,"
"31,32,Fizz,34,Buzz,Fizz,37,38,Fizz,Buzz,41,Fizz,43,44,FizzBuzz,"
"46,47,Fizz,49,Buzz,Fizz,52,53,Fizz,Buzz,56,Fizz,58,59,FizzBuzz,"
"61,62,Fizz,64,Buzz,Fizz,67,68,Fizz,Buzz,71,Fizz,73,74,FizzBuzz,"
"76,77,Fizz,79,Buzz,Fizz,82,83,Fizz,Buzz,86,Fizz,88,89,FizzBuzz,"
"91,92,Fizz,94,Buzz,Fizz,97,98,Fizz,Buzz"
)
def check_correct_100(fn):
print 'checking function {fn.__name__}'.format(**locals()),
output = fn(range(1, 101))
if output == CORRECT_100:
print '.. ok'
else:
print ‘.. failed'
‹#›
Быстрый FizzBuzz
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
samples = tuple(fizzbuzz_samples_helper(xrange(15)))
‹#›
FizzBuzz. Перестановка операций
def fizzbuzz(arr):
output_array = [
samples[i % 15] or str(i) for i in arr]
return ",".join(output_array)
‹#›
FizzBuzz. Перестановка операций
def fizzbuzz(arr):
output_array = [
samples[i % 15] or str(i) for i in arr]
return ",".join(output_array)
1,35x
‹#›
Быстрый FizzBuzz
def fizzbuzz_with_precached_samples(
arr,
# shorteners
__join=",".join,
__samples=samples,
__str=str
):
return __join(__samples[i % 15] or __str(i) for i in arr)
‹#›
Быстрый FizzBuzz
def fizzbuzz_with_precached_samples(
arr,
# shorteners
__join=",".join,
__samples=samples,
__str=str
):
return __join(__samples[i % 15] or __str(i) for i in arr)
0,96x
‹#›
FizzBuzz: Результаты
cpython
pypy
cpython
pypy
to FizzBuzz OOP to FizzBuzz OOP
cpython
cpython
FizzBuzz OOP
24,11218
0,72933
1х
33x
FizzBuzz simple
1,23326
0,23751
19,5х
101х
Adding eval
3,49037
6,34854
6,9х
3,8x
Adding exec
3,90273
—
6х
—
FizzBuzz
optimized
0,72047
0,24492
33,4x
101x
‹#›
СОПРОЦЕСС / COROUTINE
‹#›
Coroutines
64
0 LOAD_FAST
3 LOAD_CLOSURE
6 LOAD_CLOSURE
9 BUILD_TUPLE
12 LOAD_CONST
"./___.py", line 64>)
15 MAKE_CLOSURE
18 LOAD_FAST
21 GET_ITER
22 CALL_FUNCTION
25 CALL_FUNCTION
28 RETURN_VALUE
1 (__join)
0 (__samples)
1 (__str)
2
1 (<code object <genexpr> at 0x10d849930, file
0
0 (arr)
1
1
‹#›
Coroutines
def fizzbuzz_co(
# shorteners
__join=",".join,
__samples=samples,
__str=str
):
arr = ()
while True:
arr = yield __join(__samples[i % 15] or __str(i) for i in arr)
_ = fizzbuzz_co()
_.next()
fizzbuzz_co= _.send
‹#›
Coroutines
def fizzbuzz_co(
# shorteners
__join=",".join,
__samples=samples,
__str=str
):
arr = ()
while True:
arr = yield __join(__samples[i % 15] or __str(i) for i in arr)
input output
output = co.send(input)
создать сопроцесс
_ = fizzbuzz_co()
_.next()
fizzbuzz_co= _.send
«инициализировать» и
получить первый output
заменить ссылку на метод send
‹#›
Coroutines
def co():
x = yield y
[return None]
c = co()
c.send(X)
Как это работает
•def + yield = ключевые слова
•объявление с ключевыми словами создает «конструктор» генератора
•вызов c = co() создает генератор c
•c.next() выполняет все до первого yield, вернет результат выражения y, «встанет
на паузу»
•c.send(X) продолжит выполнение, присвоит значение X переменной x, продолжит
выполнение до следующего yield/return
•return завершает выполнение (исключение StopIteration)
‹#›
Coroutines
def coroutine(fn):
_ = fn()
_.next()
return _.send
‹#›
Coroutines
@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)
‹#›
КЕШИРУЮЩИЕ ФУНКЦИИ
‹#›
Быстрый 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
‹#›
Быстрый FizzBuzz, кэширующая функция
@cached
def process_one(
i,
# shorteners
__samples=samples,
__str=str
):
return __samples[i % 15] or __str(i)
‹#›
Быстрый FizzBuzz, кэширующая функция
def fizzbuzz_with_cache(
arr,
# shorteners
__join=",".join,
):
return __join(map(process_one, arr))
‹#›
«ЧИСЛОДРОБИЛКИ»
‹#›
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
‹#›
Cython, numpy, weave, etc..
Почти тот же 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..
Почти «чистый С»
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'])
‹#›
Cython, numpy, weave, etc..
Method
Time (sec)
relative speed
(меньше-лучше)
Pure python
560
250
NumPy
2,24
1
Cython
1,28
0,51
Weave
1,02
0,45
Faster Cython
0,94
0,42
‹#›
РЕЦЕПТ
‹#›
Рецепт
• найти слабое место
• убедиться что все упирается в производительность кода, а не в
дисковое/сетевое IO
• упростить ООП до простых функций и процедур
• оптимизировать алгоритм
• избавиться от лишних переменных
• избавиться от конструкций object.method()
• использовать итераторы/генераторы вместо списков
• завернуть все в сопроцессы
• постоянно замерять производительность на данных, схожих с
реальными
• тестировать
• знать когда остановиться
‹#›
• Ссылки, литература:
• Дэвид Бизли: генераторы/сопроцессы 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-pythonnumpy-cython-and.html
• google
• Контакты:
• email: iremizov@parallels.com #piterPy
• twitter: @iremizov
‹#›
Q&A
‹#›
Скачать