Дополнительные издержки при вызове lambda n: n
столько раз действительно просто так дороги.
In [17]: key = lambda n: n
In [18]: x = [random() for _ in range(1234567)]
In [19]: %timeit nlargest(10, x)
33.1 ms ± 2.71 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [20]: %timeit nlargest(10, x, key=key)
133 ms ± 3.7 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [21]: %%timeit
...: for i in x:
...: key(i)
...:
93.2 ms ± 978 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [22]: %%timeit
...: for i in x:
...: pass
...:
10.1 ms ± 298 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Как видите, стоимость вызова key
для всех элементов составляет почти всю суммунакладных расходов.
Оценка ключей одинаково затратна для sorted
, но поскольку общая работа по сортировке обходится дороже, накладные расходы на вызовы ключей составляют меньший процент от общей суммы.Вы должны были сравнить абсолютные накладные расходы на использование ключа с nlargest
или sorted
, а не накладные расходы в процентах от базы.
In [23]: %timeit sorted(x)
542 ms ± 13.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [24]: %timeit sorted(x, key=key)
683 ms ± 12.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Как видите, стоимость key
вызовы составляют около половины накладных расходов при использовании этого ключа с sorted
на этом входе, остальная часть накладных расходов, вероятно, связана с работой по перетасовке большего количества данных в самой сортировке.
Выможет возникнуть вопрос, как nlargest
удается выполнять так мало работы на элемент.Для случая без ключа большая итерация происходит в следующем цикле:
for elem in it:
if top < elem:
_heapreplace(result, (elem, order))
top = result[0][0]
order -= 1
или для случая с ключом:
for elem in it:
k = key(elem)
if top < k:
_heapreplace(result, (k, order, elem))
top = result[0][0]
order -= 1
Важнейшая реализация заключается в том, что top < elem
и top < k
веток почти никогда не берут.Как только алгоритм найдет 10 довольно больших элементов, большинство оставшихся элементов будет меньше, чем 10 текущих кандидатов.В редких случаях, когда необходимо заменить элемент кучи, это еще больше усложняет прохождение дополнительными элементами полосы, необходимой для вызова heapreplace
.
При случайном вводе количество вызовов, заменяемых в heap nlargest
делает ожидаемый логарифмический размер ввода.В частности, для nlargest(10, x)
, кроме первых 10 элементов x
, элемент x[i]
имеет 10/(i+1)
вероятность оказаться в верхних 10 элементах l[:i+1]
, что является условием, необходимым для вызова heapreplace.По линейности ожидания ожидаемое количество вызовов heapreplace является суммой этих вероятностей, и эта сумма равна O (log (len (x))).(Этот анализ выполняется с заменой 10 на любую константу, но для переменной n
в nlargest(n, l)
требуется чуть более сложный анализ.)
История производительности будет сильно отличаться для отсортированного ввода, гдекаждый элемент прошел бы проверку if
:
In [25]: sorted_x = sorted(x)
In [26]: %timeit nlargest(10, sorted_x)
463 ms ± 26 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
В 10 раз дороже, чем несортированный случай!