Сверхоптимизация кода на python

96
Ремизов Иван Cloud Architect Оптимизация производительности Python

Upload: codefest

Post on 16-Jul-2015

516 views

Category:

Software


1 download

TRANSCRIPT

Ремизов Иван Cloud Architect

Оптимизация производительности Python

‹#›

Картинка для привлечения внимания

* CPython, без привлечения внешних зависимостей, компиляции и тп

30x

‹#›

Python

Рассматриваем язык: Python 2.x

Конкретные реализации: CPython (*nix default) PyPy (JIT)

‹#›

Проблемы приложения.

Какие проблемы вообще бывают?

• неудачные архитектурные решения • неудачно выбранные компоненты и фреймворки • медленный I/O • высокий расход памяти, утечки памяти • медленный код

‹#›

Проблемы приложения.

Как решается большинство проблем?

• добавление воркеров • кеширование • отложенные задания, очереди • замена компонентов • map/reduce • изменение архитектуры • …

‹#›

Когда это критично и не решаемо «привычными» способами?

Обработка потоковых данных пример: процессинг датчиков (акселерометры, гироскопы)

Десериализация пример: JSON, pickle, ..

Авторегрессия пример: EMA (скользящая средняя), численное интегрирование, ряды

Стейт-машины пример: AI, синтаксические анализаторы текста

Медленный код.

‹#›

Профилирование специальными утилитами • ручной профайлинг (тайминг) • статистический профайлинг (сэмплинг) • событийный профайлинг (граф вызовов)

Логгирование и сбор статистики • настройка конфигов apache/nginx/… • логи приложения

Как найти критические участки кода?

‹#›

Утилиты • profile/cprofile • pycallgraph • dis (иногда бывает полезно)

Profiling.

‹#›

Выбор огромен • line_profiler • hotshot • gprof2dot • memory_profiler • objgraph • memprof •для django есть миддлвары с картинками и графиками • django debug toolbar • django live profiler

•…

Profiling.

‹#›

Задача: профилирование живого WEB-сервера • мы не хотим чтобы профилировщик значительно снижал производительность

• мы хотим получить более-менее репрезентативные данные

Решение: 1. поднять апстрим на ~1% и собирать статистику с него (*) 2. воспроизвести на стейджинге/тестовом окружении

Альтернатива: • настраиваем access logs • смотрим, где медленно • разбираемся почему

Итого.

‹#›

• проводим серию испытаний • замеряем среднее время • исключаем I/O, профилировщик и тп • помним про погрешность • разогреваем JIT (* PyPy ~ 0.2c — см. доки) • как-то используем результаты теста, иначе JIT может его «вырезать»

• целевой пробег сопоставим по производительности с разогревочным

• целевой пробег на JIT должен работать быстрее

Как правильно писать тесты на производительность?

‹#›

• Регрессионные тесты • Не нужно делать гипотез и предположений: только цифры

• Проблему с I/O исключили • Первое что стоит оптимизировать — алгоритм • Проблема скорее всего в каком-то из циклов • Все статические переменные должны быть вынесены из цикла

• eval, exec — плохо • Не увлекаться!

О чем всегда помнить

‹#›

CPython — интерпретатор. Он честно интерпретирует каждую строку кода.

• Lookup-ы — очень дороги • атрибуты и методы • локальные/глобальные переменные • замыкание

• Запоминание переменных дорого • Создание объектов — дорого • Изменение размеров объектов в памяти — дорого • eval, exec — плохо

Особенности присущие CPython

‹#›

PyPy использует JIT. PyPy пытается исполнить то, что вы имели в виду Исполняется совсем не тот код, который вы пишите.

• JIT scope != trace: locals(), globals(), sys._getframe(), sys.exc_info(), sys.settrace, …

• На JIT компиляцию требуется время (>0.2s) • => то, что «гоняется редко» — оптимизировано не будет

• C-модули поддерживаются плохо: используем Python-версию

• eval, exec — плохо

Особенности присущие PyPy

‹#›

ПРИМЕРЫ

‹#›

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 shuffledef _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 disdis(fizzbuzz_simple)

• Модуль Profile from profile import runrun('fizzbuzz_simple(range(100000))')

• Утилита Pycallgraph from pycallgraph import PyCallGraphfrom pycallgraph.output import GraphvizOutputwith PyCallGraph(output=GraphvizOutput()): fizzbuzz_simple(range(100000))

‹#›

Как выглядит вывод dis

4 0 BUILD_LIST 0 3 STORE_FAST 1 (output_array)

