O que significam os termos "CPU bound" e "I / O bound"?
O que significam os termos "CPU bound" e "I / O bound"?
Respostas:
É bem intuitivo:
Um programa é vinculado à CPU se for mais rápido se a CPU for mais rápida, ou seja, ele passa a maior parte do tempo simplesmente usando a CPU (fazendo cálculos). Um programa que calcula novos dígitos de π normalmente será vinculado à CPU, são apenas números.
Um programa é vinculado à E / S se for mais rápido se o subsistema de E / S for mais rápido. Qual sistema de E / S exato se destina pode variar; Eu normalmente o associo ao disco, mas é claro que redes ou comunicação em geral também são comuns. Um programa que procura alguns dados em um arquivo enorme pode se tornar vinculado à E / S, já que o gargalo é a leitura dos dados do disco (na verdade, este exemplo talvez seja antiquado atualmente, com centenas de MB / s provenientes de SSDs).
Limite da CPU significa que a taxa na qual o processo progride é limitada pela velocidade da CPU. Uma tarefa que executa cálculos em um pequeno conjunto de números, por exemplo, multiplicando matrizes pequenas, provavelmente está vinculada à CPU.
Limite de E / S significa que a taxa na qual um processo progride é limitada pela velocidade do subsistema de E / S. Uma tarefa que processa dados do disco, por exemplo, contando o número de linhas em um arquivo provavelmente está vinculada à E / S.
Memória ligada significa que a taxa na qual um processo progride é limitada pela quantidade de memória disponível e pela velocidade do acesso à memória. É provável que uma tarefa que processe grandes quantidades de dados na memória, por exemplo, multiplicar matrizes grandes, seja Limite de Memória.
Limite de cache significa a taxa na qual um progresso do processo é limitado pela quantidade e velocidade do cache disponível. Uma tarefa que simplesmente processa mais dados do que cabe no cache será vinculada ao cache.
O limite de E / S seria mais lento que o limite de memória e o cache seria mais lento que o limite da CPU.
A solução para vincular a E / S não é necessariamente obter mais memória. Em algumas situações, o algoritmo de acesso pode ser projetado com base nas limitações de E / S, memória ou cache. Consulte Algoritmos inconscientes de cache .
Multiencadeamento
Nesta resposta, investigarei um caso de uso importante para distinguir entre trabalho limitado de CPU versus IO: ao escrever código multiencadeado.
Exemplo de ligação de E / S de RAM: soma vetorial
Considere um programa que soma todos os valores de um único vetor:
#define SIZE 1000000000
unsigned int is[SIZE];
unsigned int sum = 0;
size_t i = 0;
for (i = 0; i < SIZE; i++)
/* Each one of those requires a RAM access! */
sum += is[i]
Paralelamente, dividir a matriz igualmente para cada um de seus núcleos é de utilidade limitada em desktops modernos comuns.
Por exemplo, no meu Ubuntu 19.04, o laptop Lenovo ThinkPad P51 com CPU: CPU Intel Core i7-7820HQ (4 núcleos / 8 threads), RAM: 2x Samsung M471A2K43BB1-CRC (2x 16GiB) obtenho resultados como este:
Observe que há muita variação entre as execuções. Mas não posso aumentar ainda mais o tamanho do array, pois já estou no 8GiB e não estou com disposição para estatísticas em várias execuções hoje. No entanto, isso pareceu uma execução típica depois de várias execuções manuais.
Código de referência:
pthread
Código-fonte POSIX C usado no gráfico.
E aqui está uma versão em C ++ que produz resultados análogos.
Não conheço arquitetura de computador suficiente para explicar completamente o formato da curva, mas uma coisa é clara: o cálculo não se torna 8x mais rápido do que o esperado, devido a eu usar todos os meus 8 threads! Por alguma razão, 2 e 3 threads foram os melhores, e adicionar mais apenas torna as coisas muito mais lentas.
Compare isso ao trabalho vinculado à CPU, que na verdade fica 8 vezes mais rápido: o que 'real', 'user' e 'sys' significam na saída do tempo (1)?
O motivo é que todos os processadores compartilham um único barramento de memória vinculado à RAM:
CPU 1 --\ Bus +-----+
CPU 2 ---\__________| RAM |
... ---/ +-----+
CPU N --/
portanto, o barramento de memória se torna rapidamente o gargalo, não a CPU.
Isso acontece porque adicionar dois números requer um único ciclo da CPU, as leituras de memória levam cerca de 100 ciclos da CPU no hardware de 2016.
Portanto, o trabalho da CPU realizado por byte de dados de entrada é muito pequeno e chamamos isso de processo vinculado à IO.
A única maneira de acelerar ainda mais esse cálculo seria acelerar o acesso individual à memória com um novo hardware de memória, por exemplo, memória multicanal .
Atualizar para um clock de CPU mais rápido, por exemplo, não seria muito útil.
Outros exemplos
a multiplicação de matrizes é vinculada à CPU na RAM e nas GPUs. A entrada contém:
2 * N**2
números, mas:
N ** 3
são feitas multiplicações e isso é suficiente para que a paralelização valha a pena para N. prático grande
É por isso que existem bibliotecas de multiplicação de matrizes de CPU paralelas como as seguintes:
O uso do cache faz uma grande diferença na velocidade das implementações. Veja, por exemplo, este exemplo didático de comparação de GPU .
Veja também:
Rede é o exemplo de protótipo de IO-bound.
Mesmo quando enviamos um único byte de dados, ainda leva muito tempo para chegar ao destino.
Paralelamente solicitações de rede pequenas, como solicitações HTTP, podem oferecer enormes ganhos de desempenho.
Se a rede já estiver em plena capacidade (por exemplo, baixar um torrent), a paralelização ainda poderá aumentar, melhorando a latência (por exemplo, você pode carregar uma página da Web "ao mesmo tempo").
Uma operação encadernada de CPU C ++ fictícia que pega um número e o tritura muito:
A classificação parece ser CPU com base no seguinte experimento: Os algoritmos paralelos do C ++ 17 já estão implementados? que mostrou uma melhoria de desempenho de 4x para classificação paralela, mas eu gostaria de ter uma confirmação mais teórica também
Como descobrir se você está vinculado à CPU ou IO
E / S não-RAM vinculada como disco, rede: e ps aux
, em seguida, verifique se CPU% / 100 < n threads
. Se sim, você está vinculado à IO, por exemplo, o bloqueio de read
s está apenas aguardando dados e o planejador está pulando esse processo. Em seguida, use outras ferramentas sudo iotop
para decidir qual é o problema exatamente.
Ou, se a execução for rápida e você parametrizar o número de threads, poderá ver facilmente a partir time
desse desempenho, à medida que o número de threads aumenta para o trabalho vinculado à CPU: O que significam 'real', 'user' e 'sys' em a saída do tempo (1)?
Limite de RAM-IO: mais difícil de dizer, já que o tempo de espera da RAM está incluído nas CPU%
medições, veja também:
Algumas opções:
GPUs
As GPUs têm um gargalo de IO quando você transfere os dados de entrada da RAM legível da CPU normal para a GPU.
Portanto, as GPUs podem ser melhores que as CPUs para aplicativos vinculados à CPU.
No entanto, uma vez que os dados são transferidos para a GPU, eles podem operar nesses bytes mais rapidamente que a CPU, porque a GPU:
possui mais localização de dados que a maioria dos sistemas de CPU e, portanto, os dados podem ser acessados mais rapidamente em alguns núcleos do que em outros
explora o paralelismo dos dados e sacrifica a latência, ignorando apenas os dados que não estão prontos para serem operados imediatamente.
Como a GPU precisa operar com grandes dados de entrada paralela, é melhor pular para os próximos dados que possam estar disponíveis, em vez de esperar que os dados atuais estejam disponíveis e bloquear todas as outras operações, como a CPU geralmente faz.
Portanto, a GPU pode ser mais rápida que uma CPU se o seu aplicativo:
Essas escolhas de design visavam originalmente a aplicação de renderização em 3D, cujas etapas principais são mostradas em O que são shaders no OpenGL e para que precisamos deles?
e assim concluímos que esses aplicativos são vinculados à CPU.
Com o advento da GPGPU programável, podemos observar vários aplicativos GPGPU que servem como exemplos de operações ligadas à CPU:
Processamento de imagem com shaders GLSL?
As operações locais de processamento de imagem, como um filtro de desfoque, são altamente paralelas por natureza.
É possível criar um mapa de calor a partir de dados de pontos a 60 vezes por segundo?
Plotagem de gráficos de mapa de calor se a função plotada for suficientemente complexa.
https://www.youtube.com/watch?v=fE0P6H8eK4I "Dinâmica de fluidos em tempo real: CPU vs GPU" por Jesús Martín Berlanga
Resolvendo equações diferenciais parciais, como a equação de Navier Stokes da dinâmica de fluidos:
Veja também:
Bloqueio global de intérpretes CPython (GIL)
Como um estudo de caso rápido, quero destacar o GIL (Global Interpreter Lock) do Python: O que é o GIL (Global Interpreter Lock ) no CPython?
Esse detalhe de implementação do CPython impede que vários threads do Python utilizem com eficiência o trabalho associado à CPU. Os documentos do CPython dizem:
Detalhes da implementação do CPython: No CPython, devido ao Bloqueio Global de Intérpretes, apenas um encadeamento pode executar o código Python de uma só vez (mesmo que certas bibliotecas orientadas ao desempenho possam superar essa limitação). Se você deseja que seu aplicativo faça melhor uso dos recursos computacionais de máquinas com vários núcleos, é recomendável usar
multiprocessing
ouconcurrent.futures.ProcessPoolExecutor
. No entanto, o encadeamento ainda é um modelo apropriado se você deseja executar várias tarefas ligadas a E / S simultaneamente.
Portanto, aqui temos um exemplo em que o conteúdo vinculado à CPU não é adequado e o limite de E / S é.
CPU bound significa que o programa está com gargalo na CPU ou unidade de processamento central, enquanto E / S limite de significa que o programa está com gargalo de E / S ou entrada / saída, como leitura ou gravação em disco, rede, etc.
Em geral, ao otimizar programas de computador, tenta-se buscar o gargalo e eliminá-lo. Saber que seu programa está vinculado à CPU ajuda, para que você não otimize desnecessariamente outra coisa.
[E por "gargalo", quero dizer o que torna seu programa mais lento do que seria.]
Outra maneira de expressar a mesma ideia:
Se acelerar a CPU não acelerar o seu programa, pode estar ligado à E / S.
Se acelerar a E / S (por exemplo, usar um disco mais rápido) não ajudar, seu programa pode estar ligado à CPU.
(Eu usei "pode ser" porque você precisa levar outros recursos em consideração. A memória é um exemplo.)
Quando seu programa está aguardando E / S (ou seja, uma leitura / gravação em disco ou leitura / gravação em rede etc), a CPU está livre para executar outras tarefas, mesmo que seu programa esteja parado. A velocidade do seu programa dependerá principalmente da rapidez com que a IO pode acontecer e, se você quiser acelerá-la, precisará acelerar a E / S.
Se o seu programa estiver executando muitas instruções do programa e não aguardando E / S, é dito que ele está vinculado à CPU. Acelerar a CPU tornará o programa mais rápido.
Em qualquer um dos casos, a chave para acelerar o programa pode não ser a velocidade do hardware, mas otimizar o programa para reduzir a quantidade de E / S ou CPU necessária, ou fazer I / O enquanto também consome muita CPU coisa.
O limite de E / S refere-se a uma condição na qual o tempo necessário para concluir um cálculo é determinado principalmente pelo período gasto aguardando a conclusão das operações de entrada / saída.
É o oposto de uma tarefa vinculada à CPU. Essa circunstância ocorre quando a taxa na qual os dados são solicitados é mais lenta que a taxa em que são consumidos ou, em outras palavras, é gasto mais tempo solicitando dados do que processando-os.
O núcleo da programação assíncrona são os objetos Task e Task, que modelam operações assíncronas. Eles são suportados pelo assíncrono e aguardam palavras-chave. O modelo é bastante simples na maioria dos casos:
Para código vinculado à E / S, você espera uma operação que retorne uma tarefa ou tarefa dentro de um método assíncrono.
Para código vinculado à CPU, você aguarda uma operação que é iniciada em um encadeamento em segundo plano com o método Task.Run.
A palavra-chave wait é onde a mágica acontece. Ele fornece controle para o chamador do método que foi executado em espera e, em última análise, permite que uma interface do usuário seja responsiva ou que um serviço seja elástico.
Exemplo vinculado de E / S: Fazendo Download de Dados de um Serviço da Web
private readonly HttpClient _httpClient = new HttpClient();
downloadButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI as the request
// from the web service is happening.
//
// The UI thread is now free to perform other work.
var stringData = await _httpClient.GetStringAsync(URL);
DoSomethingWithData(stringData);
};
Exemplo vinculado à CPU: executando um cálculo para um jogo
private DamageResult CalculateDamageDone()
{
// Code omitted:
//
// Does an expensive calculation and returns
// the result of that calculation.
}
calculateButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI while CalculateDamageDone()
// performs its work. The UI thread is free to perform other work.
var damageResult = await Task.Run(() => CalculateDamageDone());
DisplayDamage(damageResult);
};
Os exemplos acima mostraram como você pode usar o assíncrono e aguardar o trabalho vinculado à E / S e à CPU. É fundamental que você possa identificar quando um trabalho que você precisa executar é vinculado à E / S ou à CPU, porque isso pode afetar muito o desempenho do seu código e pode levar ao mau uso de certas construções.
Aqui estão duas perguntas que você deve fazer antes de escrever qualquer código:
Seu código estará "aguardando" algo, como dados de um banco de dados?
- Se sua resposta for "sim", seu trabalho será vinculado à E / S.
Seu código estará executando um cálculo muito caro?
- Se você respondeu "sim", seu trabalho é vinculado à CPU.
Se o seu trabalho for vinculado à E / S, use async e aguarde sem o Task.Run . Você não deve usar a biblioteca paralela de tarefas. O motivo disso está descrito no artigo Async in Depth .
Se o trabalho que você tem é vinculado à CPU e se preocupa com a capacidade de resposta, use async e aguarde, mas inicie o trabalho em outro thread com o Task.Run. Se o trabalho for apropriado para simultaneidade e paralelismo, considere também usar a Biblioteca Paralela de Tarefas .
Um aplicativo é vinculado à CPU quando o desempenho aritmético / ponto lógico / ponto flutuante (A / L / FP) durante a execução se aproxima principalmente do desempenho teórico máximo do processador (dados fornecidos pelo fabricante e determinados pelas características do processador: número de núcleos, frequência, registros, ALUs, FPUs, etc.).
O desempenho da espiada é muito difícil de ser alcançado em aplicativos do mundo real, por não dizer impossível. A maioria dos aplicativos acessa a memória em diferentes partes da execução e o processador não está executando operações de A / L / FP durante vários ciclos. Isso se chama Limitação de Von Neumann devido à distância que existe entre a memória e o processador.
Se você deseja estar próximo do pico de desempenho da CPU, uma estratégia pode ser a reutilização da maioria dos dados na memória cache, para evitar a necessidade de dados da memória principal. Um algoritmo que explora esse recurso é a multiplicação matriz-matriz (se as duas matrizes puderem ser armazenadas na memória cache). Isso acontece porque, se as matrizes tiverem tamanho n x n
, você precisará fazer 2 n^3
operações usando apenas 2 n^2
números de dados FP. Por outro lado, a adição de matriz, por exemplo, é um aplicativo menos vinculado à CPU ou mais vinculado à memória do que a multiplicação da matriz, pois requer apenas n^2
FLOPs com os mesmos dados.
Na figura a seguir, são mostrados os FLOPs obtidos com algoritmos ingênuos para adição de matriz e multiplicação de matriz em um Intel i5-9300H:
Observe que, como esperado, o desempenho da multiplicação da matriz é maior que a adição da matriz. Esses resultados podem ser reproduzidos executando test/gemm
e test/matadd
disponíveis neste repositório .
Sugiro também ver o vídeo dado por J. Dongarra sobre esse efeito.
Processo vinculado de E / S: - Se a maior parte da vida útil de um processo é gasta no estado de E / S, então o processo é um processo vinculado a / o.exemplo: -calculator, internet explorer
Processo vinculado à CPU: - Se a maior parte da vida útil do processo é gasta na CPU, é um processo vinculado à CPU.