6.19. Hiệu năng

Các quyết định thiết kế giúp numpy hoạt động nhanh trên camera -- gọi thư viện theo mảng toàn phần, bộ đệm kiểu dữ liệu đóng gói, các view chia sẻ dữ liệu với nguồn -- cũng tạo ra một tập thói quen đáng lưu ý. Trang Hình dạng và stride đã đề cập quy tắc bố cục trục cuối; trang này liệt kê các thói quen cấp phát và dtype quan trọng nhất trong vòng lặp truyền phát.

6.19.1. Chọn dtype hợp lý

Dtype mặc định của mọi hàm khởi tạo là float. Với dữ liệu vốn 8-bit hoặc 16-bit -- mẫu ADC, điểm ảnh, số đọc từ cảm biến -- hãy truyền dtype= tường minh vào một trong các kiểu số nguyên:

adc = np.array(adc_samples, dtype=np.uint16)

Mức tiết kiệm RAM là 2x với uint16 và 4x với uint8 so với mặc định 4 byte float. Phép tính cũng chạy nhanh hơn vì các nhánh mã số nguyên bên trong numpy gọn hơn các nhánh float tổng quát. Quy tắc tràn số nguyên đã đề cập trên Dtypes vẫn áp dụng -- hãy ép kiểu sang kiểu rộng hơn trước khi thực hiện phép toán có thể bị tràn.

6.19.2. Ưu tiên ndarray thay vì iterable

Hầu hết các phép rút gọn và hàm đa dụng chấp nhận cả iterable lẫn ndarray

np.sum([1, 2, 3, 4, 5])               # works, but slow
np.sum(np.array([1, 2, 3, 4, 5]))     # ~3x faster

Dạng iterable buộc numpy phải đi qua đầu vào từng đối tượng Python một, chuyển đổi từng đối tượng thành số trước khi sử dụng được. Với ndarray, quá trình chuyển đổi đã hoàn tất và lệnh gọi chạy thẳng qua bộ đệm đóng gói.

Khi cùng một dữ liệu được sử dụng nhiều lần, hãy tạo ndarray một lần và truyền nó đi. Khi dữ liệu chỉ tồn tại dưới dạng danh sách Python và chỉ được dùng một lần, chi phí chuyển đổi có thể vượt qua lợi ích tăng tốc -- bản thân hàm khởi tạo array() phải duyệt qua danh sách và cấp phát.

6.19.3. Ưu tiên view thay vì bản sao

Slicing, chỉ mục đơn trục của mảng bậc cao hơn, reshape(), transpose(), và frombuffer() đều trả về view chia sẻ dữ liệu với nguồn. Chúng về cơ bản là miễn phí.

copy(), flatten(), chỉ mục boolean (a[mask]), và mọi biểu thức số học đều cấp phát một bản sao. Chỉ dùng chúng khi thực sự cần một bộ đệm độc lập.

Khi không chắc chắn, ndinfo() in ra vị trí của bộ đệm nền; hai mảng cùng báo cáo một địa chỉ thì đang chia sẻ dữ liệu. Bảng đầy đủ về view và bản sao có tại View và bản sao.

6.19.4. Cấp phát một lần, sau đó ghi

Cạm bẫy hiệu năng lớn nhất trên camera là cấp phát mảng mới trong vòng lặp chạy nhiều lần mỗi giây. Mỗi ndarray mới yêu cầu camera cấp RAM, và việc cấp phát mới liên tục sẽ lãng phí RAM.

Hầu hết các hàm đa dụng chấp nhận out= để kết quả có thể được ghi vào mảng đã tồn tại:

x = np.linspace(0, 2 * np.pi, num=512)
y = np.zeros(512)        # allocate once

while True:
    np.sin(x, out=y)
    # use y ...

image.Image.to_ndarray() chấp nhận buffer= vì lý do tương tự; spectrogram() và các bộ chuyển đổi kiểu from_int32_buffer() chấp nhận cả out= lẫn scratchpad=. Hãy cấp phát tất cả một lần và tái sử dụng.

