Оптимізація коду Python, System Development

system development and more.

Оптимізація коду Python

Вирішив прояснити собі можливості оптимізації додатків написаних на Python. В інтернеті існує досить багато рекомендацій щодо цього, так що я просто намагаюся звести всю інформацію разом і з'ясувати чим викликані ті чи інші відмінності.

Хоча, на перший погляд, здається, що Python і швидкий код не сумісні поняття, це не зовсім правда.

Всі тести проводилися на Python 3.3.3 і, само собою, не обійшлося без IPython, який просто killer-feature цієї мови.

Python надає досить зручні можливості роботи з рядками, проте, не всі способи однаково швидкі. Тож просто візьмемо кілька рядків і спробуємо їх поєднати тим чи іншим способом.

Чесно кажучи, виміри швидкості конкатенації рядків вийшли дещо спантеличуючими. На мій погляд, вони суперечать не тільки рекомендаціям не використовувати + як повільну операцію 1 , а й здоровому глузду 2

Як видно з тестів, найкращий, та й найчитабельніший варіант - використовувати функцію format явно вказуючи порядковий номер підставки, що підставляється 3 .

Масиви та списки

На жаль, у Python відсутні звичні C++ розробникам масиви. При цьому доступні списки, в CPython реалізовані масивами покажчиків, що забезпечують константний доступ до елементів. Крім того, є масиви з модуля array, масиви з NumPy.

Я очікував що найкращий результат у довільному RW доступі покажуть масиви з NumPy, але, з огляду на те, що вони були розроблені не для цього, припущення виявилося хибним. Тому наведу тести в порядку зростання для масиву розміром 1000000 елементів. Для масивів даних у 10-100 елементів варіант зпреаллокацією виявляється швидшим, але далі починає відставати.

%% timeit l = [ i for i in range ( count ) ]

До речі, C/C++ розробники зверне увагу на наступний момент. Здавалося б швидший спосіб з попередньою аллокацією масиву потрібного розміру виявляється повільнішим варіантом порівняно з використанням List comprehensions. Вся справа в тому, що Python – мова, що інтерпретується, і будь-яка вбудована функція дає відчутний приріст продуктивності.

При використанні NumPy масивів важливо відразу вказати тип даних, що розміщуються в ньому, чому це важливо буде зрозуміло з пункту "Порівняння типів", інакше швидкість роботи буде ще нижче.

%% timeit import numpy as np

l = np. array (np. zeros (count), dtype = int) # (1) for i in range (count): l [i] = i

Наведений нижче приклад використовує тип array за призначенням і, як наслідок, швидкість роботи відповідна.

%% timeit from array import array

l = array ( 'i', ( i for i in range ( count ) ) )

Мені здається, результат цікавий: найшвидший спосіб є очевидним способом. Після C++ це навіть трохи дивно

Множення та розподіл

% timeit bit_k >> 1 % timeit bit_k / 2

У Python зрушення відчутно повільніше операцій множення/поділу.

Порівняння типів

Цього пункту слід звернути особливо пильну увагу, т.к. на алгоритмах щось, що активно вважають, завдяки неуважному відношенню до типу даних, можна отримати падіння швидкості в 2-4 рази (. ).

Вся справа в тому, що операції порівняння між цілими типами і типами з плаваючою комою надзвичайно повільні, що добре видно з прикладу нижче. Тому якщо в процесі оптимізації якась функціяактивно порівнює щось, бажано переконатися в тому, що типи даних, що порівнюються, збігаються. Наприклад, тести на Computer Language Shootout нерідко схильні до цієї проблеми і шляхом додавання .0 деякі з них можна прискорити в ті самі 2-4 рази.

Можна припустити, що вся річ у тому, що конвертація між типами дуже повільна операція:

Але, незважаючи на те, що конвертація операція справді дуже повільна, справа не в цьому:

def comparation ( ) : 1 10.0

def conversion ( ) : int ( 10.0 )

print ("Comparation:") dis. dis (comparation) print ("Conversion:") dis. dis (conversion)

А в тому, що інструкція VM Python COMPARE_OP неефективно опрацьовує параметри різних типів.

Всі принципи оптимізації в Python схожі, і цикли не є винятком. Якщо можна замінити на вбудовані функції – треба змінювати, т.к. VM досить повільна.

%% timeit з functools import reduce з operator import add

reduce ( add , range ( 1000 ) )

Вбудована функція з Python може виявитися в

1.7 разів швидше самописної:

%% timeit з functools import reduce

reduce (lambda res, x: res+x, range (1000))

І лямбди тут майже ні до чого.

від functools import reduce from operator import add

% timeit reduce ( lambda res , x: res + x , range ( 1000 ) )

