Оптимизация производительности 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 ‹#›