6.19. Desempenho¶
As mesmas decisões de desenho que tornam o numpy rápido na câmara – chamadas de biblioteca em arrays inteiros, buffers tipificados compactos, vistas que partilham dados com a sua fonte – também expõem um conjunto de hábitos que vale a pena conhecer. A página Forma e strides já abordou a regra de disposição no último eixo; esta página cataloga os hábitos de alocação e dtype que mais importam num ciclo de processamento contínuo.
6.19.1. Escolha um dtype razoável¶
O dtype predefinido de cada construtor é float. Para dados que são naturalmente de 8 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 poupança 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 corre mais rapidamente porque os caminhos de código inteiro dentro do numpy são mais enxutos do que os genéricos de vírgula flutuante. A regra de overflow inteiro abordada em Dtypes aplica-se – converta para um tipo mais largo antes de aritmética que possa causar overflow.
6.19.2. Prefira ndarray a um iterável¶
A maioria das reduções e funções universais aceita tanto um iterável como 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 iterável obriga o numpy a percorrer a entrada um objeto Python de cada vez, convertendo cada um para um número antes de o poder utilizar. Com um ndarray, a conversão já está feita e a chamada percorre diretamente o buffer compacto.
Quando os mesmos dados são utilizados mais do que uma vez, construa o ndarray uma vez e passe-o adiante. Quando os dados existem apenas como uma lista Python e são consumidos uma vez, o custo de conversão pode superar a aceleração – o próprio construtor array() tem de percorrer a lista e alocar memória.
6.19.3. Prefira vistas a cópias¶
O fatiamento, a indexação de eixo único de um array de maior rank, reshape(), transpose() e frombuffer() devolvem vistas que partilham dados com a fonte. São essencialmente gratuitas.
copy(), flatten(), a indexação booleana (a[mask]) e qualquer expressão aritmética alocam uma cópia. Recorra a estas apenas quando for genuinamente necessário um buffer independente.
Em caso de dúvida, ndinfo() imprime a localização do buffer subjacente; dois arrays que reportam o mesmo endereço partilham os seus dados. A tabela completa de vista vs. cópia encontra-se em Vistas e cópias.
6.19.4. Alocar uma vez, depois escrever¶
A maior armadilha de desempenho na câmara é alocar arrays novos dentro de um ciclo que corre muitas vezes por segundo. Cada novo ndarray solicita RAM à câmara, e alocações frequentes desperdiçam-na.
A maioria das funções universais aceita out= para que o resultado possa ser escrito num array já existente:
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= pelo mesmo motivo; spectrogram() e os conversores no estilo from_int32_buffer() aceitam tanto out= como scratchpad=. Aloque tudo uma vez e reutilize.
6.19.5. Utilize operadores in-place¶
b = b + 1 aloca um temporário do tamanho de b, copia e volta a atribuir. b += 1 modifica b diretamente:
# makes a temporary
b = b + 1
# no temporary
b += 1
A mesma ideia aplica-se a expressões compostas. a + b * c aloca um temporário para b * c. Dividir a expressão em sub-atribuições simples que escrevem num 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 lhe acrescente elementos¶
ndarray não tem append – intencionalmente. Aumentar um array implicaria alocar um novo buffer maior e copiar o conteúdo antigo para ele. Num 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 genuinamente não é conhecido antecipadamente, escreva para uma list Python e converta uma vez no 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 partes de outros» podem ser expressos como atribuições por fatia num 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 contém as últimas N amostras; em cada iteração descarta a mais antiga e acrescenta a mais recente. 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: duas vistas sobrepostas no mesmo buffer, a fatia do lado direito lida de uma extremidade e escrita na outra. O numpy percorre a memória subjacente na ordem que torna o deslocamento in-place seguro. Nunca é alocado um novo array dentro do ciclo.
6.19.8. Atenção às máscaras booleanas em ciclos de processamento contínuo¶
A indexação booleana e where() produzem um novo array a cada chamada – o tamanho do resultado depende dos dados, por isso nenhum buffer pré-alocado pode absorver a alocação. A construção repetida de máscaras num ciclo de processamento contínuo enche a RAM com 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()
O mesmo aviso aplica-se 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