Почему явные вызовы магических методов медленнее, чем засахаренный синтаксис?

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

def __hash__(self):
    return self.foo.__hash__()

Однако при тестировании я обнаружил, что hash(self.foo) заметно быстрее. Любопытно, что я протестировал __eq__, __ne__ и другие магические сравнения только для того, чтобы обнаружить, что все работали быстрее, если я использовал слащавые формы (==, !=, < и т. д.). Почему это? Я предполагал, что засахаренная форма должна будет выполнять тот же вызов функции под капотом, но, возможно, это не так?

Результаты Timeit

Настройки: тонкие оболочки вокруг атрибута экземпляра, которые контролируют все сравнения.

Python 3.3.4 (v3.3.4:7ff62415e426, Feb 10 2014, 18:13:51) [MSC v.1600 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import timeit
>>> 
>>> sugar_setup = '''\
... import datetime
... class Thin(object):
...     def __init__(self, f):
...             self._foo = f
...     def __hash__(self):
...             return hash(self._foo)
...     def __eq__(self, other):
...             return self._foo == other._foo
...     def __ne__(self, other):
...             return self._foo != other._foo
...     def __lt__(self, other):
...             return self._foo < other._foo
...     def __gt__(self, other):
...             return self._foo > other._foo
... '''
>>> explicit_setup = '''\
... import datetime
... class Thin(object):
...     def __init__(self, f):
...             self._foo = f
...     def __hash__(self):
...             return self._foo.__hash__()
...     def __eq__(self, other):
...             return self._foo.__eq__(other._foo)
...     def __ne__(self, other):
...             return self._foo.__ne__(other._foo)
...     def __lt__(self, other):
...             return self._foo.__lt__(other._foo)
...     def __gt__(self, other):
...             return self._foo.__gt__(other._foo)
... '''

Тесты

Мой пользовательский объект обертывает datetime, поэтому я использовал его, но это не должно иметь никакого значения. Да, я создаю дату и время в тестах, поэтому, очевидно, есть некоторые связанные с этим накладные расходы, но эти накладные расходы постоянны от одного теста к другому, поэтому это не должно иметь значения. Для краткости я опустил тесты __ne__ и __gt__, но эти результаты были практически идентичны показанным здесь.

>>> test_hash = '''\
... for i in range(1, 1000):
...     hash(Thin(datetime.datetime.fromordinal(i)))
... '''
>>> test_eq = '''\
... for i in range(1, 1000):
...     a = Thin(datetime.datetime.fromordinal(i))
...     b = Thin(datetime.datetime.fromordinal(i+1))
...     a == a # True
...     a == b # False
... '''
>>> test_lt = '''\
... for i in range(1, 1000):
...     a = Thin(datetime.datetime.fromordinal(i))
...     b = Thin(datetime.datetime.fromordinal(i+1))
...     a < b # True
...     b < a # False
... '''

Полученные результаты

>>> min(timeit.repeat(test_hash, explicit_setup, number=1000, repeat=20))
1.0805227295846862
>>> min(timeit.repeat(test_hash, sugar_setup, number=1000, repeat=20))
1.0135617737162192
>>> min(timeit.repeat(test_eq, explicit_setup, number=1000, repeat=20))
2.349765956168767
>>> min(timeit.repeat(test_eq, sugar_setup, number=1000, repeat=20))
2.1486044757355103
>>> min(timeit.repeat(test_lt, explicit_setup, number=500, repeat=20))
1.156479287717275
>>> min(timeit.repeat(test_lt, sugar_setup, number=500, repeat=20))
1.0673696685109917
  • Hash:
    • Explicit: 1.0805227295846862
    • Подслащено: 1,0135617737162192
  • Equal:
    • Explicit: 2.349765956168767
    • Подслащено: 2.1486044757355103
  • Less Than:
    • Explicit: 1.156479287717275
    • Подслащено: 1,0673696685109917

person Henry Keiter    schedule 08.04.2014    source источник


Ответы (1)


Две причины:

  • Поиски API учитывают только тип. Они не смотрят на self.foo.__hash__, они ищут type(self.foo).__hash__. На один словарь меньше.

  • Поиск слота C выполняется быстрее, чем поиск атрибута в чистом Python (который будет использовать __getattribute__); вместо этого поиск объектов метода (включая привязку дескриптора) выполняется полностью на C, минуя __getattribute__.

Таким образом, вам придется кэшировать поиск type(self._foo).__hash__ локально, и даже тогда вызов будет не таким быстрым, как из кода C. Просто придерживайтесь стандартных библиотечных функций, если скорость в приоритете.

Другая причина избегать прямого вызова магических методов заключается в том, что операторы сравнения делают больше, чем просто вызывают один магический метод; у методов тоже есть отраженные версии; для x < y, если x.__lt__ не определено или x.__lt__(y) возвращает синглтон NotImplemented, также используется y.__gt__(x).

person Martijn Pieters    schedule 08.04.2014