#python #python-2.7 #cython
#python #python-2.7 #cython
Вопрос:
У меня есть большой блок кода Cython, который анализирует файлы Touchstone, с которыми я хочу работать с Python 2 и Python 3. Я использую методы синтаксического анализа в стиле C для того, что, как я думал, будет максимально эффективным, включая ручную блокировку и освобождение char*
вместо использования bytes
, чтобы я мог избежать GIL. При компиляции с использованием
python 3.5.2 0 anaconda
cython 0.24.1 py35_0 anaconda
Я вижу скорости, которыми я доволен, умеренный прирост для небольших файлов (на ~ 20% быстрее) и огромный прирост для больших файлов (в ~ 2,5 раза быстрее). При компиляции с
python 2.7.12 0 anaconda
cython 0.24.1 py27_0 anaconda
Он выполняется примерно в 125 раз медленнее (~ 17 мс в Python 3 против ~ 2,2 с в Python 2). Это точно такой же код, скомпилированный в разных средах с использованием довольно простого setuputils
скрипта. В настоящее время я не использую NumPy из Cython ни для синтаксического анализа, ни для хранения данных.
import cython
cimport cython
from cython cimport array
import array
from libc.stdlib cimport strtod, malloc, free
from libc.string cimport memcpy
ctypedef long long int64_t # Really VS2008? Couldn't include this by default?
# Bunch of definitions and utility functions omitted
@cython.boundscheck(False)
cpdef Touchstone parse_touchstone(bytes file_contents, int num_ports):
cdef:
char c
char* buffer = <char*> file_contents
int64_t length_of_buffer = len(file_contents)
int64_t i = 0
# These are some cpdef enums
FreqUnits freq_units
Domain domain
Format fmt
double z0
bint option_line_found = 0
array.array data = array.array('d')
array.array row = array.array('d', [0 for _ in range(row_size)])
while i < length_of_buffer:
c = buffer[i] # cdef char c
if is_whitespace(c):
i = 1
continue
if is_comment_char(c):
# Returns the last index of the comment
i = parse_comment(buffer, length_of_buffer)
continue
if not option_line_found and is_option_leader_char(c):
# Returns the last index of the option line
# assigns values of all references passed in
i = parse_option_line(
buffer, length_of_buffer, i,
amp;domain, amp;fmt, amp;z0, amp;freq_units)
if i < 0:
# Lots of boring code along the lines of
# if i == some_int:
# raise Exception("message")
# I did this so that only my top-level parse has to interact
# with the interpreter, all the lower level functions have nogil
option_line_found = 1
if option_line_found:
if is_digit(c):
# Parse a float
row[row_idx] = strtod(buffer i, amp;end_of_value)
# Jump the cursor to the end of that float
i = end_of_value - p - 1
row_idx = 1
if row_idx == row_size:
# append this row onto the main data array
data.extend(row)
row_idx = 0
i = 1
return Touchstone(num_ports, domain, fmt, z0, freq_units, data)
Я исключил несколько вещей, таких как приведение типов. Я также тестировал, где код просто перебирает весь файл, ничего не делая. Либо Cython оптимизировал это, либо это просто очень быстро, потому что это приводит parse_touchstone
к тому, что он даже не отображается в cProfile/pstats
отчете. Я определил, что это не просто синтаксический анализ строки комментариев, пробелов и параметров (не показан значительно более сложный синтаксический анализ значений ключевых слов) после того, как я ввел оператор print в последнем if row_idx == row_size
блоке для распечатки статуса и обнаружил, что требуется около 0,5-1 секунды (по приблизительным подсчетам), чтобыпроанализируйте строку с 512 числами с плавающей запятой. Это действительно не должно занимать так много времени, особенно при использовании strtod
для синтаксического анализа. Я также проверил синтаксический анализ значений всего на 2 строки, а затем выпрыгнул из цикла while, и он сказал мне, что синтаксический анализ комментариев, пробелов и строки параметров занял около 800 мс (1/3 от общего времени), и это было для 6 строк текста общим объемом менее 150 байт.
Я просто что-то здесь упускаю? Есть ли небольшой трюк, который заставил бы код Cython работать на 3 порядка медленнее в Python 2, чем в Python 3?
(Примечание: я не показал здесь полный код, потому что я не уверен, разрешено ли мне это по юридическим причинам, и потому что всего около 450 строк)
Комментарии:
1. Я подозреваю, что вам нужно создать небольшой тестовый пример, который мы действительно можем запустить и протестировать. Если кто-то не работал с подобной задачей, проблема не исчезнет, просто прочитав код. Как
cython
скорость сравнивается с чистым Python (любой версией)?2. @hpaulj к сожалению, я не уверен, что смогу опубликовать полную рабочую версию по юридическим причинам, но я скажу, что> 99% файла будет проанализировано в этом последнем блоке if. Другие детали реализации не важны, возможно, я могу написать простой анализатор, который просматривает только данные. Скорость с чистым Python намного лучше, 22 мс для небольших файлов и около 1,2 с для больших файлов. Я подозреваю, что замедление может быть вызвано функциями libc, хотя я бы не ожидал, что оно будет настолько массовым.
3. Я не думал обо всем коде; просто подмножество, которое демонстрирует проблему. Да, вам потребуется больше работы.
4. Две мысли. 1) это может быть связано с выделением памяти для ваших массивов (строка
data.extend(row)
?). Возможно, что Python3 внес некоторые улучшения в этом отношении. Возможно, составитьdata
список строк, а затем объединить их в непрерывный массив в конце (как только вы узнаете, насколько он велик), чтобы избежать перераспределения?5. 2) Я знаю, что есть некоторые различия между Python2 и Python3 в том, как он выполняет GIL-переключатели. Вы не показали никаких выпусков и исправлений GIL в своем коде, но ваше описание подразумевает, что вы пытаетесь работать без GIL. Если ваш реальный код имеет
with gil:
/with nogil:
in, то это вполне может работать по-другому. Возможно, попробуйте убедиться, что вы работаете с большимиnogil
блоками, а не с множеством маленьких блоков.
Ответ №1:
Проблема заключается в strtod
том, что он не оптимизирован в VS2008. По-видимому, он внутренне вычисляет длину входной строки при каждом ее вызове, и если вы вызываете ее с длинной строкой, это значительно замедлит ваш код. Чтобы обойти это, вам нужно написать оболочку strtod
для одновременного использования только небольших буферов (см. Ссылку выше для одного примера того, как это сделать) или написать свою собственную strtod
функцию.