Tại sao a.insert (0,0) chậm hơn nhiều so với [0: 0] = [0]?


61

Sử dụng insertchức năng của danh sách chậm hơn nhiều so với việc đạt được hiệu quả tương tự bằng cách sử dụng phép gán lát:

> 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

(Lưu ý rằng đó a=[]chỉ là thiết lập, vì vậy abắt đầu trống nhưng sau đó tăng lên 100.000 phần tử.)

Lúc đầu, tôi nghĩ có thể đó là tra cứu thuộc tính hoặc gọi hàm trên hoặc như vậy, nhưng việc chèn gần cuối cho thấy điều đó không đáng kể:

> python -m timeit -n 100000 -s "a=[]" "a.insert(-1,0)"
100000 loops, best of 5: 79.1 nsec per loop

Tại sao chức năng "chèn phần tử đơn" chuyên dụng đơn giản hơn có lẽ lại chậm hơn nhiều?

Tôi cũng có thể sao chép nó tại 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)

Tôi sử dụng Python 3.8.1 32 bit trên Windows 10 64 bit.
repl.it sử dụng Python 3.8.1 64 bit trên Linux 64 bit.


Thật thú vị khi lưu ý rằng a=[]; a[0:0]=[0]nó giống nhưa=[]; a[100:200]=[0]
smac89

Có bất kỳ lý do tại sao bạn đang thử nghiệm điều này chỉ với một danh sách trống?
MisterMiyagi

@MisterMiyagi Vâng, tôi phải bắt đầu với một cái gì đó . Lưu ý rằng nó chỉ trống trước khi chèn đầu tiên và tăng lên 100.000 phần tử trong điểm chuẩn.
Heap tràn

@ smac89 a=[1,2,3];a[100:200]=[4]đang nối 4đến cuối danh sách athú vị.
Ch3steR

1
@ smac89 Trong khi đó là sự thật, nó không thực sự liên quan đến câu hỏi và tôi sợ nó có thể khiến ai đó nghĩ rằng tôi đang đo điểm chuẩn a=[]; a[0:0]=[0]hoặc điều a[0:0]=[0]đó giống như a[100:200]=[0]...
Heap Overflow

Câu trả lời:


57

Tôi nghĩ rằng nó có thể chỉ là họ quên sử dụng memmovetrong list.insert. Nếu bạn xem list.insert sử dụng để dịch chuyển các phần tử, bạn có thể thấy đó chỉ là một vòng lặp thủ công:

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

trong khi list.__setitem__trên đường dẫn gán lát sử dụngmemmove :

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

memmove thường có rất nhiều tối ưu hóa được đưa vào, chẳng hạn như tận dụng các hướng dẫn SSE / AVX.


5
Cảm ơn. Tạo ra một vấn đề tham khảo điều này.
Heap tràn

7
Nếu trình thông dịch được xây dựng với tính năng -O3tự động hóa vector, vòng lặp thủ công đó có thể biên dịch hiệu quả. Nhưng trừ khi trình biên dịch nhận ra vòng lặp là một memmove và biên dịch nó thành một cuộc gọi thực tế memmove, nó chỉ có thể tận dụng các phần mở rộng của tập lệnh được kích hoạt trong thời gian biên dịch. (Tốt nếu bạn đang tự xây dựng với -march=native, không quá nhiều cho các nhị phân phân phối được xây dựng với đường cơ sở). Và GCC sẽ không hủy các vòng lặp theo mặc định trừ khi bạn sử dụng PGO ( -fprofile-generate/ run / ...-use)
Peter Cordes

@PeterCordes Tôi có hiểu chính xác bạn rằng nếu trình biên dịch thực hiện biên dịch nó thành một memmovecuộc gọi thực tế , thì điều đó có thể tận dụng tất cả các tiện ích mở rộng có mặt tại thời điểm thực hiện không?
Heap tràn

1
@HeapOverflow: Có. Ví dụ, trên GNU / Linux, glibc làm quá tải độ phân giải biểu tượng liên kết động với chức năng chọn phiên bản memmove viết tay tốt nhất cho máy này dựa trên kết quả phát hiện CPU đã lưu. (ví dụ trên x86, hàm init glibc sử dụng cpuid). Tương tự cho một số chức năng mem / str khác. Vì vậy, các distro có thể biên dịch chỉ -O2để tạo các nhị phân chạy ở mọi nơi, nhưng ít nhất có memcpy / memmove sử dụng một vòng lặp AVX không được kiểm soát tải / lưu trữ 32 byte cho mỗi lệnh. (Hoặc thậm chí AVX512 trên một vài CPU trong đó đó là một ý tưởng hay; tôi nghĩ chỉ là Xeon Phi.)
Peter Cordes

1
@HeapOverflow: Không, một số memmovephiên bản đang ngồi ở đó trong libc.so, thư viện chia sẻ. Đối với mỗi chức năng, công văn xảy ra một lần, trong quá trình phân giải biểu tượng (ràng buộc sớm hoặc trong cuộc gọi đầu tiên với ràng buộc lười truyền thống). Như tôi đã nói, nó chỉ quá tải / móc cách thức liên kết động xảy ra, không phải bằng cách tự gói chức năng. (cụ thể thông qua cơ chế ifunc của GCC: code.woboq.org/userspace/glibc/sysdeps/x86_64/multiarch/ù ). Liên quan: để ghi nhớ sự lựa chọn thông thường trên các CPU hiện đại, __memset_avx2_unaligned_erms hãy xem phần Hỏi & Đáp này
Peter Cordes
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.