Каков самый быстрый (для доступа) структурный объект в Python?


я оптимизирую некоторый код, основным узким местом которого является запуск и доступ к очень большому списку структурно-подобных объектов. В настоящее время я использую 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 и т. д. что тоже не идеальный.

Я застрял с этими вариантами, или есть альтернатива, которую я пропустил?

5 67

5 ответов:

одна вещь, чтобы иметь в виду, что 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. Однако вы на самом деле не получаете эквивалентную по списку производительность. Существует большая скорость хита только в том, что подкласс (вводит проверки для перегрузок pure-python). Таким образом, struct [0] все еще занимает около 0,5 С (по сравнению с 0.18 для необработанного списка) в этом случае, и вы удваиваете использование памяти, так что это может не стоить того.

этот вопрос довольно старый (Интернет-время), поэтому я подумал, что попробую повторить ваш тест сегодня, как с обычным CPython (2.7.6), так и с pypy (2.2.1) и посмотреть, как сравниваются различные методы. (Я также добавил в индексированном поиске для именованного кортежа.)

Это немного микро-бенчмарк, поэтому YMMV, но pypy, казалось, ускорял доступ к именованному кортежу в 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

пара моментов и идей:

1) Вы синхронизируете доступ к одному и тому же индексу много раз подряд. Ваша фактическая программа, вероятно, использует случайный или линейный доступ, который будет иметь другое поведение. В частности, будет больше пропусков кэша процессора. Вы можете получить немного другие результаты, используя вашу фактическую программу.

2) OrderedDictionary пишется как обертка вокруг dict, следовательно, это будет медленнее, чем dict. Это не решение проблемы.

3) Вы пробовали как новый стиль, так и старый? (классы нового стиля наследуются от object; классы старого стиля не делают)

4) Вы пробовали использовать psyco или Без Груза Глотать?

5) ваш внутренний цикл, чтобы изменить данные или просто доступ к нему? Возможно, можно преобразовать данные в наиболее эффективную возможную форму перед входом в цикл, но использовать наиболее удобную форму в другом месте программы.

У меня возникнет соблазн либо (а) изобрести какое-то конкретное кэширование рабочей нагрузки и разгрузить хранение и извлечение моих данных в процесс, подобный memcachedb, чтобы улучшить масштабируемость, а не только производительность, или (б) переписать как расширение C, с собственным хранилищем данных. Возможно, тип упорядоченного словаря.

вы могли бы начать с этого: http://www.xs4all.nl/~anthon / Python / ordereddict/

вы можете сделать ваши классы последовательности, как путем добавления __iter__ и __getitem__ методы, чтобы сделать их последовательность как (индексируемые и итерационные.)

будет OrderedDict работы? Есть несколько реализаций доступны, и он включен в Python31 collections.