Що є найшвидшим (доступним) структурним об'єктом у Python?


77

Я оптимізую деякий код, головне вузьке місце якого проходить і отримую доступ до дуже великого списку структурних об'єктів. В даний час я використовую 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тестах щось, що зробило б їх недійсними? Я запускав кожну кілька разів, щоб переконатися, що жодна випадкова системна подія не відкинула їх, і результати були майже однаковими.

Здається, словники пропонують найкращий баланс між продуктивністю та читабельністю, а заняття посідають друге місце. Це прикро, оскільки для моїх цілей мені також потрібен об’єкт, схожий на послідовність; звідси мій вибір іменованого.

Списки значно швидші, але постійні ключі неможливо підтримати; Мені довелося б створити купу констант індексу, тобто KEY_1 = 1, KEY_2 = 2 тощо, що також не є ідеальним.

Я застряг у цьому виборі, чи є альтернатива, яку я пропустив?


1
Якщо продуктивність є таким пріоритетом, чому б не використовувати С?
Skilldrick

10
@Skilldrick: Це лише невелика частина великої програми, яка отримує користь від написання на Python. Переписати цю частину як розширення С - варіант, але дещо небажаний, оскільки інший код також торкається даних, дещо ускладнюючи ситуацію. Продуктивність дуже важлива, але не , що важливо; Я був би дуже задоволений покращенням у чотири рази, пропонованим списками, якби не знижена ремонтопридатність. Я просто шукаю інші варіанти, перш ніж вирішити, яким шляхом піти.
DNS

1
@Warren P: Так; Я не передчасно оптимізую. Це дуже щільний цикл, в якому просто доступ до конструкцій є значною частиною роботи. Це найповільніший цикл програми, що залишився. Навіть незначне вдосконалення може скоротити секунду чи дві від реального часу роботи. Оскільки все це повторюється, це додає.
DNS

1
Також розгляньте спробу pypy. З pypy я не отримав жодної різниці у продуктивності між випадками.
Thomas Ahle

2
numpy має певні структури, і в деяких випадках може дати кращу продуктивність, ніж C. docs.scipy.org/doc/numpy/user/basics.rec.html Я не пробував цього та YMMV!
Sam Watkins

Відповіді:


55

Майте на увазі одне, що названі кортежі оптимізовані для доступу як кортежі. Якщо ви заміните аксесуар на a[2]замість a.c, ви побачите ефективність, подібну до кортежів. Причина полягає в тому, що засоби доступу до імен ефективно перетворюються на виклики до себе [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__насправді це не надто сприяє швидкості. Це економить пам’ять, але без них час доступу приблизно однаковий.)

Третім варіантом було б дублювання даних, наприклад. підклас зі списку та зберігає значення як в атрибутах, так і в списку даних. Однак ви насправді не отримуєте еквівалентну списку продуктивність. Велика швидкість потрапляє лише в підкласифікацію (введення перевірок на наявність перевантажень чистого пітона). Таким чином, struct [0] у цьому випадку все ще займає близько 0,5 с (порівняно з 0,18 для необробленого списку), і ви вдвічі більше використовуєте пам’ять, тому це може бути не варто.


3
Обережно з цим рецептом, де поля можуть містити дані, введені користувачем, - exec на полях може запускати довільний код. Інакше супер круто.
Майкл Скотт Катберт,

13
Чи не глупо, що тоді доступ за іменем є повільнішим, ніж доступ за індексом для namedtuples? Якщо реалізувати NAMEDtuple, чому б я оптимізував для доступу за допомогою індексу?
Ротареті,

44

Це запитання досить давнє (час в Інтернеті), тому я подумав спробувати дублювати ваш тест сьогодні як із звичайним 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

2
цікаво, що з pypy найгіршим є словник.
РоменЛ.

6

Ця проблема може застаріти незабаром. CPython dev, очевидно, зробив значні поліпшення продуктивності доступу до іменованих значень кортежу за іменем атрибута. Зміни планується випустити в Python 3.8 , ближче до кінця жовтня 2019 року.

Див .: https://bugs.python.org/issue32492 та https://github.com/python/cpython/pull/10495 .


1
Дякую за інформацію! Дійсно, цитата з docs.python.org/3/whatsnew/3.8.html : " Пришвидшені пошуки полів у колекціях.namedtuple (). Вони тепер більш ніж у два рази швидші, що робить їх найшвидшою формою пошуку змінних екземплярів у Python ".
Ісмаель Ель Атіфі

3

Пара моментів та ідей:

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

  2. OrderedDictionary пишеться як обгортка навколо dict, ergo це буде повільніше ніж dict. Це не рішення.

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

  4. Ви пробували використовувати Psyco або Unladen Swallow ? (Оновлення 2020 року - ці два проекти мертві)

  5. Чи має ваш внутрішній цикл змінити дані або просто отримати до них доступ? Можливо, можливо перетворити дані в максимально ефективну форму перед входом у цикл, але скористайтеся найбільш зручною формою в іншому місці програми.


1

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

Ви можете почати з цього: http://www.xs4all.nl/~anthon/Python/ordereddict/


-1

Ви можете зробити свої послідовності класів подібними, додавши __iter__, і __getitem__методи, щоб зробити їх послідовними як (індексуються та ітерабельні).

Працював би OrderedDict? Доступно декілька реалізацій, і це включено до collectionsмодуля Python31 .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.