2014-12-13 60 views
14

Cuối cùng tôi đã tìm thấy một nút cổ chai hiệu suất trong mã của tôi nhưng tôi bối rối vì lý do là gì. Để giải quyết nó, tôi đã thay đổi tất cả các cuộc gọi của mình là numpy.zeros_like để thay thế sử dụng numpy.zeros. Nhưng tại sao lại là zeros_like sooooo chậm hơn nhiều?Tại sao hiệu suất khác biệt giữa numpy.zeros và numpy.zeros_like?

Ví dụ (lưu ý e-05 trên zeros gọi):

>>> timeit.timeit('np.zeros((12488, 7588, 3), np.uint8)', 'import numpy as np', number = 10) 
5.2928924560546875e-05 
>>> timeit.timeit('np.zeros_like(x)', 'import numpy as np; x = np.zeros((12488, 7588, 3), np.uint8)', number = 10) 
1.4402990341186523 

Nhưng sau đó kỳ lạ bằng văn bản cho một mảng được tạo ra với zeros đáng chú ý là chậm hơn so với một mảng được tạo ra với zeros_like:

>>> timeit.timeit('x[100:-100, 100:-100] = 1', 'import numpy as np; x = np.zeros((12488, 7588, 3), np.uint8)', number = 10) 
0.4310588836669922 
>>> timeit.timeit('x[100:-100, 100:-100] = 1', 'import numpy as np; x = np.zeros_like(np.zeros((12488, 7588, 3), np.uint8))', number = 10) 
0.33325695991516113 

My đoán là zeros đang sử dụng một số mẹo CPU và không thực sự ghi vào bộ nhớ để phân bổ nó. Điều này được thực hiện ngay lập tức khi nó được ghi vào. Nhưng điều đó vẫn không giải thích sự khác biệt lớn trong thời gian tạo mảng.

Tôi đang chạy Mac OS X Yosemite với phiên bản hiện tại NumPy:

>>> numpy.__version__ 
'1.9.1' 
+0

'số không' sử dụng' memset'; 'zeros_like' dường như có hiệu quả làm một' điền' mà làm một tấn vô nghĩa. Tôi đã cố gắng đuổi theo việc thực hiện thực tế nhưng nó không cần thiết. – Veedrac

+0

'số không' không sử dụng memset. –

Trả lời

10

timings của tôi trong IPython là (với một giao diện đơn giản hơn timeit):

In [57]: timeit np.zeros_like(x) 
1 loops, best of 3: 420 ms per loop 

In [58]: timeit np.zeros((12488, 7588, 3), np.uint8) 
100000 loops, best of 3: 15.1 µs per loop 

Khi tôi nhìn vào mã với IPython (np.zeros_like??) Tôi thấy:

res = empty_like(a, dtype=dtype, order=order, subok=subok) 
multiarray.copyto(res, 0, casting='unsafe') 

trong khi np.zeros là một hộp đen - mã được biên dịch thuần túy.

Thời gian cho empty là:

In [63]: timeit np.empty_like(x) 
100000 loops, best of 3: 13.6 µs per loop 

In [64]: timeit np.empty((12488, 7588, 3), np.uint8) 
100000 loops, best of 3: 14.9 µs per loop 

Vì vậy, thêm thời gian trong zeros_like là trong copy đó.

Trong các thử nghiệm của tôi, sự khác biệt về thời gian chuyển nhượng (x[]=1) là không đáng kể.

Đoán của tôi là zeros, ones, empty là tất cả các sáng tạo được biên dịch sớm. empty_like được thêm vào như một sự tiện lợi, chỉ cần vẽ hình dạng và nhập thông tin từ đầu vào của nó. zeros_like được viết với nhiều hơn một mắt hướng tới bảo trì lập trình dễ dàng (tái sử dụng empty_like) hơn cho tốc độ.

np.onesnp.full cũng sử dụng trình tự np.empty ... copyto và hiển thị thời gian tương tự.


