Sự khác nhau giữa các mảng liền kề và không liền kề là gì?


101

Trong sổ tay hướng dẫn về hàm reshape (), nó nói

>>> a = np.zeros((10, 2))
# A transpose make the array non-contiguous
>>> b = a.T
# Taking a view makes it possible to modify the shape without modifying the
# initial object.
>>> c = b.view()
>>> c.shape = (20)
AttributeError: incompatible shape for a non-contiguous array

Câu hỏi của tôi là:

  1. Mảng liên tục và không liền kề là gì? Nó giống với khối bộ nhớ liền kề trong C như Khối bộ nhớ liền kề là gì?
  2. Có sự khác biệt nào về hiệu suất giữa hai loại này không? Khi nào chúng ta nên sử dụng cái này hay cái kia?
  3. Tại sao phép hoán vị làm cho mảng không liền nhau?
  4. Tại sao c.shape = (20)ném một lỗi incompatible shape for a non-contiguous array?

Cảm ơn câu trả lời của bạn!

Câu trả lời:


222

Mảng liền kề chỉ là một mảng được lưu trữ trong một khối bộ nhớ không bị gián đoạn: để truy cập giá trị tiếp theo trong mảng, chúng ta chỉ cần di chuyển đến địa chỉ bộ nhớ tiếp theo.

Hãy xem xét mảng 2D arr = np.arange(12).reshape(3,4). Nó trông như thế này:

nhập mô tả hình ảnh ở đây

Trong bộ nhớ của máy tính, các giá trị của arrđược lưu trữ như sau:

nhập mô tả hình ảnh ở đây

Điều này có nghĩa arrlà một mảng liền kề C vì các hàng được lưu trữ dưới dạng các khối bộ nhớ liền kề. Địa chỉ bộ nhớ tiếp theo giữ giá trị hàng tiếp theo trên hàng đó. Nếu chúng ta muốn di chuyển xuống một cột, chúng ta chỉ cần nhảy qua ba khối (ví dụ: để nhảy từ 0 đến 4 nghĩa là chúng ta bỏ qua 1,2 và 3).

Việc hoán vị mảng với arr.Tnghĩa là sự tiếp giáp C bị mất vì các mục nhập hàng liền kề không còn trong địa chỉ bộ nhớ liền kề. Tuy nhiên, arr.Ttiếp giáp Fortran kể từ cột trong khối tiếp giáp bộ nhớ:

nhập mô tả hình ảnh ở đây


Về mặt hiệu suất, việc truy cập các địa chỉ bộ nhớ cạnh nhau thường nhanh hơn so với việc truy cập các địa chỉ "dàn trải" hơn (tìm nạp một giá trị từ RAM có thể dẫn đến một số địa chỉ lân cận được tìm nạp và lưu vào bộ nhớ đệm cho CPU.) có nghĩa là các thao tác trên các mảng liền kề thường sẽ nhanh hơn.

Kết quả của việc bố trí bộ nhớ liền kề C, các hoạt động theo hàng thường nhanh hơn các hoạt động theo cột. Ví dụ, bạn thường thấy rằng

np.sum(arr, axis=1) # sum the rows

nhanh hơn một chút so với:

np.sum(arr, axis=0) # sum the columns

Tương tự, các thao tác trên các cột sẽ nhanh hơn một chút đối với các mảng liền kề Fortran.


Cuối cùng, tại sao chúng ta không thể làm phẳng mảng liền kề Fortran bằng cách gán một hình dạng mới?

>>> arr2 = arr.T
>>> arr2.shape = 12
AttributeError: incompatible shape for a non-contiguous array

Để có thể thực hiện được điều này, NumPy sẽ phải đặt các hàng arr.Tlại với nhau như sau:

nhập mô tả hình ảnh ở đây

(Việc đặt shapethuộc tính trực tiếp giả định thứ tự C - tức là NumPy cố gắng thực hiện thao tác theo hàng.)

Điều này là không thể làm được. Đối với bất kỳ trục nào, NumPy cần có độ dài sải chân không đổi (số byte để di chuyển) để đến phần tử tiếp theo của mảng. Làm phẳng arr.Ttheo cách này sẽ yêu cầu bỏ qua tiến và lùi trong bộ nhớ để truy xuất các giá trị liên tiếp của mảng.

Nếu chúng tôi đã viết arr2.reshape(12)thay thế, NumPy sẽ sao chép các giá trị của arr2 vào một khối bộ nhớ mới (vì nó không thể trả lại chế độ xem về dữ liệu ban đầu cho hình dạng này).


Tôi đang khó hiểu điều này, bạn có thể vui lòng giải thích một chút? Theo ý kiến ​​của tôi, trong biểu diễn đồ họa mới nhất về thứ tự bất khả thi trong bộ nhớ, các bước tiến là không đổi. Ví dụ để đi từ 0 đến 1, sải chân là 1 byte (giả sử mỗi phần tử là một byte) và nó giống nhau cho mỗi cột. Tương tự như vậy, khoảng cách là 4 byte để đi từ một phần tử trong hàng sang phần tử tiếp theo và nó cũng không đổi.
Vesnog

