Почему __setitem__ намного быстрее, чем эквивалентный "нормальный" метод для классов cdef? - PullRequest
0 голосов
/ 29 ноября 2018

Похоже, что для cdef-классов Cython использование специальных методов класса иногда быстрее, чем идентичный "обычный" метод, например __setitem__ в 3 раза быстрее, чем setitem:

%%cython
cdef class CyA:
    def __setitem__(self, index, val):
        pass
    def setitem(self, index, val):
        pass

и теперь:

cy_a=CyA()
%timeit cy_a[0]=3              # 32.4 ns ± 0.195 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
%timeit cy_a.setitem(0,3)      # 97.5 ns ± 0.389 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

Это не «нормальное» поведение для Python, для которого специальные функции даже несколько медленнее (и явно медленнее, чем эквивалентные Cython):

class PyA:
    def __setitem__(self, index, val):
        pass
    def setitem(self, index, val):
        pass

py_a=PyA()
%timeit py_a[0]=3           # 198 ns ± 2.51 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit py_a.setitem(0,3)   # 123 ns ± 0.619 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

и это не относится к Cython для всех специальных функций:

%%cython
cdef class CyA:
    ...
    def __len__(self):
        return 1
    def len(self):
        return 1

, что приводит к:

cy_a=CyA()
%timeit len(cy_a)    #  59.6 ns ± 0.233 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
%timeit cy_a.len()   #  66.5 ns ± 0.326 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

то есть почти идентичным временам выполнения.

Почему __setitem__(...) намного быстрее, чем setitem(...) в классе cdef, даже если оба они цитонизированы?

Ответы [ 2 ]

0 голосов
/ 30 ноября 2018

@ Ответ DavidW бьет по голове, вот еще несколько экспериментов и подробностей, которые подтверждают его ответ.

Вызов специальной функции, которая возвращает «Нет», быстро, независимо от того, сколькоаргументы:

%%cython
cdef class CyA:
# special functions
    def __setitem__(self, index, val):
        pass
    def __getitem__(self, index):
        pass

и теперь

a=CyA()  
%timeit a[0]    # 29.8 ns ± 1.9 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
%timeit a[0]=3  # 29.3 ns ± 0.942 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

Сигнатура функций известна, нет необходимости строить *args, **kwargs.Поиск в слоте выполняется так быстро, как только может.

Затраты на вызов нормальной функции зависят от количества аргументов :

%%cython
cdef class CyA:
...
# normal functions:   
    def fun0(self):
        pass    
    def fun1(self, arg):
        pass    
    def fun2(self, arg1, arg2):
        pass

и теперь:

a=CyA()  
...
%timeit a.fun0()     # 64.1 ns ± 2.49 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)     
%timeit a.fun1(1)    # 67.6 ns ± 0.785 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each) 
%timeit a.fun2(2,3)  # 94.7 ns ± 1.04 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

Издержки больше, чем для вызова методов из слотов, но также больше, если есть (как минимум) два аргумента (без учета self): 65ns против 95ns.

Причина: cython-методы могут быть одного из следующих типов

  1. METH_NOARGS - только с аргументом self
  2. METH_O - только с self + один аргумент
  3. METH_VARARGS|METH_KEYWORDS - с произвольным числом элементов

Метод fun2 относится к третьему типу, поэтому для его вызова Python должен создать список *args, что приводит к дополнительным издержкам.

** Возврат из специального метода может иметь больше издержек, так какиз обычного метода ":

%%cython
cdef class CyA:
...
def __len__(self):
    return 1  # return 1000 would be slightly slower
def len(self):
    return 1

приводит к:

a=CyA()
...  
%timeit len(a)   # 52.1 ns ± 1.57 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
%timeit a.len()  # 57.3 ns ± 1.39 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

Как указывалось @DavidW, для __len__ в каждом вызове" новое "int-object должен быть сконструирован из возвращенного Py_ssize_t (в случае 1 это целое число из пула, поэтому он на самом деле не создан - но это было в случае больших чисел).

Этоэто не относится к len(): для этой специальной реализации Cython инициализирует глобальный объект, который возвращается len() - увеличение счетчика ссылок не требует больших затрат (по сравнению с созданием целого числа!).

Таким образом, __len__ и len() работают примерно одинаково быстро, но время тратится на разные вещи (создание целочисленных и поисковых запросов).

0 голосов
/ 29 ноября 2018

Существует довольно много накладных расходов для вызова общего метода Python - Python ищет соответствующий атрибут (поиск по словарю), гарантирует, что атрибут является вызываемым объектом, и, как только он вызывается, обрабатывает результат.Эти издержки также применяются к общим def функциям для cdef классов (единственное отличие состоит в том, что реализация метода определена в C).

Однако специальные методы в классах C / Cython могут бытьоптимизируется следующим образом:

Скорость поиска

В качестве ярлыка, PyTypeObject в Python C API определяет ряд различных «слотов» - указателей на прямые функции для специальных методов.Для __setitem__ на самом деле доступно два: PyMappingMethods.mp_ass_subscript, что соответствует общему вызову "mapping", и PySequenceMethods.sq_ass_item, который позволяет вам использовать int в качестве индексатора напрямую исоответствует функции C API PySequence_SetItem.

Для cdef class Cython, похоже, генерирует только первый (универсальный), поэтому ускорение происходит не за счет передачи Cint напрямую.Cython не заполняет эти слоты при генерации не cdef класса.

Преимущество этого состоит в том, что (для класса C / Cython) нахождение функции __setitem__ включает только парупроверка указателя на NULL с последующим вызовом функции C .Это также относится к __len__, который также определяется слотами в PyTypeObject

В отличие от

  • для класса Python, вызывающего __setitem__, вместо него использует реализацию по умолчанию , которая выполняет поиск в словаре для строки "__setitem__".

  • Для cdef или класса Python, вызывающего не специальный defфункция, атрибут ищется из словаря класса / экземпляра (который медленнее)

Обратите внимание, что если бы setitem обычная функция была определена в cdef class как *Вместо 1054 * (и вызывается из Cython), затем Cython реализует свой собственный механизм для быстрого поиска.

Эффективность вызова

Найдя атрибут, он должен быть вызван.Если специальные функции были получены из PyTypeObject (например, __setitem__ и __len__ для cdef class), они являются просто указателями на функции C и поэтому могут вызываться напрямую.

Для любого другого случаяPyObject, полученный из поиска атрибутов, должен быть оценен, чтобы определить, вызывается ли он, а затем вызван.

Обработка возврата

Когда __setitem__ вызывается из PyTypeObject в качестве специальной функции возвратаvalue - это int, которое просто используется как флаг ошибки.Подсчет ссылок или обработка объектов Python не требуются.

Когда __len__ вызывается из PyTypeObject в качестве специальной функции, тип возвращаемого значения - Py_ssize_t, который должен быть преобразован в объект Python.и затем уничтожается, когда больше не требуется.

Для обычных функций (например, setitem, вызываемых из класса Python или Cython, или __setitem__, определенных в классе Python), возвращаемое значение равно PyObject*,который должен быть соответствующим образом подсчитан / уничтожен.


Таким образом, на самом деле различие заключается в ярлыках при поиске и вызове функции, а не в том, является ли содержимое функции Cythonized.

...