Сравнение производительности Python для создания множеств - set() и {} литерал

Обсуждение этого вопроса заставило меня задуматься, поэтому я решил запустить несколько тестов и сравнить время создания set((x,y,z)) с {x,y,z} для создания наборов в Python (I '). используя Python 3.7).

Я сравнил два метода, используя time и timeit. Оба соответствовали * следующим результатам:

test1 = """
my_set1 = set((1, 2, 3))
"""
print(timeit(test1))

Результат: 0.30240735499999993

test2 = """
my_set2 = {1,2,3}
"""
print(timeit(test2))

Результат: 0.10771795900000003

Таким образом, второй метод был почти в 3 раза быстрее, чем первый. Это было довольно неожиданное различие для меня. Что происходит внутри, чтобы таким образом оптимизировать производительность литерала набора над методом set()? Что было бы целесообразно для каких случаев?

* Примечание: я показываю только результаты тестов timeit так как они усредняются по многим образцам и, следовательно, возможно, более надежны, но результаты при тестировании со time показали схожие различия в обоих случаях.


Редактировать: я знаю об этом похожем вопросе, и хотя он отвечает на некоторые аспекты моего первоначального вопроса, он не охватил все это. Множества не были рассмотрены в этом вопросе, и поскольку пустые множества не имеют буквального синтаксиса в python, мне было любопытно, как (если вообще) создание множеств с использованием литерала будет отличаться от использования метода set(). Кроме того, мне было интересно, как происходит обработка параметра tuple в set((x,y,z) за кулисами и как это может повлиять на время выполнения. Отличный ответ от coldspeed помог прояснить ситуацию.

Ответ 1

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

test1 = """
def foo1():
     my_set1 = set((1, 2, 3))
foo1()
"""    
timeit(test1)
# 0.48808742000255734

test2 = """
def foo2():
    my_set2 = {1,2,3}
foo2()
"""    
timeit(test2)
# 0.3064506609807722

Теперь причина разницы во времени заключается в том, что set() - это вызов функции, требующий поиска в таблице символов, тогда как конструкция набора {...} является артефактом синтаксиса и намного быстрее.

Разница очевидна при просмотре разобранного байтового кода.

import dis

dis.dis("set((1, 2, 3))")
  1           0 LOAD_NAME                0 (set)
              2 LOAD_CONST               3 ((1, 2, 3))
              4 CALL_FUNCTION            1
              6 RETURN_VALUE

dis.dis("{1, 2, 3}")
  1           0 LOAD_CONST               0 (1)
              2 LOAD_CONST               1 (2)
              4 LOAD_CONST               2 (3)
              6 BUILD_SET                3
              8 RETURN_VALUE

В первом случае вызов функции выполняется инструкцией CALL_FUNCTION для кортежа (1, 2, 3) (которая также имеет свои собственные издержки, хотя и незначительные - она загружается как константа через LOAD_CONST), тогда как во втором инструкция - это просто вызов BUILD_SET, который более эффективен.

Re: ваш вопрос относительно времени, необходимого для построения кортежа, мы видим, что это на самом деле незначительно:

timeit("""(1, 2, 3)""")
# 0.01858693000394851

timeit("""{1, 2, 3}""")
# 0.11971827200613916

Кортежи являются неизменяемыми, поэтому компилятор оптимизирует эту операцию, загружая ее как константу - это называется постоянным свертыванием (вы можете ясно увидеть это из инструкции LOAD_CONST выше), поэтому время, затрачиваемое на это, незначительно. С наборами это не видно, поскольку они изменчивы (спасибо @user2357112 за указание на это).


Для больших последовательностей мы видим похожее поведение. Синтаксис {..} работает быстрее при построении множеств с использованием определений множеств, в отличие от set() который должен строить множество из генератора.

timeit("""set(i for i in range(10000))""", number=1000)
# 0.9775058150407858

timeit("""{i for i in range(10000)}""", number=1000)
# 0.5508635920123197

Для справки, вы также можете использовать повторяемую распаковку на более поздних версиях:

timeit("""{*range(10000)}""", number=1000)
# 0.7462548640323803

Интересно, однако, что set() быстрее, когда вызывается непосредственно в range:

timeit("""set(range(10000))""", number=1000)
# 0.3746800610097125

Это происходит быстрее, чем заданная конструкция. Вы увидите похожее поведение для других последовательностей (таких как list s).

Моя рекомендация заключается в использовании {...} понимания набора при создании литералов набора и в качестве альтернативы передаче понимания генератора функции set(); и вместо этого используйте set() для преобразования существующей последовательности/итерируемой в набор.