Чому a.insert (0,0) набагато повільніше, ніж a [0: 0] = [0]?


61

Використання списку 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 в Windows 10 64-розрядний.
repl.it використовує 64-розрядний Python 3.8.1 в Linux 64-розрядному.


Цікаво зазначити, що a=[]; a[0:0]=[0]робить те саме, щоa=[]; a[100:200]=[0]
smac89

Чи є причина, чому ви тестуєте це лише з порожнім списком?
MisterMiyagi

@MisterMiyagi Ну, я повинен почати з чогось . Зауважте, що він порожній лише перед першим введенням і зростає до 100 000 елементів під час еталону.
Купа переповнення

@ Smac89 a=[1,2,3];a[100:200]=[4]є додавання 4до кінця списку aцікавого.
Ch3steR

1
@ smac89 Хоча це правда, це насправді не пов'язане з питанням, і я боюся, що це може ввести когось в оману, думаючи, що я тестую тести, a=[]; a[0:0]=[0]або a[0:0]=[0]це те саме, що a[100:200]=[0]...
Heap Overflow

Відповіді:


57

Я думаю , що це, ймовірно , тільки що вони забули використовувати memmoveв list.insert. Якщо ви подивитеся на код, який list.insert використовується для переміщення елементів, ви побачите, що це просто ручний цикл:

for (i = n; --i >= where; )
    items[i+1] = items[i];

тоді як list.__setitem__на шляху призначення фрагмента використовуєтьсяmemmove :

memmove(&item[ihigh+d], &item[ihigh],
    (k - ihigh)*sizeof(PyObject *));

memmove Зазвичай в нього вкладено багато оптимізацій, таких як використання інструкцій SSE / AVX.


5
Дякую. Створено проблему з посиланням на це.
Купа переповнення

7
Якщо інтерпретатор був побудований з -O3включеною автоматичною векторизацією, цей цикл вручну може скластись ефективно. Але якщо компілятор не визнає цикл пам’яті і компілює його в фактичний виклик memmove, він може скористатися лише розширеннями набору інструкцій, включеними під час компіляції. (Добре, якщо ви будуєте свій власний -march=native, а не стільки для дистрибутивів дистрибутивів, побудованих за базовою лінією). І GCC не буде розгортати цикли за замовчуванням, якщо ви не використовуєте PGO ( -fprofile-generate/ run / ...-use)
Пітер Кордес

@PeterCordes Чи правильно я розумію, що якщо компілятор компілює його у фактичний memmoveвиклик, то він може скористатися всіма розширеннями, наявними під час виконання?
Купа переповнення

1
@HeapOverflow: Так. Наприклад, у GNU / Linux, glibc перевантажує динамічну роздільну здатність символу лінкера функцією, яка вибирає найкращу рукописну версію пам’яті memmove для цієї машини на основі збережених результатів виявлення процесора. (наприклад, на x86 використовується функція inli glibc cpuid). Те саме для декількох інших функцій mem / str. Таким чином, дистрибутив може компілюватися лише -O2для того, щоб робити бінарні файли, де виконується будь-де, але принаймні мати memcpy / memmove, використовуючи завантаження / зберігання циклу AVX для завантаження / зберігання 32 байтів за інструкцію. (Або навіть AVX512 на кількох процесорах, де це гарна ідея; я думаю, що тільки Xeon Phi.)
Peter Cordes

1
@HeapOverflow: Ні, кілька memmoveверсій сидять там у libc.so, спільній бібліотеці. Для кожної функції відправлення відбувається один раз, під час вирішення символу (раннє прив'язування або під час першого дзвінка з традиційним лінивим прив'язкою). Як я вже говорив, це просто перевантажує / зачіпляє те, як відбувається динамічне зв’язування, а не загортаючи саму функцію. (зокрема через механізм ifunc GCC: code.woboq.org/userspace/glibc/sysdeps/x86_64/multiarch/… ). Пов’язано: для пам’яті звичайним вибором для сучасних процесорів є __memset_avx2_unaligned_erms це запитання та відповіді
Пітер Кордес
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.