Каков самый быстрый (доступ к) структурно подобный объект в Python?
Я оптимизирую некоторый код, чье основное узкое место работает и обращается к очень большому списку объектов типа struct. В настоящее время я использую namedtuples для удобочитаемости. Но некоторые быстрые бенчмаркинга с использованием "timeit" показывают, что это действительно неправильный путь, когда производительность является фактором:
Именованный кортеж с a, b, c:
>>> timeit("z = a.c", "from __main__ import a")
0.38655471766332994
Класс с использованием __slots__
, с a, b, c:
>>> timeit("z = b.c", "from __main__ import b")
0.14527461047146062
Словарь с ключами a, b, c:
>>> timeit("z = c['c']", "from __main__ import c")
0.11588272541098377
Кортеж с тремя значениями, используя постоянный ключ:
>>> timeit("z = d[2]", "from __main__ import d")
0.11106188992948773
Список с тремя значениями, используя постоянный ключ:
>>> timeit("z = e[2]", "from __main__ import e")
0.086038238242508669
Кортеж с тремя значениями, используя локальный ключ:
>>> timeit("z = d[key]", "from __main__ import d, key")
0.11187358437882722
Список с тремя значениями, используя локальный ключ:
>>> timeit("z = e[key]", "from __main__ import e, key")
0.088604143037173344
Прежде всего, есть ли что-нибудь об этих небольших тегах timeit
, которые сделают их недействительными? Я бегал каждый раз, чтобы убедиться, что случайное системное событие не отбросило их, и результаты были почти идентичными.
Казалось бы, словари предлагают лучший баланс между производительностью и удобочитаемостью, при этом классы идут вторым. Это печально, так как для моих целей мне также нужен объект, похожий на последовательность; следовательно, мой выбор namedtuple.
Списки выполняются значительно быстрее, но постоянные ключи недоступны; Мне нужно создать кучу индексных констант, т.е. KEY_1 = 1, KEY_2 = 2 и т.д., Что также не идеально.
Я придерживаюсь этих вариантов, или есть альтернатива, которую я пропустил?
Ответы
Ответ 1
Следует иметь в виду, что namedtuples оптимизированы для доступа как кортежи. Если вы измените свой аксессор на a[2]
вместо a.c
, вы увидите аналогичную производительность для кортежей. Причина в том, что аксессоры имен эффективно переводят на вызовы self [idx], поэтому платите как индексирование, так и цену поиска имени.
Если ваш шаблон использования таков, что доступ по имени является общим, но доступ как кортеж не является, вы можете написать быстрый эквивалент namedtuple, который делает все наоборот: откладывает поиск индексов для доступа по имени. Однако тогда вы будете платить цену за индексные запросы. Например, быстрая реализация:
def makestruct(name, fields):
fields = fields.split()
import textwrap
template = textwrap.dedent("""\
class {name}(object):
__slots__ = {fields!r}
def __init__(self, {args}):
{self_fields} = {args}
def __getitem__(self, idx):
return getattr(self, fields[idx])
""").format(
name=name,
fields=fields,
args=','.join(fields),
self_fields=','.join('self.' + f for f in fields))
d = {'fields': fields}
exec template in d
return d[name]
Но время очень хорошее, если __getitem__
должно быть вызвано:
namedtuple.a : 0.473686933517
namedtuple[0] : 0.180409193039
struct.a : 0.180846214294
struct[0] : 1.32191514969
т.е. такая же производительность, как класс __slots__
для доступа к атрибутам (неудивительно - что это такое), но огромные штрафы из-за двойного поиска в доступе на основе индексов. (Примечательно, что __slots__
на самом деле не помогает значительно по скорости. Это экономит память, но время доступа примерно такое же без них.)
Один третий вариант заключается в дублировании данных, например. подкласс из списка и сохранить значения как в атрибутах, так и в listdata. Однако вы фактически не получаете производительность, эквивалентную спискам. Там большая скорость попадала только в том случае, когда она была подклассифицирована (приносит проверки на перегрузку с чистым питоном). Таким образом, struct [0] по-прежнему занимает около 0,5 с (по сравнению с 0,18 для сырого списка), и вы удваиваете использование памяти, поэтому это может не стоить.
Ответ 2
Этот вопрос довольно старый (интернет-время), поэтому я решил попробовать повторить ваш тест сегодня, как с обычным CPython (2.7.6), так и с pypy (2.2.1) и посмотреть, как различные методы в сравнении. (Я также добавил в индексный поиск для названного кортежа.)
Это немного микро-бенчмарк, поэтому YMMV, но pypy, похоже, ускорил доступ к имени tuple в 30 раз по сравнению с CPython (в то время как доступ к словарю был ускорен только в 3 раза).
from collections import namedtuple
STest = namedtuple("TEST", "a b c")
a = STest(a=1,b=2,c=3)
class Test(object):
__slots__ = ["a","b","c"]
a=1
b=2
c=3
b = Test()
c = {'a':1, 'b':2, 'c':3}
d = (1,2,3)
e = [1,2,3]
f = (1,2,3)
g = [1,2,3]
key = 2
if __name__ == '__main__':
from timeit import timeit
print("Named tuple with a, b, c:")
print(timeit("z = a.c", "from __main__ import a"))
print("Named tuple, using index:")
print(timeit("z = a[2]", "from __main__ import a"))
print("Class using __slots__, with a, b, c:")
print(timeit("z = b.c", "from __main__ import b"))
print("Dictionary with keys a, b, c:")
print(timeit("z = c['c']", "from __main__ import c"))
print("Tuple with three values, using a constant key:")
print(timeit("z = d[2]", "from __main__ import d"))
print("List with three values, using a constant key:")
print(timeit("z = e[2]", "from __main__ import e"))
print("Tuple with three values, using a local key:")
print(timeit("z = d[key]", "from __main__ import d, key"))
print("List with three values, using a local key:")
print(timeit("z = e[key]", "from __main__ import e, key"))
Результаты Python:
Named tuple with a, b, c:
0.124072679784
Named tuple, using index:
0.0447055962367
Class using __slots__, with a, b, c:
0.0409136944224
Dictionary with keys a, b, c:
0.0412045334915
Tuple with three values, using a constant key:
0.0449477955531
List with three values, using a constant key:
0.0331083467148
Tuple with three values, using a local key:
0.0453569025139
List with three values, using a local key:
0.033030056702
Результаты PyPy:
Named tuple with a, b, c:
0.00444889068604
Named tuple, using index:
0.00265598297119
Class using __slots__, with a, b, c:
0.00208616256714
Dictionary with keys a, b, c:
0.013897895813
Tuple with three values, using a constant key:
0.00275301933289
List with three values, using a constant key:
0.002760887146
Tuple with three values, using a local key:
0.002769947052
List with three values, using a local key:
0.00278806686401
Ответ 3
Несколько моментов и идей:
1) Вы время от времени выбираете один и тот же индекс много раз подряд. Ваша фактическая программа, вероятно, использует случайный или линейный доступ, который будет иметь другое поведение. В частности, будет больше промахов кэша процессора. Вы можете получить несколько разные результаты, используя свою фактическую программу.
2) OrderedDictionary написан как обертка вокруг dict
, ergo будет медленнее, чем dict
. Это не решение.
3) Пробовали ли вы как новые, так и старые классы? (классы нового стиля наследуют от object
, классы старого стиля не работают)
4) Вы пробовали использовать psyco или Без нагрузки Ласточка?
5) Создает ли ваш внутренний цикл изменение данных или просто доступ к нему? Возможно, можно было бы преобразовать данные в наиболее эффективную возможную форму перед входом в цикл, но использовать наиболее удобную форму в другом месте программы.
Ответ 4
У меня возникнет соблазн либо (а) изобрести какое-то специфическое кэширование рабочей нагрузки, и выгрузите хранение и извлечение моих данных в memcachedb-подобный процесс, чтобы улучшить масштабируемость, а не только производительность, или (б) переписать как C с собственным хранилищем данных. Возможно, тип упорядоченного словаря.
Вы можете начать с этого:
http://www.xs4all.nl/~anthon/Python/ordereddict/
Ответ 5
Эта проблема может скоро устареть. Очевидно, что CPython dev значительно улучшил производительность доступа к именованным значениям кортежей по имени атрибута. Изменения планируется выпустить в Python 3.8, ближе к концу октября 2019 года.
Смотрите: https://bugs.python.org/issue32492 и https://github.com/python/cpython/pull/10495.
Ответ 6
Вы можете сделать свою последовательность классов, например, добавив методы __iter__
и __getitem__
, чтобы сделать их такими же, как (индексируемые и итерабельные.)
Будет ли работать OrderedDict
? Доступно несколько реализаций, и он включен в модуль Python31 collections
.