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

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

Upload: ruparallels

Post on 24-Jul-2015

122 views

Category:

Software


2 download

TRANSCRIPT

Page 1: Сверхоптимизация кода на Python

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

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

Page 2: Сверхоптимизация кода на Python

‹#›

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

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

30x

Page 3: Сверхоптимизация кода на Python
Page 4: Сверхоптимизация кода на Python

‹#›

Python

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

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

Page 5: Сверхоптимизация кода на Python

‹#›

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

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

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

Page 6: Сверхоптимизация кода на Python

‹#›

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

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

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

Page 7: Сверхоптимизация кода на Python

‹#›

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

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

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

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

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

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

Page 8: Сверхоптимизация кода на Python

‹#›

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

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

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

Page 9: Сверхоптимизация кода на Python

‹#›

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

Profiling.

Page 10: Сверхоптимизация кода на Python

‹#›

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

•…

Profiling.

Page 11: Сверхоптимизация кода на Python

‹#›

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

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

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

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

Итого.

Page 12: Сверхоптимизация кода на Python

‹#›

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

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

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

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

Page 13: Сверхоптимизация кода на Python

‹#›

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

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

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

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

Page 14: Сверхоптимизация кода на Python

‹#›

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

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

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

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

Page 15: Сверхоптимизация кода на Python

‹#›

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

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

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

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

• eval, exec — плохо

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

Page 16: Сверхоптимизация кода на Python

‹#›

ПРИМЕРЫ

Page 17: Сверхоптимизация кода на 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

Page 18: Сверхоптимизация кода на Python

‹#›

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

Page 19: Сверхоптимизация кода на Python

‹#›

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)

Page 20: Сверхоптимизация кода на Python

‹#›

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'

Page 21: Сверхоптимизация кода на Python

‹#›

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)

Page 22: Сверхоптимизация кода на Python

‹#›

Инструменты

• Юнит-тесты или иной способ проверки правильности алгоритма 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))

Page 23: Сверхоптимизация кода на Python

‹#›

Как выглядит вывод 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

. . .

Page 24: Сверхоптимизация кода на Python

‹#›

Как выглядит вывод профайлера

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)

Page 25: Сверхоптимизация кода на Python

‹#›

Как выглядит вывод профайлера

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)

Проблемный участок

Page 26: Сверхоптимизация кода на Python

‹#›

Как выглядит вывод профайлера

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)

Артефакт

Page 27: Сверхоптимизация кода на Python

‹#›

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

Page 28: Сверхоптимизация кода на Python

‹#›

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)

Page 29: Сверхоптимизация кода на Python

‹#›

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)

Page 30: Сверхоптимизация кода на Python

‹#›

FizzBuzz: OOP.

ArrayProcessor

Replacer

ItemProcessor

Array of items

ReplacerReplacer

processed as string

Page 31: Сверхоптимизация кода на Python

‹#›

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

Page 32: Сверхоптимизация кода на Python

‹#›

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

Page 33: Сверхоптимизация кода на Python

‹#›

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

Page 34: Сверхоптимизация кода на Python

‹#›

FizzBuzz: OOP.

class ImproperInputValue(Exception): passclass ImproperOutputValue(Exception): pass

Page 35: Сверхоптимизация кода на Python

‹#›

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

Page 36: Сверхоптимизация кода на Python

‹#›

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

Page 37: Сверхоптимизация кода на Python

‹#›

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

Page 38: Сверхоптимизация кода на Python

‹#›

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)

Page 39: Сверхоптимизация кода на Python

‹#›

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()

Page 40: Сверхоптимизация кода на Python

‹#›

ЗАМЕРЫ

Page 41: Сверхоптимизация кода на Python

‹#›

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

cpython pypy cpython to FizzBuzz OOP cpython

pypy to FizzBuzz OOP cpythonFizzBuzz

OOP24,11218 1х

FizzBuzz simple

Adding eval

Adding exec

FizzBuzz optimized

Page 42: Сверхоптимизация кода на Python

‹#›

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

Page 43: Сверхоптимизация кода на Python

‹#›

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

Page 44: Сверхоптимизация кода на Python

‹#›

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

Page 45: Сверхоптимизация кода на Python

‹#›

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

Page 46: Сверхоптимизация кода на Python

‹#›

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

? ? ? ?

Page 47: Сверхоптимизация кода на Python

‹#›

FizzBuzz: OOP. PyCallGraph

Page 48: Сверхоптимизация кода на Python

‹#›

FizzBuzz: OOP. PyCallGraph

Самые ресурсоемкие вызовы

Page 49: Сверхоптимизация кода на Python

‹#›

О ПРЕЖДЕВРЕМЕННОЙ ОПТИМИЗАЦИИ

Page 50: Сверхоптимизация кода на Python

‹#›