5 6 SETUP_LOOP 129 (to 138) 9 LOAD_FAST 0 (arr) 12 GET_ITER >> 13 FOR_ITER 121 (to 137) 16 STORE_FAST 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 1 (output_array) 38 LOAD_ATTR 0 (append) 41 LOAD_CONST 3 ('FizzBuzz') 44 CALL_FUNCTION 1 47 POP_TOP 48 JUMP_ABSOLUTE 13

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

9 67 LOAD_FAST 1 (output_array) 70 LOAD_ATTR 0 (append) 73 LOAD_CONST 5 ('Fizz') 76 CALL_FUNCTION 1 79 POP_TOP 80 JUMP_ABSOLUTE 13

10 >> 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 99 LOAD_FAST 1 (output_array) 102 LOAD_ATTR 0 (append) 105 LOAD_CONST 7 ('Buzz') 108 CALL_FUNCTION 1 111 POP_TOP 112 JUMP_ABSOLUTE 13

13 >> 115 LOAD_FAST 1 (output_array) 118 LOAD_ATTR 0 (append) 121 LOAD_GLOBAL 1 (str) 124 LOAD_FAST 2 (i) 127 CALL_FUNCTION 1 130 CALL_FUNCTION 1 133 POP_TOP 134 JUMP_ABSOLUTE 13 >> 137 POP_BLOCK

14 >> 138 LOAD_CONST 8 (',') 141 LOAD_ATTR 2 (join) 144 LOAD_FAST 1 (output_array) 147 CALL_FUNCTION 1 150 RETURN_VALUE

4 0 BUILD_LIST 0 3 STORE_FAST 1 (output_array)

5 6 SETUP_LOOP 129 (to 138) 9 LOAD_FAST 0 (arr) 12 GET_ITER >> 13 FOR_ITER 121 (to 137) 16 STORE_FAST 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 1 (output_array) 38 LOAD_ATTR 0 (append) 41 LOAD_CONST 3 ('FizzBuzz') 44 CALL_FUNCTION 1 47 POP_TOP 48 JUMP_ABSOLUTE 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.

ArrayProcessor

Replacer

ItemProcessor

Array of items

ReplacerReplacer

processed as string

‹#›

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): passclass 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 + BUZZclass MultiplesOfThreeReplacer(BaseReplacer): return_value = FIZZ divider = 3 class MultiplesOfFiveReplacer(BaseReplacer): return_value = BUZZ divider = 5 class MultiplesOfThreeAndFiveReplacer(BaseReplacer): return_value = FIZZBUZZ divider = 15class 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 = FizzBuzzItemProcessordef fizzbuzz_oop(arr): fbp = FizzBuzzProcessor(arr) fbp.process() return fbp.get_output_value()

‹#›

ЗАМЕРЫ

‹#›

FizzBuzz: Результаты

cpython pypy cpython to FizzBuzz OOP cpython

pypy to FizzBuzz OOP cpythonFizzBuzz

OOP24,11218 1х

FizzBuzz simple

Adding eval

Adding exec

FizzBuzz optimized

‹#›

FizzBuzz: Результаты

cpython pypy cpython to FizzBuzz OOP cpython

pypy to FizzBuzz OOP cpythonFizzBuzz

OOP24,11218 0,72933 1х 33x

FizzBuzz simple

Adding eval

Adding exec

FizzBuzz optimized

‹#›

FizzBuzz: Результаты

cpython pypy cpython to FizzBuzz OOP cpython

pypy to FizzBuzz OOP cpythonFizzBuzz

OOP24,11218 0,72933 1х 33x

FizzBuzz simple

1,23326 0,23751 19,5х 101х

Adding eval

Adding exec

FizzBuzz optimized

‹#›

FizzBuzz: Результаты

cpython pypy cpython to FizzBuzz OOP cpython

pypy to FizzBuzz OOP cpythonFizzBuzz

OOP24,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 to FizzBuzz OOP cpython

pypy to FizzBuzz OOP cpythonFizzBuzz

OOP24,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 to FizzBuzz OOP cpython

pypy to FizzBuzz OOP cpythonFizzBuzz

OOP24,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

‹#›

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% разницы По количеству операций ~ 30%

А что если переставить порядок сравнений?

‹#›

Оптимизация алгоритма. Перестановка операций

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 to FizzBuzz OOP cpython

pypy to FizzBuzz OOP cpythonFizzBuzz

OOP24,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 Falsesamples = tuple(fizzbuzz_samples_helper(xrange(15)))

‹#›

FizzBuzz. Перестановка операций

samples = (False, False, «Fizz" ,False, «Buzz", . . . , "FizzBuzz")

