Страницы

Поиск по вопросам

среда, 10 октября 2018 г.

Почему один способ проверки чисел в списке медленнее второго?

import time
lst = [-3, 3, 7, 0, -10, 23, -9, -8, -5, -10, 9, 3, -2, 8, -3, 6, -1, 0, 10, -1, -6, -6, 10, -7, 3, 8, 0, 7, 1, 5, -3, -6, 4, 6, -6, -4, -3, 10, 10, -5, -7, 0, -4, -8, 2, 9, 0, -10, -3, 3, -4, 9, -7, -8, 0, -1, 1, 7, 2, -1, 3, 0, 9, -9, 4, 7, 6, 10, 8, -6, 3, 1, 1, 9, -8, -8, 2, 4, 10, 1, 5, -1, -1, 5, -9, 9, -3, 3, 0, -6, 2, 5, 10, 10, 5, -6, -10, -2, -9, 'СТРОКА']
start1 = time.clock() a = all(isinstance(item, (int, float, complex)) for item in lst) finish1 = time.clock() print("Результат: {}, время:{:.2}ms".format(a, (finish1 - start1) * 1000))
start2 = time.clock() def is_numbers(iterable): for item in iterable: if not isinstance(item, (int, float, complex)): return False return True b = is_numbers(lst) finish2 = time.clock() print("Результат: {}, время:{:.2}ms".format(b, (finish2 - start2) * 1000))
Результат:
>>> ====== RESTART: /home/dzmitry/adasdsadasdasdsad.py ====== Результат: False, время:0.081ms Результат: False, время:0.042ms >>> ====== RESTART: /home/dzmitry/adasdsadasdasdsad.py ====== Результат: False, время:0.085ms Результат: False, время:0.043ms >>>
Функция all() эквивалентна
def all(iterable): for element in iterable: if not element: return False return True
Получается, что первый и второй способ равнозначны, за исключением использования генераторного выражения. Почему первый способ медленнее в 1.5-2 раза?


Ответ

Дело не в проверке типов—разница во времени сохраняется даже если заменить isinstance(item, ..) вызов на f(item), где f функция ничего не делает, а просто возвращает True значение: f = lambda item: True (чтобы all() весь lst список просматривала без раннего выхода). Более того, разница остаётся, если вообще вызов функции убрать:
In [1]: %timeit all(True for _ in range(1000)) 10000 loops, best of 3: 69.7 µs per loop
In [2]: def loop(): ...: for _ in range(1000): ...: if not True: ...: return False ...: return True ...:
In [3]: %timeit loop() 10000 loops, best of 3: 30.3 µs per loop
Явный for-цикл может быть эффективней генератора в CPython (результат также может зависеть от платформы).
all_(), реализованная вручную, слегка медленнее встроенной версии all(), которая реализует похожий цикл в C
def all_(iterable): for item in iterable: if not item: return False return True
то есть важно что генератор используется, а не то как all() реализована:
In [4]: %timeit all_(range(1, 1000)) 10000 loops, best of 3: 41.1 µs per loop In [5]: %timeit all_(i for i in range(1, 1000)) 10000 loops, best of 3: 93.9 µs per loop

Даже генератор списков (listcomp—списковое включение) может быть эффективней эквивалентного genexpr (хотя в Jython это с точностью до наоборот):
In [6]: %timeit [True for _ in range(1000)] 10000 loops, best of 3: 44.4 µs per loop
In [7]: %timeit list(True for _ in range(1000)) 10000 loops, best of 3: 83.2 µs per loop
Хотя между этими выражениями не должно быть особой разницы в Питоне 3: Why this list comprehension is faster than equivalent generator expression?
Байт-код ничего особо подозрительного не показывает:
>>> import dis >>> dis.dis(lambda: [True for _ in range(1000000)]) 1 0 LOAD_CONST 1 ( at 0x610efc445eea, file "", line 1>) 3 LOAD_CONST 2 ('..') 6 MAKE_FUNCTION 0 9 LOAD_GLOBAL 0 (range) 12 LOAD_CONST 3 (1000000) 15 CALL_FUNCTION 1 (1 positional, 0 keyword pair) 18 GET_ITER 19 CALL_FUNCTION 1 (1 positional, 0 keyword pair) 22 RETURN_VALUE >>> dis.dis(lambda: list(True for _ in range(1000000))) 1 0 LOAD_GLOBAL 0 (list) 3 LOAD_CONST 1 ( at 0x610efc4bc0b2, file "", line 1>) 6 LOAD_CONST 2 ('..') 9 MAKE_FUNCTION 0 12 LOAD_GLOBAL 1 (range) 15 LOAD_CONST 3 (1000000) 18 CALL_FUNCTION 1 (1 positional, 0 keyword pair) 21 GET_ITER 22 CALL_FUNCTION 1 (1 positional, 0 keyword pair) 25 CALL_FUNCTION 1 (1 positional, 0 keyword pair) 28 RETURN_VALUE
Первый фрагмент: создаёт функцию из listcomp объекта и вызывает её с результатом iter(range(1000000)). Второй фрагмент: создаёт функцию из genexpr и вызывает её с тем же аргументом, дополнительно вызывается list() функция с результатом.
listcomp реализуется достаточно прямолинейно:
>>> code = compile('[True for _ in range(1000000)]', '', 'eval') >>> code.co_consts[0] at 0x610efc445f2c, file "", line 1> >>> dis.dis(code.co_consts[0]) 1 0 BUILD_LIST 0 3 LOAD_FAST 0 (.0) >> 6 FOR_ITER 12 (to 21) 9 STORE_FAST 1 (_) 12 LOAD_CONST 0 (True) 15 LIST_APPEND 2 18 JUMP_ABSOLUTE 6 >> 21 RETURN_VALUE
Что близко к:
def listcomp(it): L = [] for _ in it: L.append(True) return L
где it = iter(range(1000000)), полученный ранее.
genexpr выглядит похоже:
>>> code = compile('list(True for _ in range(1000000))', '', 'eval') >>> code.co_consts[0] at 0x610efc44145d, file "", line 1> >>> dis.dis(code.co_consts[0]) 1 0 LOAD_FAST 0 (.0) >> 3 FOR_ITER 11 (to 17) 6 STORE_FAST 1 (_) 9 LOAD_CONST 0 (True) 12 YIELD_VALUE 13 POP_TOP 14 JUMP_ABSOLUTE 3 >> 17 LOAD_CONST 1 (None) 20 RETURN_VALUE
Что близко к:
def genexpr(it): for _ in it: yield True
созданный генератор (объект) передаётся во встроеннуюlist() функцию, которая реализована вызовом listextend(), что выполняет код близкий к listcomp(it), приведённому выше. Снова разница в производительности не в использовании list(), а в том что ей передан генератор (list(range(1000)) и list(iter(range(1000)) могут быть быстрее как list(i for i in range(1000)) так и [True for _ in range(1000)]).
Уже не так удивительно, что даже list([True for _ in range(1000)]), который казалось бы выполняет лишнюю работу, может быть быстрее list(True for _ in range(1000)) (CPython/Pypy 2/3, Ubuntu).
Не стоит слишком увлекаться микрооптимизациями—пишите самый простой и понятный код, который решает поставленную задачу. Не изменяйте код, пока измерения не показали, что это необходимо в вашем конкретном случае.

Комментариев нет:

Отправить комментарий