6.19. Desempenho¶
As mesmas decisões de design que tornam o numpy rápido na câmera – chamadas de biblioteca sobre o array inteiro, buffers tipados e compactados, views que compartilham dados com sua origem – também expõem um conjunto de hábitos que vale a pena conhecer. A página Formato e strides já abordou a regra de layout do último eixo; esta página cataloga os hábitos de alocação e de dtype que mais importam em um laço de streaming.
6.19.1. Escolha um dtype razoável¶
O dtype padrão de todo construtor é float. Para dados que são naturalmente de 8 bits ou 16 bits – amostras de ADC, pixels de imagem, leituras de sensor – passe dtype= explicitamente para um dos tipos inteiros:
adc = np.array(adc_samples, dtype=np.uint16)
A economia de RAM é de 2x para uint16 e de 4x para uint8 em relação ao padrão float de 4 bytes. A matemática também roda mais rápido porque os caminhos de código inteiro dentro do numpy são mais enxutos do que os genéricos de ponto flutuante. A regra de overflow de inteiros abordada em Dtypes se aplica – faça o cast para um tipo mais largo antes de uma operação aritmética que possa causar overflow.
6.19.2. Prefira um ndarray a um iterável¶
A maioria das reduções e funções universais aceita tanto um iterável quanto um ndarray
np.sum([1, 2, 3, 4, 5]) # works, but slow
np.sum(np.array([1, 2, 3, 4, 5])) # ~3x faster
A forma com iterável força o numpy a percorrer a entrada um objeto Python por vez, convertendo cada um em número antes de poder usá-lo. Com um ndarray a conversão já está feita e a chamada percorre diretamente o buffer compactado.
Quando os mesmos dados são usados mais de uma vez, construa o ndarray uma única vez e passe-o adiante. Quando os dados existem apenas como uma lista Python e são consumidos uma única vez, o custo da conversão pode superar o ganho de velocidade – o próprio construtor array() precisa percorrer a lista e alocar.
6.19.3. Prefira views a cópias¶
Fatiamento, indexação de um único eixo de um array de posto mais alto, reshape(), transpose() e frombuffer() retornam todos views que compartilham dados com a origem. Elas são essencialmente gratuitas.
copy(), flatten(), indexação booleana (a[mask]) e qualquer expressão aritmética alocam uma cópia. Recorra a elas apenas quando um buffer independente for realmente necessário.
Em caso de dúvida, ndinfo() imprime a localização do buffer subjacente; dois arrays que reportam o mesmo endereço compartilham seus dados. A tabela completa de view-vs-cópia está em Views e cópias.
6.19.4. Aloque uma vez, depois escreva¶
A maior armadilha de desempenho na câmera é alocar arrays novos dentro de um laço que roda muitas vezes por segundo. Cada novo ndarray pede RAM à câmera, e alocações novas frequentes a desperdiçam.
A maioria das funções universais aceita out= para que o resultado possa ser escrito em um array que já existe:
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() aceita buffer= pela mesma razão; spectrogram() e os conversores no estilo from_int32_buffer() aceitam tanto out= quanto scratchpad=. Aloque tudo uma vez e reutilize.
6.19.5. Use operadores in-place¶
b = b + 1 aloca um temporário do tamanho de b, copia e reatribui. b += 1 modifica b diretamente:
# makes a temporary
b = b + 1
# no temporary
b += 1
A mesma ideia se aplica a expressões compostas. a + b * c aloca um temporário para b * c. Dividir a expressão em sub-atribuições simples escrevendo em um buffer pré-alocado elimina os temporários:
# one temporary for (a + b), another for the ``* 2``
out = (a + b) * 2
# zero temporaries
out[:] = a
out += b
out *= 2
6.19.6. Construa o resultado, não faça append a ele¶
ndarray não tem append – de propósito. Aumentar um array significaria alocar um buffer novo e maior e copiar o conteúdo antigo para ele. Em um microcontrolador, pré-aloque o tamanho final e preencha-o:
out = np.zeros(N, dtype=np.float)
for i in range(N):
out[i] = some_calculation(i)
Quando N realmente não é conhecido de antemão, escreva em uma list Python e converta uma única vez ao final com array().
6.19.7. Atribuição por fatia em vez de novos arrays¶
Muitos padrões de “construir um novo array a partir de pedaços de outros” podem ser expressos como atribuições por fatia em um buffer pré-alocado, em vez de uma nova alocação a cada chamada.
Uma janela deslizante sobre um fluxo de amostras – a base de um filtro de média móvel – é o caso canônico. O buffer guarda as últimas N amostras; a cada iteração descarta-se a mais antiga e adiciona-se a mais nova. A forma óbvia reconstrói o buffer a cada iteração:
while True:
sample = read_sample()
buf = np.concatenate((buf[1:], # new buffer every loop
np.array([sample])))
avg = np.mean(buf)
Isso é uma nova alocação – e uma cópia de N - 1 elementos – por amostra. A forma com atribuição por fatia desloca in-place:
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:] é a linha interessante: dois views sobrepostos no mesmo buffer, a fatia da direita lida de uma ponta e escrita na outra. O numpy percorre a memória subjacente na ordem que torna o deslocamento in-place seguro. Nenhum array novo é alocado dentro do laço.
6.19.8. Cuidado com máscaras booleanas em laços de streaming¶
A indexação booleana e where() produzem um novo array a cada chamada – o tamanho do resultado depende dos dados, então nenhum buffer pré-alocado pode absorver a alocação. A construção repetida de máscaras em um laço de streaming enche a RAM de arrays descartáveis. Um gc.collect() periódico recupera o espaço:
import gc
for i in range(1000):
mask = a < threshold
_ = a[mask]
if i % 100 == 0:
gc.collect()
A mesma ressalva se aplica a expressões booleanas compostas como (a > lo) & (a < hi) – cada operador aloca um novo array bool. Quando uma máscara é reutilizada, construa-a uma vez e guarde-a:
mask = a < threshold
foo[mask] = 0
bar[mask] = 1