Код Cython выполняется в 125 раз медленнее при компиляции с python 2 против python 3

#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 функцию.