def my_add (res, x): return res + x % timeit reduce (my_add, range (1000))

% timeit reduce ( add , range ( 1000 ) )

Глобальні та локальні змінні

oldlist = [ str ( i ) for i in range ( 500 ) ] [ cc lang = 'python' ] newlist = [ ]

def f_global ( ) : для слів у oldlist: newlist. append (word. upper ()

def f_local ( l ) : newlist = [ ] for word in l: newlist. append (word. upper ()) return newlist

% timeit newlist = f_local (oldlist)

В даному випадку вся справа в інструкціях, які використовуються для завантаження локальних і глобальних змінних LOAD_GLOBAL і LOAD_FAST відповідно. Як видно з тестів, на Python 3.3.3 за правильної організації доступу до даних можна отримати прискорення близько 20%.

print ( "Global data:") dis. dis (f_global) print ("Local data:") dis. dis (f_local)

Global data : 4 0 SETUP_LOOP 33 ( to 36 ) 3 LOAD_GLOBAL 0 ( oldlist ) 6 GET_ITER >> 7 FOR_ITER 25 ( to 35 ) 10 STORE_FAST 0 ( word )

5 13 LOAD_GLOBAL 1 ( newlist ) 16 LOAD_ATTR 2 ( append ) 19 LOAD_FAST 0 ( word ) 22 LOAD_ATTR 3 ( upper ) 25 CALL_FUNCTION 0 ( 0 positional , 0>28 CALL_FUNCTION 1 ( 1 positional , 0 keyword pair ) 31 POP_TOP 32 JUMP_ABSOLUTE 7 >> 35 POP_BLOCK >> 36 LOAD_CONST 0 ( None ) 39 RETURN_VALUE Local data : 2 0 BUILD_LIST 0 3 STORE_FAST 1 ( newlist )

3 6 SETUP_LOOP 33 ( to 42 ) 9 LOAD_FAST 0 ( l ) 12 GET_ITER >> 13 FOR_ITER 25 ( to 41 ) 16 STORE_FAST 2 ( word )

4 19 LOAD_FAST 1 ( newlist ) 22 LOAD_ATTR 0 ( append ) 25 LOAD_FAST 2 ( word ) 28 LOAD_ATTR 1 ( upper ) 31 CALL_FUNCTION 0 ( 0 positional , 0>34 CALL_FUNCTION 1 ( 1 positional , 0 keyword pair ) 37 POP_TOP 38 JUMP_ABSOLUTE 13 >> 41 POP_BLOCK

5 >> 42 LOAD_FAST 1 (newlist) 45 RETURN_VALUE

Виклик функції VS збереження та виклик

У рекомендаціях щодо оптимізації коду на Python можна зустріти згадки про те, що якщо функцію, що викликається багато разівпідряд спочатку зберегти в будь-якій змінній, а потім викликати, то код швидшим (на шкоду читабельності, звичайно). Причина та сама – глобальні дані (у разі функція) завантажуються довше ніж локальні. Тепер у ролі повільної команди виступає LOAD_ATTR , а як швидка, як і раніше, LOAD_FAST .

def direct_call ( ) : newlist = [ ] newlist. append ( 1 ) return Newlist

def store_and_call ( ) : newlist = [ ] na = newlist. append na ( 1 ) return Newlist

import dis print ("Direct call:") dis. dis (direct_call) print ("Store and call:") dis. dis (store_and_call)

Direct call : 2 0 BUILD_LIST 0 3 STORE_FAST 0 ( newlist )

3 6 LOAD_FAST 0 ( Newlist ) 9 LOAD_ATTR 0 ( append ) 12 LOAD_CONST 1 ( 1 ) 15 CALL_FUNCTION 1 ( 1 positional , 0 keyword pair ) 18 POP_TOP

4 19 LOAD_FAST 0 ( newlist ) 22 RETURN_VALUE Store and call : 7 0 BUILD_LIST 0 3 STORE_FAST 0 ( newlist )

8 6 LOAD_FAST 0 (newlist) 9 LOAD_ATTR 0 (append) 12 STORE_FAST 1 (na)

9 15 LOAD_FAST 1 ( na ) 18 LOAD_CONST 1 ( 1 ) 21 CALL_FUNCTION 1 ( 1 positional , 0 keyword pair ) 24 POP_TOP

10 25 LOAD_FAST 0 ( Newlist ) 28 RETURN_VALUE

Здається, на цьому корисні рекомендації закінчуються, у крайньому випадку мені більше нічого працюючого і не потребує чогось нетривіального і складного, наприклад PyCUDA, Numba і т.д. не попалося. А про PyCUDA, Numba я, напевно, напишу окремо і трохи згодом.