Время выполнения для печати номеров и списка номеров - PullRequest
0 голосов
/ 24 августа 2018

Я пытаюсь напечатать числа от 1 до 1000.

print(*range(1,1001)) занимает больше времени, чем печать его списка print([*range(1,1001)]).

Любая причина, по которой печать списка происходит намного быстрее, чемпечать ряда чисел?

Ответы [ 3 ]

0 голосов
/ 24 августа 2018

Здесь можно задать две разные вещи.


Вторая будет быстрее только из-за таких вещей, как:

  • Вместо передачи одного аргументаиз 1000. (В последних версиях CPython это на самом деле не означает пропуск 1000 значений в стеке, но это означает, что вам нужно пройти через CALL_FUNCTION_EX вместо того, чтобы пройти через простой CALL_FUNCTION, и на самом деле его оптимизироватьбыстрый путь.)
  • Цикл по элементам списка внутри оптимизированного list.__repr__, а не с помощью общего цикла итератора.
  • Материал, который print выполняет между __str__ каждого элементанемного сложнее, чем то, что list.__repr__ делает между __repr__ каждого элемента (условно печатая локальную переменную, вместо того, чтобы всегда привязывать постоянную строку).

Это должно бытьвопрос микросекунд - достаточно для измерения , но недостаточно для уведомления .


Но второе будет также быстрее bпотому что он делает меньше вызовов ввода / вывода.Это может затмить все эти небольшие различия.И если ваш терминал работает медленно, например, Windows cmd или поддельный терминал IDLE, этого может быть достаточно легко заметить.


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

In [765]: def dummy(*args): pass    
In [766]: %timeit dummy(*range(1, 1001))
19 µs ± 71.1 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)    
In [767]: %timeit dummy([*range(1, 1001)])
13 µs ± 609 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

Итак, второй примерно на 50% быстрее только из-за передачи аргументов, но это всего лишь 6us.


Что, если он итерирует свои аргументы, фактически printделает?

In [768]: def dummy(*args):
     ...:     for _ in args: pass
In [769]: %timeit dummy(*range(1, 1001))
22.8 µs ± 1.31 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)    
In [770]: %timeit dummy([*range(1, 1001)])
13.1 µs ± 148 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

Теперь она приближается к 2: 1, но разница все равно составляет всего 9 единиц.

Конечно, я немного обманываю, потому что print - это Cфункция - но, в любом случае, *args превращается в кортеж, который они должны каким-то образом циклически повторять.


Что, если он также вызывает __str__ каждого из своих аргументов, дляболее справедливое сравнение?

In [776]: def dummy(*args):
     ...:     for arg in args: str(arg)
In [776]: %timeit dummy(*range(1, 1001))
185 µs ± 1.67 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)    
In [777]: %timeit dummy([*range(1, 1001)])
86.3 µs ± 826 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Даже пытаясь сделать вещи более справедливыми по отношению к первому, оно все равно примерно 2: 1.


Давайте на самом деле назовем print, но напечатаемк объекту файла, который просто выбрасывает свои входные данные:

In [747]: class Nully:
     ...:     def write(self, *args): pass
In [749]: null = Nully()    
In [750]: %timeit print(*range(1, 1001), file=null)
390 µs ± 7.44 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)   
In [751]: %timeit print([*range(1, 1001)], file=null)
88.4 µs ± 2.35 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Теперь второй примерно в 4 раза быстрее, но мы все еще говорим лишь доли миллисекунды.


Теперь давайте попробуем подключить фактический ввод-вывод, но к нулевому устройству:

In [745]: %timeit with open(os.devnull, 'w') as null: print(*range(1, 1001), file=null)
436 µs ± 13.3 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)    
In [746]: %timeit with open(os.devnull, 'w') as null: print([*range(1, 1001)], file=null)
140 µs ± 1.74 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Они оба замедлились примерно на одинаковую небольшую величину.


Теперь давайте попробуем записать в файл, который занимает много времени - скажем, 10 мс - для каждой записи:

In [767]: class Slowy:
     ...:     def write(self, *args): time.sleep(0.01)
In [768]: null = Slowy()
In [770]: %timeit print(*range(1, 1001), file=null)
26.8 s ± 15.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [771]: %timeit print([*range(1, 1001)], file=null)
28.2 ms ± 39.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Первоначальная разница в 300us, вероятно, все еще существует, но кого это волнует?Здесь имеет значение разница в 3 порядка на 27 секунд, вызванная 1000 операций записи вместо 1.

Конечно, даже cmd.exe и IDLE не , что медленно.Но они довольно медленные.

Итак, я думаю, что последняя часть - это то, о чем вы спрашиваете.


Фактически, из комментариев, добавленных позже:

Это заметная задержка человека.

timeit.timeit("print(*range(1,1001))",number=1) => 10 s 
timeit.timeit("print([*range(1,1001)])",number=1) => 95 ms

Кстати, я использую Windows и запускаю этот код на Python IDLE.

Так что я ошибся: IDLE равно почти точно так медленно.(Ничего себе!)

0 голосов
/ 24 августа 2018

Печать чисел приведет к получению одного значения за раз. Печать списка приведет к выводу всего списка за раз.

0 голосов
/ 24 августа 2018

Вы, похоже, правы, это вывод на моей машине (Mac OSX)

import timeit

# 1
timeit.timeit('print(*range(1,1001))',number=10000)
7.32013652799651

# 2
timeit.timeit('print([*range(1,1001)])',number=10000)
3.6037830549757928

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

Просмотр вывода dis()

 dis.dis('print(*range(1,10))')

          0 LOAD_NAME                0 (print)
          2 LOAD_NAME                1 (range)
          4 LOAD_CONST               0 (1)
          6 LOAD_CONST               1 (10)
          8 CALL_FUNCTION            2
         10 CALL_FUNCTION_EX         0
         12 RETURN_VALUE


 dis.dis('print([*range(1,10)])')
          0 LOAD_NAME                0 (print)
          2 LOAD_NAME                1 (range)
          4 LOAD_CONST               0 (1)
          6 LOAD_CONST               1 (10)
          8 CALL_FUNCTION            2
         10 BUILD_LIST_UNPACK        1
         12 CALL_FUNCTION            1
         14 RETURN_VALUE

можно видеть, что во втором случае элементы списка объединяются (BUILD_LIST_UNPACK) перед вызовом функции печати. ​​

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...