Производительность в Python. Легкий путь
Всегда знал, что одно из достоинств Python — возможность переписать самые тормозные куски кода на Си и увеличить быстродействие программы до недостижимых интерпретируемым языкам высот. Но сам ни разу не пробовал, т.к. считал что это слишком сложно. После прочтения этой статьи больше так не считаю.
Программисты знакомые с ctypes врядли найдут тут что-то интересное, новичков же прошу под кат.
Ctypes — механизм Python для импорта функций из внешних библиотек.
%timeit — magic-функция оболочки IPython, измеряющая время выполнения выражения на Python
Ctypes — это прекрасно! Давайте начнем с небольшого банального примера: суммирование чисел в определенном диапазоне.
Вот реализация этой функции на Python
def sumrange(arg): return sum(xrange(arg))
Отлично! Но что если мы попробуем суммировать действительно большой диапазон чисел, например от 0 до 10**8 (т.е. 100,000,000)
In [2]: %timeit sumrange(10**2) 1000000 loops, best of 3: 1.53 us per loop In [3]: %timeit sumrange(10**8) 1 loops, best of 3: 9.77 s per loop In [4]: %timeit sumrange(10**9) 1 loops, best of 3: 97.8 s per loop
Уже не так весело. Попробуем кое-что другое:
def sumrange2(arg): x = i = 0 while i < arg: x += i i += 1 return x
Что из этого получится?
In [10]: %timeit sumrange2(10**2) 100000 loops, best of 3: 10.5 us per loop In [11]: %timeit sumrange2(10**8) 1 loops, best of 3: 18.5 s per loop
Вот это да… Так еще хуже… В этот раз даже не буду пробовать 10**9.
Так как же нам ускорить выполнение? Только не предлагайте математические оптимизации… мы же в новом мире компьютеров! (в оригинале: don't suggest math tricks… this is the the new world of computing!)
Да, я знаю, что сложность алгоритма — постоянная величина и не зависит о величины аргумента, n*(n+1)/2. Но статья посвящена не этому.
Как насчет ctypes?
#include unsigned long long sumrange(unsigned long long arg) < unsigned long long i, x; x = 0; for (i = 0; i < arg; i++) < x = x + i; >return x; >
Сохраним с именем sumrange.c и скомпилируем (не будем использовать оптимизации для чистоты эксперимента):
$ gcc -shared -Wl,-install_name,sumrange.so -o sumrange.so -fPIC sumrange.c
Импортируем в Python то что получилось:
import ctypes sumrange_ctypes = ctypes.CDLL('./sumrange.so').sumrange sumrange_ctypes.restype = ctypes.c_ulonglong sumrange_ctypes.argtypes = ctypes.c_ulonglong,
И Оскар получает…
In [15]: %timeit sumrange_ctypes(10**2) 1000000 loops, best of 3: 1.28 us per loop In [16]: %timeit sumrange_ctypes(10**8) 1 loops, best of 3: 381 ms per loop In [17]: %timeit sumrange_ctypes(10**9) 1 loops, best of 3: 3.79 s per loop In [18]: %timeit sumrange_ctypes(10**10) 1 loops, best of 3: 37.8 s per loop
| 10**2 | 10**8 | 10**9 | 10**10 | |
|---|---|---|---|---|
| Чистый Python, способ №1 | 1.53 мкс | 9.77 с | 97.8 с | - |
| Чистый Python, способ №2 | 10.5 мкс | 18.5 с | - | - |
| ctypes | 1.28 мкс | 381 мс | 3.79 с | 37.8 с |
Адский прирост производительности!
Для Node.js хакеров, есть эквивалент ctypes — FFI (Foreign Function Interface): github.com/rbranson/node-ffi
6 способов измерить скорость программы на Python
Недавно мы решали сложную задачу Эйнштейна с помощью кода на Python, а потом оптимизировали его, чтобы сократить время выполнения. Там всё было просто: с четырёх часов мы оптимизировали время выполнения до долей секунды, и это было явно заметно. Но бывает так, что даже полсекунды оптимизации — это очень хорошо, когда речь идёт о высоконагруженных сервисах. Например, в соцсетях, которыми пользуются сотни тысяч пользователей в минуту. Сегодня мы покажем целых 6 простых способов измерения времени работы кода, которые может использовать каждый.
Как измерять время выполнения кода
В большинстве случаев измерить время работы кода можно так:
- Зафиксировать время начала работы.
- Зафиксировать время окончания работы.
- Вычесть первое значение из второго.
Ещё важно измерять время выполнения кода при одних и тех же условиях:
- конфигурация и мощность компьютера должны совпадать для всех замеров;
- загрузка процессора должна быть одинаковой;
- программа для работы с кодом должна быть одной и той же с одинаковой версией.
Но даже если эти условия выполняются, результаты нескольких замеров могут немного различаться для одного и того же кода. На это могут влиять фоновые процессы, поэтому для точных измерений проводят несколько замеров в чистой среде, когда кроме кода и системных процессов ничего нет.
Если будете заниматься оптимизацией, вот вам на вырост: нужно различать понятия wall time («время на стене») и процессорное время. Первое показывает прошедшее время от начала до конца работы, второе — время, которое процессор затратил на выполнение кода. Их значения могут различаться, если программа ожидает высвобождение ресурсов для выполнения. Но сейчас можно без этих тонкостей.
Модуль datetime
С помощью такого способа можно измерить время выполнения кода в формате часы:минуты:секунды:микросекунды. Мы использовали модуль datetime, когда оптимизировали код для решения загадки Эйнштейна и ускоряли работу программы более чем в 200 тысяч раз.
# подключаем модуль datetime import datetime # фиксируем и выводим время старта работы кода start = datetime.datetime.now() print('Время старта: ' + str(start)) # код, время работы которого измеряем #фиксируем и выводим время окончания работы кода finish = datetime.datetime.now() print('Время окончания: ' + str(finish)) # вычитаем время старта из времени окончания print('Время работы: ' + str(finish - start))
Результат — 51 тысячная секунды. Неплохо, но что покажут другие способы?

Модуль time
Модуль time предоставляет разные возможности для измерения времени работы кода:
- time.time() поможет измерить время работы в секундах. Если нужно получить время в минутах, результат вычисления нужно разделить на 60, в миллисекундах — умножить на 1000.
- time.perf_counter() также можно использовать для измерения времени в секундах, но таймер не будет зависеть от системных часов. Функцию используют, чтобы избежать погрешностей. Функция time.perf_counter_ns() вернёт значение в наносекундах.
- time.monotonic() подходит для больших программ, поскольку эта функция не зависит от корректировки времени системы. Функция использует отдельный таймер, как и time.perf_counter(), но имеет более низкое разрешение. С помощью time.monotonic_ns() можно получить результат в наносекундах.
- time.process_time() поможет получить сумму системного и пользовательского процессорного времени в секундах, не включая время сна. Если процесс выполнения блокируется функцией time.sleep() или приостанавливается операционной системой, это время не включается в отчётное. Для наносекунд есть функция time.process_time_ns(), но её поддерживают не все платформы.
- time.thread_time() сообщит время выполнения текущего потока, а не процесса. Если в коде есть функция time.sleep(), время её выполнения не будет включено.
Time.time(). Давайте посчитаем время выполнения нашего кода с помощью функции time.time() в миллисекундах:
# подключаем модуль time import time # фиксируем время старта работы кода start = time.time() # код, время работы которого измеряем #фиксируем время окончания работы кода finish = time.time() # вычитаем время старта из времени окончания и получаем результат в миллисекундах res = finish - start res_msec = res * 1000 print('Время работы в миллисекундах: ', res_msec)
Получаем результат: 61 тысячная секунды. Результат отличается от предыдущего, тут уже нужно было бы хорошо сделать серию тестов и посчитать среднее значение.

Time.perf_counter(). Посчитаем время выполнения нашего кода с помощью функции time.perf_counter() в секундах:
# подключаем модуль time import time # фиксируем время старта работы кода start = time.perf_counter() # код, время работы которого измеряем #фиксируем время окончания работы кода finish = time.perf_counter() # вычитаем время старта из времени окончания и выводим результат print('Время работы: ' + str(finish - start))
Мы получили 51 тысячную секунды — почти такой же результат, как и в самый первый раз. Кажется, что это точное время, но посмотрим, что будет дальше.

Time.monotonic_ns(). Посчитаем время выполнения нашего кода с помощью функции time.monotonic_ns() в наносекундах:
# подключаем модуль time import time # фиксируем время старта работы кода start = time.monotonic_ns() # код, время работы которого измеряем #фиксируем время окончания работы кода finish = time.monotonic_ns() # вычитаем время старта из времени окончания и получаем результат в наносекундах print('Время работы в наносекундах: ' + str(finish - start))
Результат примерно такой же — 52 тысячные секунды, но количество цифр в результате меньше, чем в предыдущем случае.

Time.process_time(). Посчитаем сумму системного и пользовательского процессорного времени в секундах:
# подключаем модуль time import time # фиксируем время старта работы кода start = time.process_time() # код, время работы которого измеряем #фиксируем время окончания работы кода finish = time.process_time() # вычитаем время старта из времени окончания и выводим результат print('Время работы: ' + str(finish - start))
Время работы снова выросло — с 51 до 62 тысячных секунды. Для одних программ такой разброс вообще некритичен, а для других это может означать, что нужно провести больше тестов.

Time.thread_time(). Наконец, посчитаем время выполнения кода с помощью time.thread_time():
# подключаем модуль time import time # фиксируем время старта работы кода start = time.thread_time() # код, время работы которого измеряем #фиксируем время окончания работы кода finish = time.thread_time() # вычитаем время старта из времени окончания и выводим результат print('Время работы: ' + str(finish - start))
Время работы снова выглядит правдоподобно в сравнении с предыдущим результатом.

Что дальше
В следующий раз продолжим оптимизировать наш код решения задачи Эйнштейна: отформатируем и избавимся от вложенных данных. Подпишитесь, чтобы не пропустить продолжение.
Сравнение скорости Python и C++
Автор этой статьи делает сравнение скорости выполнения одной и той же программы на Python и C++. С++ естественно быстрее, но насколько?
Прим. ред. Это перевод статьи Назера Тамими. Мнение редакции может не совпадать с мнением автора оригинала.
Есть миллион причин любить Python (особенно если вы дата-сайентист). Но насколько Python отличается от низкоуровневых языков, таких как Си и C++? В этой статье я собираюсь сделать сравнение скорости Python и C++, на очень простом примере.
Мы будем генерировать все возможные k-меры ДНК, для фиксированного
значения “k”. О том, что такое k-меры, я расскажу чуть позже. Этот пример был выбран потому, что многие задачи обработки и анализа данных связанные с геномом, считаются ресурсоёмкими. Поэтому, многие дата-сайентисты связанные с биоинформатикой, интересуются C++ (в дополнение к Python).
Важное замечание: цель этой статьи не сравнить скорость С++ и Python когда они наиболее эффективны. Код предлагаемых программ можно сделать гораздо более быстрым. Цель этой статьи — сравнить два языка, используя один и тот же алгоритм и код.
Введение в k-меры ДНК
ДНК — это длинная цепь нуклеотидов. Эти нуклеотиды могут быть четырёх типов: A, C, G и T. У вида Homo Sapiens около 3 миллиардов пар нуклеотидов. Вот небольшой кусок ДНК человека:
ACTAGGGATCATGAAGATAATGTTGGTGTTTGTATGGTTTTCAGACAATT
Чтобы получить из него k-меры нужно разбить строку на части:
ACTA, CTAG, TAGG, AGGG, GGGA и т. д.
Эти последовательности из четырех символов называются k-меры длина которых равна четырём (4-меры).
Задача
Мы сгенерируем всё возможные 13-меры. Математически — это перестановка с проблемой замены. Следовательно мы имеем 4 в степени 13 (67 108 864) вариантов 13-меров.
Сравнение скорости Python и С++
Мы будем использовать один и тот же алгоритм для двух языков. Код на обоих языках намеренно написан аналогично и просто. Я не использовал сложные структуры данных и сторонние библиотеки. Вот код программы на Python:
def convert(c): if (c == 'A'): return 'C' if (c == 'C'): return 'G' if (c == 'G'): return 'T' if (c == 'T'): return 'A' print("Start") opt = "ACGT" s = "" s_last = "" len_str = 13 for i in range(len_str): s += opt[0] for i in range(len_str): s_last += opt[-1] pos = 0 counter = 1 while (s != s_last): counter += 1 # Следующая строка выводит результаты # print(s) change_next = True for i in range(len_str): if (change_next): if (s[i] == opt[-1]): s = s[:i] + convert(s[i]) + s[i+1:] change_next = True else: s = s[:i] + convert(s[i]) + s[i+1:] break # Следующая строка выводит результаты # print(s) print("Number of generated k-mers: <>".format(counter)) print("Finish!")
Выполнение этой программы займет 61.23 секунды. За это время сгенерируется 67 миллионов 13-меров. Чтобы не увеличивать время работы программы я закомментировал код выводящий результаты (25 и 37 строки). Если вы захотите запустить этот код и отобразить результаты, имейте ввиду, что это будет очень долго. Чтобы остановить выполнение программы вы можете нажать на клавиатуре CTRL+С.
Теперь посмотрим тот же алгоритм на языке C++:
#include #include using namespace std; char convert(char c) < if (c == 'A') return 'C'; if (c == 'C') return 'G'; if (c == 'G') return 'T'; if (c == 'T') return 'A'; return ' '; >int main() < cout for (int i=0; i int pos = 0; int counter = 1; while (s != s_last) < counter ++; Следующая строка выводит результаты: cout else < s[i] = convert(s[i]); break; >> > > Следующая строка выводит результаты: "Number of generated k-mers: " counter
В таблице указаны результаты тестов для 13, 14, и 15-меров.
После компиляции, этот код выполнится за 2.42 секунды. Получается что Python требуется в 25 раз больше времени на эту задачу. Я повторил эксперимент с 14 и 15-мерами (это можно указать на 12 строке в Python и на 22 в C++) Теперь мы видим, что производительность этих двух языков, при выполнении одной и той же задачи, значительно различается.
Я повторюсь, обе программы далеки от идеала и могут быть значительно опимизированы. Например, мы не использовали параллельные вычисления на CPU или GPU. Но для таких задач это необходимо. Также мы не храним результаты. Хотя управление памятью в Python и C++ значительно влияет на производительность.
Этот пример и тысячи других задач, подтверждают, что дата-сайентистам стоит обращать внимание на C++ и подобные ему языки, когда нужно работать с большими массивами данных или требующими большой производительности процессами.
Что может Python сделать за секунду?
Все говорят, что Python медленный, но так ли это? Пройдите наш тест и узнаете, сколько всего этот язык способен сделать за секунду!
Порой можно услышать, что Python медленный в сравнении с определёнными языками. Мы не собираемся разводить дискуссии на тему сравнения языков, а хотим посмотреть на это с другой стороны. Когда-нибудь задумывались, сколько различных операций вы можете совершить в Python всего за 1 секунду? Пройдите наш тест и узнаете!
Примечание У всех разное железо, поэтому очевидно, что приведённые здесь результаты могут отличаться от ваших, если вы решите их проверить. Поэтому ваша задача — ошибиться менее, чем в 10 раз.
Тестируемый код написан на CPython версии 2.X. Замеры проводились на современном ноутбуке с быстрым SSD и интернет-подключением.