Каков самый быстрый (доступ к) структурно подобный объект в 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.