Tại sao mảng của Python chậm?


153

Tôi dự kiến ​​sẽ array.arraynhanh hơn danh sách, vì các mảng dường như không được mở hộp.

Tuy nhiên, tôi nhận được kết quả sau:

In [1]: import array

In [2]: L = list(range(100000000))

In [3]: A = array.array('l', range(100000000))

In [4]: %timeit sum(L)
1 loop, best of 3: 667 ms per loop

In [5]: %timeit sum(A)
1 loop, best of 3: 1.41 s per loop

In [6]: %timeit sum(L)
1 loop, best of 3: 627 ms per loop

In [7]: %timeit sum(A)
1 loop, best of 3: 1.39 s per loop

Điều gì có thể là nguyên nhân của sự khác biệt như vậy?


4
công cụ numpy có thể khai thác hiệu quả mảng của bạn:% timeit np.sum (A): 100 vòng, tốt nhất là 3: 8,87 ms mỗi vòng lặp
BM

6
Tôi chưa bao giờ gặp phải tình huống tôi cần sử dụng arraygói. Nếu bạn muốn thực hiện một số lượng đáng kể toán học, Numpy hoạt động ở tốc độ ánh sáng (tức là C), và thường tốt hơn so với việc triển khai ngây thơ những thứ như sum()).
Nick T

40
Đóng cử tri: Tại sao chính xác là dựa trên ý kiến ​​này? OP dường như đang hỏi một câu hỏi kỹ thuật cụ thể về một hiện tượng có thể đo lường và lặp lại.
Kevin

5
@NickT Đọc một giai thoại tối ưu hóa . Hóa ra arraylà khá nhanh trong việc chuyển đổi một chuỗi số nguyên (đại diện cho byte ASCII) thành một strđối tượng. Bản thân Guido chỉ nghĩ ra điều này sau rất nhiều giải pháp khác và khá bất ngờ với màn trình diễn. Dù sao đây là nơi duy nhất mà tôi nhớ rằng nó hữu ích. numpytốt hơn nhiều để xử lý các mảng nhưng đó là sự phụ thuộc của bên thứ 3.
Bakuriu

Câu trả lời:


220

Bộ lưu trữ là "unboxed", nhưng mỗi khi bạn truy cập một phần tử Python phải "đóng hộp" nó (nhúng nó vào một đối tượng Python thông thường) để làm bất cứ điều gì với nó. Ví dụ, sum(A)lặp đi lặp lại của bạn trên mảng và hộp mỗi số nguyên, mỗi số một, trong một intđối tượng Python thông thường . Điều đó tốn thời gian. Trong bạn sum(L), tất cả các quyền anh đã được thực hiện tại thời điểm danh sách được tạo ra.

Vì vậy, cuối cùng, một mảng thường chậm hơn, nhưng đòi hỏi bộ nhớ ít hơn đáng kể.


Đây là mã có liên quan từ phiên bản gần đây của Python 3, nhưng các ý tưởng cơ bản tương tự áp dụng cho tất cả các triển khai CPython kể từ khi Python được phát hành lần đầu tiên.

Đây là mã để truy cập một mục danh sách:

PyObject *
PyList_GetItem(PyObject *op, Py_ssize_t i)
{
    /* error checking omitted */
    return ((PyListObject *)op) -> ob_item[i];
}

Có rất ít: somelist[i]chỉ trả về iđối tượng thứ trong danh sách (và tất cả các đối tượng Python trong CPython là con trỏ tới một cấu trúc có phân đoạn ban đầu phù hợp với bố cục của a struct PyObject).

Và đây là cách __getitem__triển khai cho một arraymã loại l:

static PyObject *
l_getitem(arrayobject *ap, Py_ssize_t i)
{
    return PyLong_FromLong(((long *)ap->ob_item)[i]);
}

Bộ nhớ thô được coi là một vectơ của các C longsố nguyên gốc ; cái ithứ C longđược đọc lên; và sau đó PyLong_FromLong()được gọi để bọc ("hộp") bản địa C longtrong một longđối tượng Python (trong Python 3, loại bỏ sự phân biệt của Python 2 giữa intlong, thực sự được hiển thị dưới dạng loại int).

Quyền anh này phải phân bổ bộ nhớ mới cho một intđối tượng Python và phun các C longbit của người bản địa vào nó. Trong ngữ cảnh của ví dụ ban đầu, thời gian tồn tại của đối tượng này rất ngắn gọn (chỉ đủ lâu sum()để thêm nội dung vào tổng số đang chạy), và sau đó cần thêm thời gian để phân bổ intđối tượng mới .

Đây là nơi mà sự khác biệt về tốc độ đến từ, luôn luôn đến từ và luôn luôn đến từ việc triển khai CPython.


87

