Para corrigir a metáfora do EightBitTony:
"Why does this happen?" is kind of easy to answer. Imagine you have two swimming pools, one full and one empty. You want to move all the water from one to the other, and have 4 buckets. The most efficient
number of people is 4.
If you have 1-3 people then you're missing out on using some buckets. If you have 5 or more people, then at least one of those people
is stuck waiting for a bucket. Adding
more and more people ... doesn't speed up
the activity.
So you want to have as many people as can do some work (use a bucket) simultaneously.
Uma pessoa aqui é um thread e um bucket representa qualquer recurso de execução que seja o gargalo. Adicionando mais tópicos não ajuda se eles não podem fazer nada. Além disso, devemos enfatizar que passar um balde de uma pessoa para outra é normalmente mais lento do que uma única pessoa carregando apenas o balde na mesma distância. Ou seja, dois segmentos que se revezam em um núcleo normalmente realizam menos trabalho do que um único segmento executando o dobro do tempo: isso é devido ao trabalho extra feito para alternar entre os dois segmentos.
Se o recurso de execução limitante (bucket) é uma CPU, ou um núcleo, ou um pipeline de instrução hyper-threaded para seus propósitos depende de qual parte da arquitetura é o seu fator limitante. Note também que estamos assumindo que os threads são inteiramente independentes. Este é apenas o caso se eles compartilham dados não (e evitam colisões de cache).
Como algumas pessoas sugeriram, para E / S, o recurso de limitação pode ser o número de operações de E / S que podem ser executadas em fila: isso pode depender de vários fatores de hardware e kernel, mas pode ser muito maior que o número de núcleos. Aqui, a troca de contexto, que é tão cara quando comparada ao código de execução, é bem barata se comparada ao código de entrada / saída. Infelizmente eu acho que a metáfora vai ficar completamente fora de controle se eu tentar justificar isso com baldes.
Note que o comportamento óptimo com código I / O é normalmente ainda para ter no máximo um thread por pipeline / core / CPU. No entanto, você precisa escrever um código de E / S assíncrono ou síncrono / não-bloqueador, e o aprimoramento de desempenho relativamente pequeno nem sempre justificará a complexidade extra.
PS. Meu problema com a metáfora original do corredor é que ela sugere strongmente que você deve ter 4 filas de pessoas, com 2 filas carregando lixo e 2 retornando para coletar mais. Então você pode tornar cada fila quase tão longa quanto o corredor, e adicionar pessoas fez acelerar o algoritmo (você basicamente transformou todo o corredor em uma esteira).
Na verdade, esse cenário é muito semelhante à descrição padrão da relação entre a latência e o tamanho da janela na rede TCP, e é por isso que ela se sobressaltou em mim.