https://github.com/numpy/numpy/blob/master/numpy/core/src/multiarray/array_assign_scalar.c dường như là tập tin mà bản sao một vô hướng (ví dụ như 0) vào một mảng. Tôi không thấy sử dụng memset.

https://github.com/numpy/numpy/blob/master/numpy/core/src/multiarray/alloc.c có các cuộc gọi đến malloccalloc.

https://github.com/numpy/numpy/blob/master/numpy/core/src/multiarray/ctors.c - nguồn cho zerosempty.Cả hai gọi PyArray_NewFromDescr_int nhưng một kết thúc bằng cách sử dụng npy_alloc_cache_zeronpy_alloc_cache khác.

npy_alloc_cache trong alloc.c gọi alloc. npy_alloc_cache_zero gọi npy_alloc_cache theo sau là memset. Mã số trong alloc.c còn bị nhầm lẫn với tùy chọn THREAD.

Thông tin thêm về các calloc v malloc+memset khác biệt tại địa chỉ: Why malloc+memset is slower than calloc?

Nhưng với bộ nhớ đệm và thu gom rác thải, tôi tự hỏi liệu sự khác biệt calloc/memset áp dụng.


thử nghiệm này đơn giản với gói memory_profile hỗ trợ tuyên bố rằng zerosempty cấp phát bộ nhớ 'on-the-fly', trong khi zeros_like phân bổ tất cả mọi thứ lên phía trước:

N = (1000, 1000) 
M = (slice(None, 500, None), slice(500, None, None)) 

Line # Mem usage Increment Line Contents 
================================================ 
    2 17.699 MiB 0.000 MiB @profile 
    3        def test1(N, M): 
    4 17.699 MiB 0.000 MiB  print(N, M) 
    5 17.699 MiB 0.000 MiB  x = np.zeros(N) # no memory jump 
    6 17.699 MiB 0.000 MiB  y = np.empty(N) 
    7 25.230 MiB 7.531 MiB  z = np.zeros_like(x) # initial jump 
    8 29.098 MiB 3.867 MiB  x[M] = 1  # jump on usage 
    9 32.965 MiB 3.867 MiB  y[M] = 1 
    10 32.965 MiB 0.000 MiB  z[M] = 1 
    11 32.965 MiB 0.000 MiB  return x,y,z 
+0

Nhìn vào bên trong là hướng đi đúng hướng. Lạ lùng rằng 'zeros_like' không chỉ gọi tới' số không'. Tôi đã chạy các bài kiểm tra phân công nhiều lần và luôn nhận được sự khác biệt nhỏ nhưng nhanh hơn đáng kể so với các mảng 'zeros_like'. –

+3

Các dự đoán trên không phải là lý do. Sự khác biệt thực sự là liệu bộ nhớ zeroing còn lại cho hệ thống con VM của hệ điều hành hay được thực hiện bởi chính quá trình đó. –

+0

Lời giải thích 'calloc/memset' có vẻ hợp lý, nhưng tôi gặp sự cố khi xác nhận rằng trong mã' numpy'. – hpaulj

10

OS Modern cấp phát bộ nhớ hầu nghĩa là bộ nhớ chỉ được cấp cho một quá trình khi nó được sử dụng lần đầu tiên. zeros lấy bộ nhớ từ hệ điều hành để hệ điều hành luôn hoạt động khi nó được sử dụng lần đầu tiên. Mặt khác, zeros_like sẽ tự lấp đầy bộ nhớ được cấp phát bằng các số 0. Cả hai cách đều yêu cầu cùng một lượng công việc --- chỉ với zeros_like số 0 được thực hiện trả trước, trong khi zeros kết thúc bằng cách thực hiện nó khi đang di chuyển.

Về mặt kỹ thuật, trong C, sự khác biệt là gọi calloc so với malloc+memset.

Các vấn đề liên quan