Использование insert
функции списка намного медленнее, чем достижение того же эффекта с помощью назначения срезов:
> python -m timeit -n 100000 -s "a=[]" "a.insert(0,0)"
100000 loops, best of 5: 19.2 usec per loop
> python -m timeit -n 100000 -s "a=[]" "a[0:0]=[0]"
100000 loops, best of 5: 6.78 usec per loop
(Обратите внимание, что a=[]
это только настройка, поэтому a
начинается с пустого значения, но затем увеличивается до 100 000 элементов.)
Сначала я подумал, что это может быть поиск атрибута или служебный вызов функции или около того, но вставка в конце показывает, что это незначительно:
> python -m timeit -n 100000 -s "a=[]" "a.insert(-1,0)"
100000 loops, best of 5: 79.1 nsec per loop
Почему предположительно более простая выделенная функция «вставка одного элемента» намного медленнее?
Я также могу воспроизвести его в repl.it :
from timeit import repeat
for _ in range(3):
for stmt in 'a.insert(0,0)', 'a[0:0]=[0]', 'a.insert(-1,0)':
t = min(repeat(stmt, 'a=[]', number=10**5))
print('%.6f' % t, stmt)
print()
# Example output:
#
# 4.803514 a.insert(0,0)
# 1.807832 a[0:0]=[0]
# 0.012533 a.insert(-1,0)
#
# 4.967313 a.insert(0,0)
# 1.821665 a[0:0]=[0]
# 0.012738 a.insert(-1,0)
#
# 5.694100 a.insert(0,0)
# 1.899940 a[0:0]=[0]
# 0.012664 a.insert(-1,0)
Я использую 32-битный Python 3.8.1 на 64-битной Windows 10.
repl.it использует 64-битный Python 3.8.1 в 64-битном Linux.
python
performance
Переполнение кучи
источник
источник
a=[]; a[0:0]=[0]
делает то же самое, что иa=[]; a[100:200]=[0]
a=[1,2,3];a[100:200]=[4]
является добавление4
к концу спискаa
интересного.a=[]; a[0:0]=[0]
или что онa[0:0]=[0]
делает то же самое, чтоa[100:200]=[0]
...Ответы:
Я думаю , что это, вероятно , только что они забыли использовать
memmove
вlist.insert
. Если вы посмотрите на код,list.insert
используемый для сдвига элементов, вы увидите, что это просто ручной цикл:в то время как
list.__setitem__
на пути назначения среза используетсяmemmove
:memmove
как правило, в него вкладывается много оптимизации, например, использование инструкций SSE / AVX.источник
-O3
включенной автоматической векторизацией, этот ручной цикл мог бы скомпилироваться эффективно. Но если компилятор не распознает цикл как memmove и не скомпилирует его в фактический вызовmemmove
, он может использовать только расширения набора команд, включенные во время компиляции. (Хорошо, если вы создаете свой собственный-march=native
, не так уж много для дистрибутивов, основанных на базовой линии). И GCC не будет развертывать циклы по умолчанию, если вы не используете PGO (-fprofile-generate
/ run /...-use
)memmove
вызов, то он сможет воспользоваться всеми расширениями, присутствующими во время выполнения?cpuid
). То же самое для нескольких других функций mem / str. Таким образом, дистрибутивы могут компилироваться только-O2
для создания исполняемых файлов в любом месте, но, по крайней мере, memcpy / memmove использует развернутый цикл AVX, загружающий / сохраняющий 32 байта на инструкцию. (Или даже AVX512 на нескольких процессорах, где это хорошая идея; я думаю, что только Xeon Phi.)memmove
в libc.so, общей библиотеке , есть несколько версий. Для каждой функции диспетчеризация происходит один раз, во время разрешения символа (раннее связывание или при первом вызове с традиционным отложенным связыванием). Как я уже сказал, он просто перегружает / перехватывает, как происходит динамическое связывание, а не упаковывая саму функцию. (в частности, с помощью механизма ifunc GCC: code.woboq.org/userspace/glibc/sysdeps/x86_64/multiarch/… ). Связанный: для memset обычный выбор на современных процессорах -__memset_avx2_unaligned_erms
см. Этот Q & A