6.19.5. Dùng toán tử tại chỗ

b = b + 1 cấp phát một biến tạm có kích thước bằng b, sao chép, và gán lại. b += 1 sửa đổi b trực tiếp:

# makes a temporary
b = b + 1

# no temporary
b += 1

Ý tưởng tương tự áp dụng cho biểu thức phức hợp. a + b * c cấp phát một biến tạm cho b * c. Tách biểu thức thành các phép gán con đơn giản ghi vào bộ đệm đã cấp phát sẵn sẽ loại bỏ các biến tạm:

# one temporary for (a + b), another for the ``* 2``
out = (a + b) * 2

# zero temporaries
out[:]  = a
out    += b
out    *= 2

6.19.6. Xây dựng kết quả, đừng nối vào nó

ndarray không có append -- điều này là có chủ đích. Mở rộng mảng đồng nghĩa với việc cấp phát một bộ đệm mới lớn hơn và sao chép nội dung cũ vào đó. Trên vi điều khiển, hãy cấp phát trước kích thước cuối cùng và điền vào:

out = np.zeros(N, dtype=np.float)
for i in range(N):
    out[i] = some_calculation(i)

Khi N thực sự chưa biết trước, hãy ghi vào list Python và chuyển đổi một lần ở cuối bằng array().

6.19.7. Gán slice thay vì tạo mảng mới

Nhiều mẫu "xây dựng mảng mới từ các phần của mảng khác" có thể được biểu diễn dưới dạng gán slice vào bộ đệm đã cấp phát sẵn thay vì cấp phát mới mỗi lần gọi.

Cửa sổ trượt trên luồng mẫu -- nền tảng của bộ lọc trung bình động -- là trường hợp kinh điển. Bộ đệm giữ N mẫu cuối; mỗi lần lặp bỏ mẫu cũ nhất và thêm mẫu mới nhất. Dạng rõ ràng xây lại bộ đệm mỗi lần lặp:

while True:
    sample = read_sample()
    buf = np.concatenate((buf[1:],              # new buffer every loop
                          np.array([sample])))
    avg = np.mean(buf)

Đó là một lần cấp phát mới -- và sao chép N - 1 phần tử -- cho mỗi mẫu. Dạng gán slice thực hiện tại chỗ:

N   = 16
buf = np.zeros(N, dtype=np.float)               # allocate once

while True:
    sample   = read_sample()
    buf[:-1] = buf[1:]                          # shift left by one
    buf[-1]  = sample                           # append at the end
    avg      = np.mean(buf)

buf[:-1] = buf[1:] là dòng thú vị: hai view chồng lên nhau trong cùng một bộ đệm, slice bên phải được đọc từ một đầu và ghi vào đầu kia. numpy duyệt bộ nhớ nền theo thứ tự đảm bảo việc dịch chuyển tại chỗ an toàn. Không có mảng mới nào được cấp phát bên trong vòng lặp.

6.19.8. Cẩn thận với mặt nạ boolean trong vòng lặp truyền phát

Chỉ mục boolean và where() tạo ra một mảng mới mỗi lần gọi -- kích thước kết quả phụ thuộc vào dữ liệu, vì vậy không có bộ đệm cấp phát sẵn nào có thể hấp thụ được. Xây dựng mặt nạ lặp đi lặp lại trong vòng lặp truyền phát sẽ lấp đầy RAM với các mảng tạm thời. Gọi gc.collect() định kỳ để thu hồi không gian:

import gc

for i in range(1000):
    mask = a < threshold
    _    = a[mask]
    if i % 100 == 0:
        gc.collect()

Lưu ý tương tự áp dụng cho biểu thức boolean phức hợp như (a > lo) & (a < hi) -- mỗi toán tử cấp phát một mảng bool mới. Khi mặt nạ được tái sử dụng, hãy xây dựng nó một lần và giữ lại:

mask = a < threshold
foo[mask] = 0
bar[mask] = 1