Оптимизация алгоритма

Для данного списка натуральный чисел (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

Page 51: Сверхоптимизация кода на Python

‹#›

Оптимизация алгоритма

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?

Page 52: Сверхоптимизация кода на Python

‹#›

Оптимизация алгоритма

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)

Page 53: Сверхоптимизация кода на Python

‹#›

Оптимизация алгоритма

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)

Page 54: Сверхоптимизация кода на Python

‹#›

Оптимизация алгоритма

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)

Page 55: Сверхоптимизация кода на Python

‹#›

Оптимизация алгоритма

Количество сравнений для списка значений 1 .. 15

До … 39 После … 30

По времени ~ 3% разницы По количеству операций ~ 30%

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

Page 56: Сверхоптимизация кода на Python

‹#›

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

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)

Page 57: Сверхоптимизация кода на Python

‹#›

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

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)

Page 58: Сверхоптимизация кода на Python

‹#›

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

Количество сравнений для списка значений 1 .. 15

Плохой вариант До … 39 После … 41 (хуже)

Улучшенный вариант До … 30 После … 30 (не изменилось)

От лучшего до худшего ~ 30%

Page 59: Сверхоптимизация кода на Python

‹#›

ОПТИМИЗИРУЕМ CPYTHON

Page 60: Сверхоптимизация кода на Python

‹#›

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

? ? ? ?

Page 61: Сверхоптимизация кода на Python

‹#›

Оптимизируем 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)

Page 62: Сверхоптимизация кода на Python

‹#›

Оптимизируем 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)

Page 63: Сверхоптимизация кода на Python

‹#›

Оптимизируем 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

Page 64: Сверхоптимизация кода на Python

‹#›

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'

Page 65: Сверхоптимизация кода на Python

‹#›

Быстрый 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)))

Page 66: Сверхоптимизация кода на Python

‹#›

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)

Page 67: Сверхоптимизация кода на Python

‹#›

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

Page 68: Сверхоптимизация кода на Python

‹#›

Быстрый 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)

Page 69: Сверхоптимизация кода на Python

‹#›

Быстрый 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 ?

Page 70: Сверхоптимизация кода на Python

‹#›

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

Page 71: Сверхоптимизация кода на Python

‹#›

ПРОДВИНУТЫЕ ПОДХОДЫ

Page 72: Сверхоптимизация кода на Python

‹#›

СОПРОЦЕСС / COROUTINE

Page 73: Сверхоптимизация кода на Python

‹#›

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

Page 74: Сверхоптимизация кода на Python

‹#›

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

Page 75: Сверхоптимизация кода на Python

‹#›

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

Page 76: Сверхоптимизация кода на Python

‹#›

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)

Page 77: Сверхоптимизация кода на Python

‹#›

Coroutines

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

Page 78: Сверхоптимизация кода на Python

‹#›

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)

Page 79: Сверхоптимизация кода на Python

‹#›

КЕШИРУЮЩИЕ ФУНКЦИИ

Page 80: Сверхоптимизация кода на Python

‹#›

Быстрый 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

Page 81: Сверхоптимизация кода на Python

‹#›

Быстрый 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))

Page 82: Сверхоптимизация кода на Python

‹#›

COROUTINE-BASED CLASS

Page 83: Сверхоптимизация кода на Python

‹#›

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

Page 84: Сверхоптимизация кода на Python

‹#›

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

Page 85: Сверхоптимизация кода на Python

‹#›

COROUTINE-BASED INHERITANCE

Page 86: Сверхоптимизация кода на Python

‹#›

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

Page 87: Сверхоптимизация кода на Python

‹#›

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

Page 88: Сверхоптимизация кода на Python

‹#›

Coroutine based class

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

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

Page 89: Сверхоптимизация кода на Python

‹#›

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

Page 90: Сверхоптимизация кода на Python

‹#›

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

Page 91: Сверхоптимизация кода на Python

‹#›

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

Page 92: Сверхоптимизация кода на Python

‹#›

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'])

Page 93: Сверхоптимизация кода на Python

‹#›

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

Page 94: Сверхоптимизация кода на Python

‹#›

РЕЦЕПТ

Page 95: Сверхоптимизация кода на Python

‹#›

Рецепт

• найти слабое место • убедиться что все упирается в производительность кода, а не в дисковое/сетевое IO

• упростить ООП до простых функций и процедур • оптимизировать алгоритм • избавиться от лишних переменных • избавиться от конструкций object.method() • использовать итераторы/генераторы вместо списков • завернуть все в сопроцессы • постоянно замерять производительность на данных, схожих с реальными

• тестировать • знать когда остановиться

Page 96: Сверхоптимизация кода на Python

‹#›

• Ссылки, литература: • Дэвид Бизли: генераторы/сопроцессы 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