6.19. 성능¶
numpy를 카메라에서 빠르게 동작하게 하는 설계 결정들 – 전체 배열 단위의 라이브러리 호출, 패킹된 타입 버퍼, 원본과 데이터를 공유하는 뷰 – 은 동시에 알아둘 가치가 있는 일련의 습관도 드러냅니다. 형태(shape)와 스트라이드(strides) 페이지에서는 이미 마지막 축 레이아웃 규칙을 다루었으며, 이 페이지에서는 스트리밍 루프에서 가장 중요한 할당 및 dtype 습관을 정리합니다.
6.19.1. 적절한 dtype 선택¶
모든 생성자의 기본 dtype은 float입니다. ADC 샘플, 이미지 픽셀, 센서 측정값처럼 본질적으로 8비트나 16비트인 데이터의 경우, dtype=을 정수 타입 중 하나로 명시적으로 전달하십시오:
adc = np.array(adc_samples, dtype=np.uint16)
RAM 절약 효과는 4바이트 float 기본값 대비 uint16의 경우 2배, uint8의 경우 4배입니다. 또한 numpy 내부의 정수 코드 경로가 일반 float 경로보다 더 효율적이므로 연산도 더 빠르게 실행됩니다. Dtype에서 다룬 정수 오버플로 규칙이 적용되므로 – 오버플로가 발생할 수 있는 산술 연산 전에 더 넓은 타입으로 캐스팅하십시오.
6.19.2. 이터러블보다 ndarray를 선호하십시오¶
대부분의 리덕션과 유니버설 함수는 이터러블이나 ndarray 중 하나를 받습니다:
np.sum([1, 2, 3, 4, 5]) # works, but slow
np.sum(np.array([1, 2, 3, 4, 5])) # ~3x faster
이터러블 형태는 numpy가 입력을 한 번에 하나의 Python 객체씩 거쳐가며 각각을 사용하기 전에 숫자로 변환하도록 강제합니다. ndarray에 대해서는 변환이 이미 완료되어 있어 호출이 패킹된 버퍼를 통해 곧바로 실행됩니다.
같은 데이터가 두 번 이상 사용될 때는 ndarray를 한 번 만들어 전달하십시오. 데이터가 Python 리스트로만 존재하고 한 번만 소비될 때는 변환 비용이 속도 향상을 능가할 수 있습니다 – array() 생성자 자체가 리스트를 순회하며 할당해야 하기 때문입니다.
6.19.3. 복사본보다 뷰를 선호하십시오¶
슬라이싱, 고차원 배열의 단일 축 인덱싱, reshape(), transpose(), frombuffer()는 모두 원본과 데이터를 공유하는 뷰를 반환합니다. 이들은 사실상 비용이 없습니다.
copy(), flatten(), 불리언 인덱싱(a[mask]), 그리고 모든 산술 표현식은 복사본을 할당합니다. 독립적인 버퍼가 정말로 필요할 때만 사용하십시오.
확실하지 않을 때는 ndinfo()가 기저 버퍼의 위치를 출력합니다. 동일한 주소를 보고하는 두 배열은 데이터를 공유합니다. 전체 뷰 대 복사본 표는 뷰와 복사본에 있습니다.
6.19.4. 한 번 할당하고, 그 후 기록하십시오¶
카메라에서 가장 큰 성능 함정은 초당 여러 번 실행되는 루프 안에서 새 배열을 할당하는 것입니다. 새로운 ndarray마다 카메라에 RAM을 요청하며, 빈번한 새 할당은 RAM을 낭비합니다.
대부분의 유니버설 함수는 out=을 받아 결과를 이미 존재하는 배열에 기록할 수 있습니다:
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()는 같은 이유로 buffer=를 받습니다. spectrogram()과 from_int32_buffer() 계열의 변환기들은 out=과 scratchpad= 모두를 받습니다. 모든 것을 한 번 할당하고 재사용하십시오.
6.19.5. 제자리(in-place) 연산자를 사용하십시오¶
b = b + 1은 b 크기의 임시 배열을 할당하고, 복사한 뒤, 재할당합니다. b += 1은 b를 직접 수정합니다:
# makes a temporary
b = b + 1
# no temporary
b += 1
같은 개념이 복합 표현식에도 적용됩니다. a + b * c는 b * c를 위한 임시 배열을 할당합니다. 표현식을 미리 할당된 버퍼에 기록하는 단순한 하위 할당으로 나누면 임시 배열이 제거됩니다:
# one temporary for (a + b), another for the ``* 2``
out = (a + b) * 2
# zero temporaries
out[:] = a
out += b
out *= 2
6.19.6. 결과를 만들어내되, 거기에 덧붙이지 마십시오¶
ndarray에는 append가 없습니다 – 의도적인 것입니다. 배열을 키운다는 것은 더 큰 새 버퍼를 할당하고 기존 내용을 그 안으로 복사한다는 의미입니다. 마이크로컨트롤러에서는 최종 크기를 미리 할당하고 채워 넣으십시오
out = np.zeros(N, dtype=np.float)
for i in range(N):
out[i] = some_calculation(i)
N을 정말로 미리 알 수 없을 때는 Python list에 기록하고 마지막에 array()로 한 번에 변환하십시오.
6.19.7. 새 배열 대신 슬라이스 할당¶
여러 개의 “다른 배열의 조각들로부터 새 배열을 만드는” 패턴은 매번 새로 할당하는 대신 미리 할당된 버퍼로의 슬라이스 할당으로 표현할 수 있습니다.
샘플 스트림에 대한 롤링 윈도우 – 이동 평균 필터의 기반 – 가 대표적인 사례입니다. 버퍼는 마지막 N개의 샘플을 보관하며, 매 반복마다 가장 오래된 것을 버리고 가장 새로운 것을 덧붙입니다. 명백한 형태는 매 반복마다 버퍼를 다시 만듭니다:
while True:
sample = read_sample()
buf = np.concatenate((buf[1:], # new buffer every loop
np.array([sample])))
avg = np.mean(buf)
이것은 샘플당 새 할당이며 – N - 1개 요소의 복사이기도 합니다. 슬라이스 할당 형태는 제자리에서 이동시킵니다:
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:]이 흥미로운 행입니다. 동일한 버퍼에 대한 두 개의 겹치는 뷰로, 오른쪽 슬라이스는 한쪽 끝에서 읽혀 다른 쪽 끝으로 기록됩니다. numpy는 제자리 이동을 안전하게 만드는 순서로 기저 메모리를 순회합니다. 루프 안에서 새 배열이 할당되는 일은 전혀 없습니다.
6.19.8. 스트리밍 루프에서 불리언 마스크를 주의하십시오¶
불리언 인덱싱과 where()는 호출할 때마다 새 배열을 생성합니다 – 결과의 크기가 데이터에 따라 달라지므로 미리 할당된 버퍼로는 그 할당을 흡수할 수 없습니다. 스트리밍 루프에서 반복적으로 마스크를 만들면 일회용 배열로 RAM이 가득 찹니다. 주기적인 gc.collect()가 공간을 회수합니다:
import gc
for i in range(1000):
mask = a < threshold
_ = a[mask]
if i % 100 == 0:
gc.collect()
같은 주의 사항이 (a > lo) & (a < hi) 같은 복합 불리언 표현식에도 적용됩니다 – 각 연산자가 새 bool 배열을 할당합니다. 마스크를 재사용할 때는 한 번 만들어 보관하십시오:
mask = a < threshold
foo[mask] = 0
bar[mask] = 1