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비우기 시작하지만 10 만 요소로 커집니다.)

처음에는 속성 조회 또는 함수 호출 오버 헤드 정도라고 생각했지만 끝 부분에 삽입하면 무시할 만하다는 것을 알 수 있습니다.

> 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)

Windows 10 64 비트에서 Python 3.8.1 32 비트를 사용합니다.
repl.it는 Linux 64 비트에서 Python 3.8.1 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로 인식하고이를 실제 호출로 컴파일하지 않으면 memmove컴파일시 활성화 된 명령어 세트 확장 만 이용할 수 있습니다. ( -march=native로베이스 라인을 빌드 한 배포 바이너리에는 그다지 크지 않은)을 사용 하여 자신 만의 것을 빌드하는 것이 좋습니다 . PGO ( -fprofile-generate/ run / ...-use)
Peter Cordes

@PeterCordes 컴파일러가 실제 memmove호출 로 컴파일하면 실행 시간에 존재하는 모든 확장을 활용할 수 있다는 것을 올바르게 이해하고 있습니까?
힙 오버 플로우

1
@HeapOverflow : 예. 예를 들어 GNU / Linux에서 glibc는 저장된 CPU 감지 결과를 기반으로이 시스템에 가장 적합한 손으로 작성한 asm 버전의 memmove를 선택하는 기능으로 동적 링커 심볼 해상도를 오버로드합니다. (예 : x86에서 glibc init 함수는을 사용합니다 cpuid). 다른 여러 mem / str 함수에서도 동일합니다. 따라서 배포판은 -O2어디에서나 바이너리를 실행하기 위해 컴파일 할 수 있지만 적어도 memcpy / memmove는 명령 당 32 바이트를로드 / 저장하지 않은 AVX 루프를 사용합니다. (또는 그것이 좋은 아이디어 인 몇몇 CPU의 AVX512조차도; 제온 피라고 생각합니다.)
Peter Cordes

1
@HeapOverflow : 아니요, memmove공유 라이브러리 인 libc.so에 여러 버전이 있습니다. 각 함수에 대해 디스패치가 심볼 분석 (초기 바인딩 또는 기존 지연 바인딩을 사용한 첫 번째 호출) 중에 한 번 발생합니다. 내가 말했듯이, 그것은 함수 자체를 감싸지 않고 동적 링크가 어떻게 발생하는지 오버로드 / 후크합니다. (특히 GCC의 ifunc 메커니즘을 통해 : code.woboq.org/userspace/glibc/sysdeps/x86_64/multiarch/… ). 관련 : 현대 CPU에서 일반적으로 선택되는 memset의 __memset_avx2_unaligned_erms 경우이 Q & A
Peter Cordes
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.