Để thêm vào câu trả lời xuất sắc của Tim Peters, các mảng thực hiện giao thức bộ đệm , trong khi các danh sách thì không. Điều này có nghĩa là, nếu bạn đang viết một phần mở rộng C (hoặc tương đương về mặt đạo đức, chẳng hạn như viết mô-đun Cython ), thì bạn có thể truy cập và làm việc với các phần tử của một mảng nhanh hơn bất kỳ thứ gì Python có thể làm. Điều này sẽ cung cấp cho bạn những cải tiến đáng kể về tốc độ, có thể là trên một mức độ lớn. Tuy nhiên, nó có một số nhược điểm:

  1. Bây giờ bạn đang kinh doanh bằng văn bản C thay vì Python. Cython là một cách để cải thiện điều này, nhưng nó không loại bỏ nhiều khác biệt cơ bản giữa các ngôn ngữ; bạn cần phải làm quen với ngữ nghĩa C và hiểu những gì nó đang làm.
  2. API C của PyPy hoạt động ở một mức độ nào đó , nhưng không nhanh lắm. Nếu bạn đang nhắm mục tiêu PyPy, có lẽ bạn chỉ nên viết mã đơn giản với các danh sách thông thường, sau đó để JITter tối ưu hóa nó cho bạn.
  3. Các phần mở rộng C khó phân phối hơn mã Python thuần vì chúng cần được biên dịch. Việc biên dịch có xu hướng phụ thuộc vào kiến ​​trúc và hệ điều hành, vì vậy bạn sẽ cần đảm bảo rằng bạn đang biên dịch cho nền tảng đích của mình.

Đi thẳng đến tiện ích mở rộng C có thể sử dụng búa tạ để bay ruồi, tùy thuộc vào trường hợp sử dụng của bạn. Trước tiên, bạn nên điều tra NumPy và xem liệu nó có đủ mạnh để làm bất cứ điều gì toán học mà bạn đang cố gắng làm không. Nó cũng sẽ nhanh hơn nhiều so với Python bản địa, nếu được sử dụng đúng cách.


10

Tim Peters đã trả lời tại sao điều này chậm, nhưng hãy xem cách cải thiện nó.

Bám sát ví dụ của bạn về sum(range(...))(yếu tố 10 nhỏ hơn ví dụ của bạn để phù hợp với bộ nhớ ở đây):

import numpy
import array
L = list(range(10**7))
A = array.array('l', L)
N = numpy.array(L)

%timeit sum(L)
10 loops, best of 3: 101 ms per loop

%timeit sum(A)
1 loop, best of 3: 237 ms per loop

%timeit sum(N)
1 loop, best of 3: 743 ms per loop

Cách này cũng cần numpy để hộp / unbox, có thêm chi phí. Để làm cho nó nhanh, người ta phải ở trong mã c numpy:

%timeit N.sum()
100 loops, best of 3: 6.27 ms per loop

Vì vậy, từ giải pháp danh sách đến phiên bản numpy, đây là một yếu tố 16 trong thời gian chạy.

Chúng ta cũng hãy kiểm tra việc tạo các cấu trúc dữ liệu đó mất bao lâu

%timeit list(range(10**7))
1 loop, best of 3: 283 ms per loop

%timeit array.array('l', range(10**7))
1 loop, best of 3: 884 ms per loop

%timeit numpy.array(range(10**7))
1 loop, best of 3: 1.49 s per loop

%timeit numpy.arange(10**7)
10 loops, best of 3: 21.7 ms per loop

Người chiến thắng rõ ràng: Numpy

Cũng lưu ý rằng việc tạo cấu trúc dữ liệu sẽ mất nhiều thời gian như tính tổng, nếu không muốn nói là nhiều hơn. Phân bổ bộ nhớ là chậm.

Sử dụng bộ nhớ của những người:

sys.getsizeof(L)
90000112
sys.getsizeof(A)
81940352
sys.getsizeof(N)
80000096

Vì vậy, chúng mất 8 byte cho mỗi số với chi phí khác nhau. Đối với phạm vi chúng tôi sử dụng int 32 bit là đủ, vì vậy chúng tôi có thể an toàn một số bộ nhớ.

N=numpy.arange(10**7, dtype=numpy.int32)

sys.getsizeof(N)
40000096

%timeit N.sum()
100 loops, best of 3: 8.35 ms per loop

Nhưng hóa ra việc thêm int 64bit nhanh hơn int 32 bit trên máy của tôi, vì vậy điều này chỉ đáng giá nếu bạn bị giới hạn bởi bộ nhớ / băng thông.


-1

xin lưu ý rằng 100000000bằng với 10^8không 10^7, và kết quả của tôi là folowwing:

100000000 == 10**8

# my test results on a Linux virtual machine:
#<L = list(range(100000000))> Time: 0:00:03.263585
#<A = array.array('l', range(100000000))> Time: 0:00:16.728709
#<L = list(range(10**8))> Time: 0:00:03.119379
#<A = array.array('l', range(10**8))> Time: 0:00:18.042187
#<A = array.array('l', L)> Time: 0:00:07.524478
#<sum(L)> Time: 0:00:01.640671
#<np.sum(L)> Time: 0:00:20.762153
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.