Desempenho Python: loops nativos NumPy, callbacks e custos de conversão
Um teste numérico controlado apenas de CPU segue valores idênticos através de loops CPython, aritmética NumPy, um wrapper de callback Python e um loop Numba compilado.
Um teste numérico controlado apenas de CPU segue valores idênticos através de loops CPython, aritmética NumPy, um wrapper de callback Python e um loop Numba compilado. NumPy com entrada pronta teve uma grande vantagem, enquanto a conversão de e para listas Python reduziu essa vantagem para um quase empate. O episódio explica o limite de execução e como medir o resultado que um aplicativo precisa.
Leia a edição escrita (Inglês) ↗
O que este vídeo aborda
- CPython compila o código-fonte para bytecode. O trabalho repetido do interpretador/objeto e um loop numérico nativo tipado são arranjos de execução diferentes.
- A aritmética numérica NumPy pode entregar lotes tipados para operações compiladas; o loop aritmético ainda existe, e as visualizações podem ter layouts não contíguos.
- Meça a conversão e a representação de saída necessária. Nesta execução, a conversão de listas Python em arrays e vice-versa anulou a grande vantagem dos arrays prontos.
- Pequenas cargas de trabalho podem favorecer o Python comum porque os custos fixos de chamadas de array importam. A compreensão de oito valores venceu esta comparação medida.
- np.vectorize é um wrapper de conveniência que chama Python para elementos sucessivos; uma interface de lote sozinha não estabelece execução numérica nativa.
- Loops compilados com sintaxe Python também podem ser rápidos. Inclua os custos de compilação/configuração a frio quando eles importam para o chamador.
- Execução nativa, SIMD da CPU, multithreading e backends de GPU são ideias separadas. Os recursos de CPU disponíveis não rastreiam as instruções usadas em uma execução específica.
Transcrição traduzida
Traduzido da narração original em inglês. Áudio e legendas disponíveis são controlados pelo YouTube.
Por que a mesma CPU pode terminar mais cedo?
0:00 Você acha que o Python precisa de uma nova linguagem para ser rápido. A mesma CPU executou nosso lote mais rápido depois que o loop se moveu. Para onde foi o trabalho? Quando o aumento de velocidade desaparece? Qual atalho traz o Python de volta? Este é o The Daily Diff, nos bastidores. Fique de olho nesse atalho. CPython é a implementação padrão do Python.
0:17 Ele compila seu código-fonte em bytecode e, em seguida, executa essas instruções em um interpretador. NumPy é uma biblioteca numérica cujos arrays armazenam valores com um tipo de dado declarado. Sua aritmética embutida pode entregar um lote inteiro para operações compiladas. Pegue um milhão de valores de ponto flutuante, eleve cada um ao quadrado, depois adicione o valor original. Todas as versões obtêm valores idênticos e produzem respostas correspondentes. Os valores são pequenos o suficiente para manter as diferenças de arredondamento fora da disputa.
0:40 Nesta máquina, a compreensão levou cerca de trinta e três milissegundos. NumPy levou menos de um. Ambos rodaram na CPU. Isso é uma evidência útil para esta carga de trabalho, e um terrível obituário para uma linguagem de programação. Comece com o loop comum.
Que trabalho um loop Python repete?
0:53 Python obtém o próximo valor, realiza a aritmética e anexa a resposta. Então ele faz isso de novo. A multiplicação é barata. Organizar a próxima multiplicação e lidar com os objetos Python ao redor dela, adiciona trabalho a cada vez. Esta é a desmontagem real da função testada. Iteração, multiplicação e adição aparecem como instruções de bytecode. Versões e interpretadores Python fazem escolhas diferentes.
1:14 Esses tempos pertencem à versão na tela, que é Python três ponto dez. Uma lista Python armazena referências a objetos, então diferentes tipos de coisas podem compartilhar uma lista. Nosso trabalho aritmético usa números em todo o processo. Ele paga por um arranjo geral enquanto solicita um serviço repetitivo. Imagine pedir o mesmo café um milhão de vezes e preencher um formulário novo para cada xícara. A máquina de café está bem.
1:36 A papelada desenvolveu sua própria carreira.
Para onde o NumPy move o loop?
1:38 NumPy muda o contrato. Este array armazena valores de ponto flutuante de tamanho fixo com um tipo declarado. Nosso array é contíguo, então seus valores ficam juntos na memória. As visualizações podem ter outros layouts. A operação aritmética sabe que tipo de dados está recebendo. A chamada entrega o lote a uma operação nativa. Um loop ainda visita os valores dentro do código compilado. Ele pode reutilizar o conhecimento do tipo em todo esse trabalho.
2:00 Python continua sendo a pessoa que entrega o trabalho. Itamar Turner Trauring explica a distinção em Python Speed. Interfaces de lote, implementações nativas e instruções de vetor do processador são coisas separadas. O nome da função fornece muito pouca evidência.
Rápido significa GPU ou muitos núcleos de CPU?
2:13 Este resultado usou NumPy na CPU. Um backend de GPU como o CuPy escolhe um lar diferente para arrays e operações. Comprar uma GPU deixa este loop Python comum rodando onde ele já rodava. As instruções de vetor da CPU podem processar vários valores juntos. NumPy tem mecanismos para escolher kernels suportados. Nosso tempo de execução relata recursos disponíveis, mas não rastreia as instruções desta expressão. Dar todo o ganho medido às instruções de vetor superestimaria a evidência.
2:40 O carrinho de compras da GPU pode permanecer fechado. Há algo profundamente satisfatório em uma correção de desempenho que também cancela uma compra. Infelizmente, o benchmark continua.
Quando o impressionante aumento de velocidade desaparece?
2:49 Com oito valores, a compreensão venceu. Chamar o maquinário do array custou mais do que o pequeno trabalho. Arrays prontos maiores assumiram a liderança. Usamos tentativas repetidas e um processo aquecido. Novas compilações Python podem mudar o ponto de equilíbrio. Então mude o que o aplicativo precisa. Comece com uma lista Python e termine com uma nova lista Python. NumPy deve converter a entrada em um array e transformar a resposta de volta em objetos
3:11 Python. Essas etapas pertencem à conta se seu aplicativo precisar delas. Em um milhão de valores, esse caminho completo levou cerca de vinte e oito milissegundos. A compreensão levou cerca de vinte e oito também. Nossa dramática liderança se tornou um quase empate. A parte rápida era real. A vantagem dependia de permitir que os dados permanecessem em sua forma numérica.
Quais limites pertencem ao benchmark?
3:28 Siga os dados. Se a próxima operação aceita um array, mantenha o array. Se você converte repetidamente entre listas e arrays, meça essas travessias. Parar antes de uma entrega cara fornece números precisos para a pergunta errada. Multiplicar este array cria um resultado temporário. Adicionar os valores originais cria a saída. A execução nativa ainda precisa de memória e move dados.
3:47 Algumas operações aceitam um buffer de saída, reduzindo as alocações quando seu código pode reutilizá-lo com segurança. Uma linha bonita pode ordenar vários buffers grandes. Python Speed também aponta esse problema. O alocador é notavelmente indiferente à sua estética.
Um loop Python compilado também pode ser rápido?
4:00 Você também pode manter a sintaxe do loop e mudar sua execução. Numba pode compilar funções numéricas Python suportadas em código nativo. Aqui está o loop do nosso teste com seu decorador de compilador. Ele ainda diz for. O caminho caro do interpretador mudou. Após a compilação, este loop levou menos de meio milissegundo com arrays prontos. A própria compilação levou cerca de um segundo nesta execução. Esse custo de configuração foi excluído dos tempos repetidos,
4:22 então um pequeno trabalho único merece uma comparação diferente. O próprio guia do Numba tem o prazer de mostrar loops explícitos. Iniciar uma migração de linguagem antes de encontrar o caminho crítico é uma maneira elaborada de evitar abrir um profiler. Primeiro, encontre o que consome o tempo. Espera de rede, análise de objetos e aritmética em massa exigem correções diferentes. Operações de array nativas ajudam quando o cálculo e os dados se encaixam.
4:43 Meça o resultado que seu chamador realmente precisa. Agora, sobre esse atalho. Você tem uma função Python e quer que ela aceite um array. Um wrapper chamado vectorize soa como o botão de desempenho que falta. Sua documentação tem uma frase importante esperando.
O que np.vectorize realmente executa?
4:56 NumPy diz que seu wrapper vectorize é fornecido principalmente por conveniência. Sua implementação é essencialmente um loop for. O wrapper aceita lotes, mas continua chamando sua função Python para elementos sucessivos. A interface ficou mais curta. O limite de execução permaneceu ocupado. Veja os mesmos valores voltarem através do callback Python. Com arrays prontos, esta versão levou cerca de oitenta e sete milissegundos. Foi mais lento do que nossa compreensão.
5:18 Um wrapper de broadcasting útil pode preservar o trabalho exato por elemento que você estava tentando remover. Verifique a implementação por trás de uma interface de lote, mantenha dados compatíveis em sua forma útil e inclua a conversão na comparação. Loops nativos, de callback e compilados podem parecer semelhantes da cadeira do chamador. O nome vectorize fez uma promessa que seu cronômetro nunca assinou. Mantenha-o quando a conveniência é o que você precisa. Para desempenho, verifique o loop que executa.
O que você deve mudar em seu código?
5:41 Meu veredito é SHIP IT para agrupar um caminho crítico numérico medido. Mantenha os loops comuns quando eles já funcionam bem. Verifique onde o trabalho acontece e o que o próximo chamador precisa. Inscreva-se, ative o sino e diga-me nos comentários se você teria carimbado de forma diferente. E essa é a diferença de hoje. Sou Niko da Axrisi. Faça o merge com responsabilidade.
Fontes
- Universal functions (ufunc) basics — NumPy 2.2NumPy
- The N-dimensional array — NumPy 2.2NumPy
- numpy.vectorize — convenience and performance notesNumPy
- CPU/SIMD optimizations — NumPy 2.2NumPy
- dis — Disassembler for Python bytecodePython Software Foundation
- CPython 3.10.12 list representationCPython contributors
- The limits of Python vectorization as a performance techniqueItamar Turner-Trauring / Python⇒Speed
- Performance TipsNumba
- Basics of CuPyCuPy



