Подтвердить что ты не робот

Точка безубыточности оптимизации: многократно повторяйте набор или конвертируйте в список в первую очередь?

Здесь я всегда задавался вопросом. Я поставил вопрос для Python, но я бы также приветствовал ответы, которые касаются стандартных библиотек в Java и С++.

Скажем, у вас есть список Python с именем "my_list", и вы хотите перебрать его уникальные элементы. Существует два естественных подхода:

#iterate over set
for x in set(my_list):
    do_something(x)

или

#list to set to list
for x in list(set(my_list)):
    do_something(x)

Напряжение заключается в том, что итерирование по списку быстрее, чем повторение по набору, но для преобразования набора в список требуется время. Я предполагаю, что ответ на этот вопрос будет зависеть от ряда факторов, таких как:

  • Сколько раз нам нужно итерации?
  • Насколько велик исходный список?
  • Сколько повторений в исходном списке мы должны ожидать?

Итак, я предполагаю, что я ищу эмпирическое правило формы "Если в списке есть x много элементов, каждый элемент повторяется не более y раз, и вам нужно только повторить z раз, тогда вы должны перебирать множество, иначе вы должны преобразовать его в список."

4b9b3361

Ответ 1

Я ищу эмпирическое правило...

Правило большого пальца

Здесь лучшее правило для написания оптимального Python: используйте как можно меньше промежуточных шагов и избегайте материализации ненужных структур данных.

Применительно к этому вопросу: наборы являются итерабельными. Не конвертируйте их в другую структуру данных, чтобы перебирать их. Доверьте Python, чтобы узнать самый быстрый способ перебора множеств. Если бы быстрее преобразовать их в списки, Python сделает это.

Оптимизация:

Не пытайтесь оптимизировать преждевременно, добавляя сложность вашей программе. Если ваша программа занимает слишком много времени, профилируйте ее, а затем оптимизируйте узкие места. Если вы используете Python, вы, вероятно, больше беспокоитесь о времени разработки, чем о том, как долго ваша программа будет работать.

Демонстрация

В Python 2.7:

import collections
import timeit
blackhole = collections.deque(maxlen=0).extend
s = set(xrange(10000))

Мы видим, что при больших n проще:

>>> timeit.timeit(lambda: blackhole(s))
108.87403416633606
>>> timeit.timeit(lambda: blackhole(list(s)))
189.0135440826416

И для меньшего n выполняется такое же соотношение:

>>> s = set(xrange(10))
>>> timeit.timeit(lambda: blackhole(s))
0.2969839572906494
>>> timeit.timeit(lambda: blackhole(list(s)))
0.630713939666748

Да, перечислить итерацию быстрее, чем наборы (попробуйте на своем интерпретаторе Python):

l = list(s)
timeit.repeat(lambda: blackhole(l))

Но это не значит, что вы должны преобразовывать наборы в списки только для итерации.

Анализ безубыточности

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

Возможно, что, внося сложный вклад и принимая на себя больший риск ошибок из большего количества строк кода, вы можете получить хороший выигрыш. Этот анализ продемонстрирует точку безубыточности в этом вопросе. Я не знаю, сколько еще производительности вам нужно будет заплатить за большие риски и время разработки, но это скажет вам, в какой момент вы можете начать платить за них.:

import collections
import timeit
import pandas as pd

BLACKHOLE = collections.deque(maxlen=0).extend

SET = set(range(1000000))

def iterate(n, iterable):
    for _ in range(n):
        BLACKHOLE(iterable)

def list_iterate(n, iterable):
    l = list(iterable)
    for _ in range(n):
        BLACKHOLE(l)

columns = ('ConvertList', 'n', 'seconds')

def main():
    results = {c: [] for c in columns}

    for n in range(21):
        for fn in (iterate, list_iterate):
            time = min(timeit.repeat((lambda: fn(n, SET)), number=10))
            results['ConvertList'].append(fn.__name__)
            results['n'].append(n)
            results['seconds'].append(time)

    df = pd.DataFrame(results)
    df2 = df.pivot('n', 'ConvertList')
    df2.plot()
    import pylab
    pylab.show()

И похоже, что ваша точка безубыточности - 5 полных итераций. С 5 или менее в среднем, это не может иметь смысла делать это. Только в 5 или более вы начинаете компенсировать дополнительное время разработки, сложность (увеличение затрат на обслуживание) и риск от большего количества строк кода.

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

введите описание изображения здесь

Эти результаты были созданы с помощью Anaconda Python 2.7 с терминала Ubuntu 14.04. Вы можете получать разные результаты с различными реализациями и версиями.


Обеспокоенность

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

>>> s = set('abc')
>>> for e in s:
...     s.add(e + e.upper())
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: Set changed size during iteration

Если вы измените свой набор во время итерации по списку производных, вы не получите сообщение об ошибке, чтобы сообщить вам, что вы это сделали.

>>> for e in list(s):
...     s.add(e + e.upper())

Вот почему я также предложил вместо этого использовать фризонаты и кортежи. Это будет встроенная защита от семантически неправильного изменения ваших данных.

>>> s = frozenset('abc')
>>> t_s = tuple(s)
>>> for e in t_s:
...     s.add(e + e.upper())
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
AttributeError: 'frozenset' object has no attribute 'add'

В конце концов, вы должны доверять себе, чтобы получить правильный алгоритм. Часто мне говорят, что я дал хороший совет, когда предупреждаю новых пользователей Python об этих вещах. Они узнали, что это хороший совет, потому что они не слушали сначала, и обнаружили, что это создало ненужную сложность, сложности и возникающие проблемы. Но есть также такие вещи, как логическая правильность, что вы будете только виноваты, если не поправитесь. Минимизация вещей, которые могут пойти не так, - это преимущество, которое обычно стоит компромисс производительности. И снова, если производительность (а не правильность или скорость разработки) была главной проблемой при решении этого проекта, вы бы не использовали Python.