2
@Vesnog định dạng lại không thành công của hình dạng 2D arr2thành 1D (12,)sử dụng thứ tự C, có nghĩa là trục 1 không được gắn trước trục 0 (nghĩa là mỗi hàng trong số bốn hàng cần được đặt cạnh nhau để tạo ra mảng 1D mong muốn). Không thể đọc chuỗi số nguyên này (0, 4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11) ra khỏi bộ đệm bằng cách sử dụng độ dài sải chân không đổi (các byte để chuyển đến truy cập các phần tử này theo thứ tự sẽ là 4, 4, -7, 4, 4, -7, 4, 4, 7, 4, 4). NumPy yêu cầu độ dài sải chân liên tục trên mỗi trục.
Alex Riley

Cảm ơn lúc đầu, tôi nghĩ nó sẽ tạo ra một mảng mới, nhưng nó sử dụng bộ nhớ của mảng cũ.
Vesnog

@AlexRiley Điều gì xảy ra đằng sau hậu trường khi một mảng được đánh dấu cận cảnh C hoặc F theo thứ tự? Ví dụ: lấy mọi arr mảng NxD và in (arr [:, :: - 1] .flags). Điều gì xảy ra trong tình huống này? Tôi đoán mảng thực sự là C hoặc F được sắp xếp, nhưng cái nào trong số đó? Và những tối ưu hóa nào của numpy mà chúng tôi mất nếu cả hai lá cờ là Sai?
Jjang

@Jjang: việc NumPy coi mảng là thứ tự C hay F là hoàn toàn phụ thuộc vào hình dạng và bước (tiêu chí ở đây ). Vì vậy, trong khi arr[:, ::-1]xem cùng một bộ đệm bộ nhớ arr, NumPy không coi nó là thứ tự C hoặc F vì nó đã duyệt các giá trị trong bộ đệm theo thứ tự "không chuẩn" ...
Alex Riley

12

Có thể ví dụ này với 12 giá trị mảng khác nhau sẽ giúp:

In [207]: x=np.arange(12).reshape(3,4).copy()

In [208]: x.flags
Out[208]: 
  C_CONTIGUOUS : True
  F_CONTIGUOUS : False
  OWNDATA : True
  ...
In [209]: x.T.flags
Out[209]: 
  C_CONTIGUOUS : False
  F_CONTIGUOUS : True
  OWNDATA : False
  ...

Các C order giá trị theo thứ tự mà chúng được tạo ra. giá trị được hoán vị không

In [212]: x.reshape(12,)   # same as x.ravel()
Out[212]: array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])

In [213]: x.T.reshape(12,)
Out[213]: array([ 0,  4,  8,  1,  5,  9,  2,  6, 10,  3,  7, 11])

Bạn có thể xem 1d của cả hai

In [214]: x1=x.T

In [217]: x.shape=(12,)

Hình dạng của x cũng có thể được thay đổi.

In [220]: x1.shape=(12,)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-220-cf2b1a308253> in <module>()
----> 1 x1.shape=(12,)

AttributeError: incompatible shape for a non-contiguous array

Nhưng hình dạng của chuyển vị không thể thay đổi. Cácdata vẫn còn trong 0,1,2,3,4...trật tự, mà không thể được truy cập truy cập như 0,4,8...trong một mảng 1d.

Nhưng một bản sao của x1 có thể được thay đổi:

In [227]: x2=x1.copy()

In [228]: x2.flags
Out[228]: 
  C_CONTIGUOUS : True
  F_CONTIGUOUS : False
  OWNDATA : True
  ...
In [229]: x2.shape=(12,)

Nhìn strides cũng có thể giúp ích. Một bước tiến là bao xa (tính bằng byte) nó phải bước để đến giá trị tiếp theo. Đối với mảng 2d, sẽ có 2 giá trị bước:

In [233]: x=np.arange(12).reshape(3,4).copy()

In [234]: x.strides
Out[234]: (16, 4)

Để đến hàng tiếp theo, hãy bước 16 byte, chỉ cột tiếp theo 4.

In [235]: x1.strides
Out[235]: (4, 16)

Transpose chỉ chuyển đổi thứ tự của các bước. Hàng tiếp theo chỉ có 4 byte- tức là số tiếp theo.

In [236]: x.shape=(12,)

In [237]: x.strides
Out[237]: (4,)

Thay đổi hình dạng cũng thay đổi các bước - chỉ cần bước qua bộ đệm 4 byte một lúc.

In [238]: x2=x1.copy()

In [239]: x2.strides
Out[239]: (12, 4)

Mặc dù x2trông giống như vậy x1, nó có bộ đệm dữ liệu riêng, với các giá trị theo thứ tự khác. Cột tiếp theo hiện hơn 4 byte, trong khi hàng tiếp theo là 12 (3 * 4).

In [240]: x2.shape=(12,)

In [241]: x2.strides
Out[241]: (4,)

Và như với x , việc thay đổi hình dạng thành 1d làm giảm các bước tiến tới (4,).

Đối với x1, với dữ liệu theo 0,1,2,...thứ tự, không có bước tiến 1 ngày nào0,4,8... .

__array_interface__ là một cách hữu ích khác để hiển thị thông tin mảng:

In [242]: x1.__array_interface__
Out[242]: 
{'strides': (4, 16),
 'typestr': '<i4',
 'shape': (4, 3),
 'version': 3,
 'data': (163336056, False),
 'descr': [('', '<i4')]}

Các x1địa chỉ bộ đệm dữ liệu sẽ được giống như cho x, mà nó chia sẻ dữ liệu. x2có địa chỉ bộ đệm khác.

Bạn cũng có thể thử nghiệm với việc thêm một order='F'tham số vào lệnh copyreshape.

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.