def fizzbuzz(arr): output_array = [ samples[i % 15] or str(i) for i in arr] return ",".join(output_array)

‹#›

FizzBuzz. Перестановка операций

samples = (False, False, «Fizz" ,False, «Buzz", . . . , "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 to FizzBuzz OOP cpython

pypy to FizzBuzz OOP cpythonFizzBuzz

OOP24,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 1 (__join) 3 LOAD_CLOSURE 0 (__samples) 6 LOAD_CLOSURE 1 (__str) 9 BUILD_TUPLE 2 12 LOAD_CONST 1 (<code object <genexpr> at 0x10d849930, file "./___.py", line 64>) 15 MAKE_CLOSURE 0 18 LOAD_FAST 0 (arr) 21 GET_ITER 22 CALL_FUNCTION 1 25 CALL_FUNCTION 1 28 RETURN_VALUE

‹#›

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)

_ = fizzbuzz_co()_.next()fizzbuzz_co= _.send

outputinputoutput = co.send(input)

«инициализировать» и получить первый output

создать сопроцесс

заменить ссылку на метод send

‹#›

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)

‹#›

Coroutines

Можно обернуть в декоратор: def coroutine(fn): _ = fn() _.next() return _.send

‹#›

Coroutines

… и поместить все внутрь (до первого yield)

@coroutinedef 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, кэширующая функция

@cacheddef process_one( i, # shorteners __samples=samples, __str=str): return __samples[i % 15] or __str(i)

def fizzbuzz_with_cache( arr, # shorteners __join=",".join, ): return __join(map(process_one, arr))

‹#›

COROUTINE-BASED CLASS

‹#›

class MakeSum(Exception): passclass ChgKoef(Exception): passdef co(): x = None y = None k = 1 rv = None while True: try: x, y = yield rv rv = k * x * y except MakeSum as e: x, y = e.args rv = k * (x + y) except ChgKoef as e: k = e.args[0]

Coroutine based class

class Cls(object): def __init__(self): self.x = None self.y = None self.k = 1 self.rv = None def main_method(self, x, y): self.x = x self.y = y self.rv = self.k * self.x * self.y return self.rv def make_sum(self, x, y): self.x = x self.y = y self.rv = self.k * (self.x + self.y) return self.rv def chg_koef(self, k): self.k = k return self.rv

‹#›

instance = co()print instance# <generator object co at 0x10047bbe0>print instance.next()# Noneprint instance.send((1, 2))# 2 == 1 * 1 * 2print instance.send((3, 4))# 12 == 1 * 3 * 4print instance.throw(MakeSum(5, 6)) # 11 == 1 * (5 + 6)print instance.send((7, 8))# 56 == 1 * 7 * 8print instance.throw(ChgKoef(10)) # 56 (last value repeated)print instance.send((1, 2))# 20 == 10 * 1 * 2

Coroutine based inheritance

instance = Cls()print instance# <__main__.Cls object at 0x10a1c6210>

print instance.main_method(1, 2) # 2 == 1 * 1 * 2print instance.main_method(3, 4) # 12 == 1 * 3 * 4print instance.make_sum(5, 6) # 11 == 1 * (5 + 6)print instance.main_method(7, 8) # 56 == 1 * 7 * 8print instance.chg_koef(10) # 56 (last value repeated)print instance.main_method(1, 2) # 20 == 10 * 1 * 2

‹#›

COROUTINE-BASED INHERITANCE

‹#›

def co( param, # ___ __some_value=5, __some_method=lambda: 10): rv = None while True: input = yield rv

def co_sub( param, # ___ __some_value=10, __some_method=lambda: 20): return co(**locals())

Coroutine based class

class Cls(object): def __init__(self, param): self.param = param some_value = 5 def some_method(self): return 10 def main_method(self): returnclass SubCls(Cls): some_value = 10 def some_method(self): return 20

‹#›

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

‹#›

Coroutine based class

Плюсы • Основной метод работает быстрее • «Наследование»

Минусы • Интерфейс «заморожен» • Основной метод «заморожен» • Код «специфичен»

‹#›

«ЧИСЛОДРОБИЛКИ»

‹#›

Cython, numpy, weave, etc..

«Числодробилки» Travis Oliphant

from numpy import zerosfrom scipy import weavedx = 0.1dy = 0.1dx2 = dx*dxdy2 = dy*dydef 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

‹#›

Почти тот же 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..

‹#›

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-

python-numpy-cython-and.html • google

• Контакты: • email: [email protected] #CodeFest • twitter: @iremizov