Como um único thread é executado em vários núcleos?

61

Estou tentando entender, em alto nível, como threads únicos são executados em vários núcleos. Abaixo está o meu melhor entendimento. Eu não acredito que esteja correto.

Com base na minha leitura do Hyper-threading , parece que o sistema operacional organiza as instruções de todos os threads de forma que eles não estejam esperando um pelo outro. Em seguida, o front-end da CPU organiza ainda mais essas instruções distribuindo um encadeamento para cada núcleo e distribui instruções independentes de cada encadeamento entre quaisquer ciclos abertos.

Portanto, se houver apenas um thread, o sistema operacional não fará nenhuma otimização. No entanto, o front-end da CPU distribuirá conjuntos de instruções independentes entre cada núcleo.

De acordo com https://stackoverflow.com/a/15936270 , uma linguagem de programação específica pode criar mais ou menos threads, mas é irrelevante ao determinar o que fazer com esses threads. O sistema operacional e a CPU lidam com isso, então isso acontece independentemente da linguagem de programação usada.

insira a descrição da imagem aqui

Apenas para esclarecer, estou perguntando sobre um único thread executado em vários núcleos, não sobre a execução de vários threads em um único núcleo.

O que há de errado com o meu resumo? Onde e como as instruções de um thread são divididas em vários núcleos? A linguagem de programação é importante? Eu sei que este é um assunto amplo; Espero uma compreensão de alto nível disso.

Evorlor
fonte
6
Um conjunto de instruções para um único thread de software pode ser executado em vários núcleos, mas não de uma vez.
Kroltan
11
Você está misturando threads de software (que envolvem o agendador do SO) e threads de hardware ou HyperThreading (um recurso de CPU que faz um núcleo se comportar como dois).
Ugoren
2
Eu tenho 20 motoristas e 4 caminhões. Como é possível que um motorista entregue pacotes com dois caminhões? Como é possível que um caminhão possa ter vários motoristas? A resposta para ambas as perguntas é a mesma. Faz voltas.
Eric Lippert

Respostas:

84

O sistema operacional oferece intervalos de tempo de CPU para threads elegíveis para execução.

Se houver apenas um núcleo, o sistema operacional agendará o encadeamento mais qualificado para execução nesse núcleo por um intervalo de tempo. Após a conclusão de um intervalo de tempo, ou quando o encadeamento em execução é bloqueado no IO ou quando o processador é interrompido por eventos externos, o sistema operacional reavalia o encadeamento a ser executado a seguir (e pode escolher o mesmo encadeamento novamente ou outro diferente).

A elegibilidade para executar consiste em variações de justiça, prioridade e prontidão, e por esse método vários threads obtêm intervalos de tempo, alguns mais que outros.

Se houver vários núcleos, N, o sistema operacional agendará os N threads mais elegíveis para execução nos núcleos.

A afinidade do processador é uma consideração de eficiência. Cada vez que uma CPU executa um encadeamento diferente do que antes, tende a desacelerar um pouco porque seu cache é quente para o encadeamento anterior, mas frio para o novo. Portanto, executar o mesmo encadeamento no mesmo processador em vários intervalos de tempo é uma vantagem de eficiência.

No entanto, o sistema operacional é livre para oferecer intervalos de tempo de um encadeamento em diferentes CPUs e pode alternar entre todos os CPUs em diferentes intervalos de tempo. No entanto, como @ gnasher729 diz , não pode executar um thread em várias CPUs simultaneamente.

O Hyperthreading é um método no hardware pelo qual um único núcleo aprimorado da CPU pode suportar a execução de dois ou mais threads diferentes simultaneamente. (Essa CPU pode oferecer threads adicionais a um custo mais baixo no setor imobiliário do silício do que núcleos completos adicionais.) Esse núcleo aprimorado da CPU precisa oferecer suporte a estados adicionais para outros threads, como valores de registro da CPU, e também possui estado e comportamento de coordenação que permite o compartilhamento de unidades funcionais nessa CPU sem confundir os threads.

O Hyperthreading, embora tecnicamente desafiador do ponto de vista do hardware, do ponto de vista do programador, o modelo de execução é apenas o de núcleos adicionais da CPU, em vez de algo mais complexo. Portanto, o sistema operacional vê núcleos adicionais da CPU, embora haja alguns novos problemas de afinidade do processador, pois vários encadeamentos com hyperthread estão compartilhando a arquitetura de cache de um núcleo da CPU.


Podemos pensar ingenuamente que dois encadeamentos executados em um núcleo hiperencadeado rodam metade da velocidade que cada um com seu próprio núcleo completo. Mas esse não é necessariamente o caso, uma vez que a execução de um único encadeamento está cheia de ciclos frouxos, e alguns deles podem ser usados ​​pelo outro encadeamento com hiperencadeamento. Além disso, mesmo durante ciclos sem folga, um encadeamento pode estar usando unidades funcionais diferentes das outras, para que a execução simultânea possa ocorrer. A CPU aprimorada para hyperthreading pode ter um pouco mais de algumas unidades funcionais muito usadas especialmente para dar suporte a isso.

Erik Eidt
fonte
3
"Portanto, executar o mesmo encadeamento no mesmo processador em vários intervalos de tempo é uma vantagem de eficiência." Não precisaria ser fatias de tempo contíguas ? Caso contrário, os caches seriam limpos por outros threads, não? +1 para uma boa explicação.
precisa saber é o seguinte
2
@Luaan: HT geralmente é bom, mas a situação não é tão simples quanto você descreve. A largura de banda do problema de front-end (4 uops por relógio na Intel, 6 na Ryzen) é igualmente compartilhada entre os threads (a menos que um esteja parado). Se esse for o gargalo, como eu disse, o HT não ajudará em nada. Não é incomum que a Skylake chegue perto disso em um loop bem ajustado, se houver uma mistura de cargas, ALU e lojas ... Os transistores são baratos (e nem todos podem ser alternados de uma vez ou a CPU derrete), tão modernas CPUs x86 têm mais portas de execução do que o front-end podem alimentar (com muitas unidades de execução a ser replicado ...
Peter Cordes
2
... em várias portas) ... Isso pode parecer um desperdício, mas muitas vezes um loop usa apenas um tipo de unidade de execução de ALU de uma só vez, portanto, duplicar tudo significa que, independentemente do tipo de código que está sendo executado, há vários portas para suas instruções. Portanto, o motivo que você citou para se beneficiar do HT não é tão comum, pois a maioria dos códigos tem algumas cargas e / ou lojas ocupando largura de banda do front-end, e o que resta geralmente não é suficiente para saturar as unidades de execução.
27568 Peter
2
@Luaan: Além disso, nas CPUs Intel, as unidades de execução inteira e FP / vetor compartilham as mesmas portas de execução . Por exemplo, as unidades FP FMA / mul / add estão nas portas 0/1. Mas o multiplicador inteiro também está na porta1, e operações inteiras simples podem ser executadas em qualquer uma das 4 portas de execução (diagrama na minha resposta). Um segundo encadeamento usando a largura de banda de problema diminui a velocidade, mesmo que eles não concorram por unidades de execução, mas geralmente há um ganho de taxa de transferência líquida se eles não competem muito mal pelo cache. Mesmo códigos de alto rendimento, bem ajustados, como x264 / x265 (codificadores de vídeo), beneficiam cerca de 15% no Skylake da HT.
Peter Cordes
3
@luaan Além do que Peter disse, sua afirmação de que "esse era o raciocínio original por trás do HT" está incorreta. O raciocínio original por trás HT foi que a microarquitetura NetBurst tinha alongou o gasoduto, a tal ponto extremo (para efeitos de condução até a velocidade do clock) que mispredictions filiais e outras bolhas de dutos absolutamente matou desempenho. O HT foi uma das soluções da Intel para minimizar a quantidade de tempo que as unidades de execução desse grande e caro chip ficaram inativas por causa de bolhas no pipeline: o código de outros threads poderia ser inserido e executado nesses buracos.
Cody Grey
24

Não existe um único thread em execução em vários núcleos simultaneamente.

No entanto, isso não significa que instruções de um thread não possam ser executadas em paralelo. Existem mecanismos chamados pipelining de instruções e execução fora de ordem que permitem isso. Cada núcleo possui muitos recursos redundantes que não são utilizados por instruções simples; portanto, várias dessas instruções podem ser executadas juntas (desde que a próxima não dependa do resultado anterior). No entanto, isso ainda acontece dentro de um único núcleo.

O hiperencadeamento é uma espécie de variante extrema dessa idéia, na qual um núcleo não apenas executa instruções de um encadeamento em paralelo, mas combina instruções de dois encadeamentos diferentes para otimizar ainda mais o uso de recursos.

Entradas relacionadas da Wikipedia: pipeline de instruções , execução fora de ordem .

Frax
fonte
3
Eles não podem correr simultaneamente, mas podem correr em paralelo? Não são a mesma coisa?
precisa saber é o seguinte
10
@Evorlor A principal coisa aqui é a diferença entre um núcleo e uma unidade de execução. Um único encadeamento pode ser executado apenas em um núcleo, mas um processador pode usar a análise dinâmica para determinar quais instruções sendo executadas por um núcleo não dependem umas das outras e executá-las simultaneamente em diferentes unidades de execução. Um núcleo pode ter várias unidades de execução.
precisa saber é o seguinte
3
@Evorlor: uma CPU fora de ordem pode encontrar e explorar o paralelismo no nível de instruções no fluxo de instruções de um único thread. por exemplo, frequentemente as instruções que atualizam um contador de loop são independentes de alguns dos outros trabalhos que um loop realiza. Ou, em um a[i] = b[i] + c[i]loop, cada iteração é independente; portanto, carrega, adiciona e armazena de diferentes iterações podem estar em andamento ao mesmo tempo. Ele deve preservar a ilusão de que as instruções executadas na ordem do programa, mas, por exemplo, um armazenamento que falha no cache não atrasa o encadeamento (até ficar sem espaço no buffer do armazenamento).
Peter Cordes
3
@ user1937198: A frase "análise dinâmica" seria melhor para um compilador JIT. CPUs fora de ordem realmente não analisam; é mais como um algoritmo ganancioso que executa qualquer instrução que tenha sido decodificada e emitida e tenha suas entradas prontas. (A janela de reordenação fora de ordem é limitada por alguns recursos de microarquitetura, por exemplo, a Intel Sandybridge possui um tamanho de buffer de ReOrder de 168 uops. Consulte também medindo o tamanho de ROB experimentalmente ). Tudo implementado com máquinas de estado de hardware para lidar com 4 uops por relógio.
Peter Cordes
3
@Luaan sim, foi uma ideia interessante, mas os compiladores AOT ainda não são inteligentes o suficiente para explorá-lo completamente. Além disso, Linus Torvalds (e outros) argumentaram que expor que grande parte das partes internas do oleoduto é uma grande restrição em projetos futuros. por exemplo, você não pode realmente aumentar a largura do pipeline sem alterar o ISA. Ou você constrói uma CPU que rastreia dependências da maneira usual e talvez emita dois grupos VLIW em paralelo, mas perdeu o benefício de complexidade da CPU do EPIC, mas ainda tem as desvantagens (perda de largura de banda quando o compilador não pode preencher uma palavra).
Peter Cordes
22

resumo: A localização e a exploração do paralelismo ( em nível de instrução) em um programa de thread único é feita exclusivamente em hardware, pelo núcleo da CPU em que está sendo executado. E apenas por uma janela de algumas centenas de instruções, sem reordenar em larga escala.

Os programas de thread único não se beneficiam das CPUs com vários núcleos, exceto que outras coisas podem ser executadas nos outros núcleos, em vez de perder tempo com a tarefa de thread único.


o sistema operacional organiza as instruções de todos os threads de forma que eles não estejam esperando um pelo outro.

O SO NÃO olha dentro dos fluxos de instruções dos threads. Ele agenda somente threads para núcleos.

Na verdade, cada núcleo executa a função de agendador do sistema operacional quando precisa descobrir o que fazer a seguir. O agendamento é um algoritmo distribuído. Para entender melhor as máquinas com vários núcleos, pense em cada núcleo como executando o kernel separadamente. Assim como um programa multithread, o kernel é escrito para que seu código em um núcleo possa interagir com segurança com seu código em outros núcleos para atualizar estruturas de dados compartilhadas (como a lista de threads que estão prontos para execução.

De qualquer forma, o sistema operacional está envolvido em ajudar processos multiencadeados a explorar o paralelismo no nível de encadeamento, que deve ser explicitamente exposto escrevendo manualmente um programa multiencadeado . (Ou por um compilador de paralelismo automático com o OpenMP ou algo assim).

Em seguida, o front-end da CPU organiza ainda mais essas instruções distribuindo um encadeamento para cada núcleo e distribui instruções independentes de cada encadeamento entre quaisquer ciclos abertos.

Um núcleo de CPU está executando apenas um fluxo de instruções, se não for interrompido (adormecido até a próxima interrupção, por exemplo, interrupção do timer). Freqüentemente isso é um encadeamento, mas também pode ser um manipulador de interrupção do kernel ou um código diverso do kernel se o kernel decidir fazer algo diferente de apenas retornar ao encadeamento anterior após o tratamento e a interrupção ou chamada do sistema.

Com o HyperThreading ou outros designs SMT, um núcleo físico da CPU atua como vários núcleos "lógicos". A única diferença da perspectiva do sistema operacional entre uma CPU quad-core com hyperthreading (4c8t) e uma máquina simples de 8 núcleos (8c8t) é que um sistema operacional compatível com HT tentará agendar threads para separar núcleos físicos, para que não Não competir entre si. Um sistema operacional que não sabia sobre hyperthreading veria apenas 8 núcleos (a menos que você desabilitasse o HT no BIOS, ele detectaria apenas 4).


O termo " front-end" refere-se à parte de um núcleo da CPU que busca o código da máquina, decodifica as instruções e as emite na parte com defeito do núcleo . Cada núcleo tem seu próprio front-end e faz parte do núcleo como um todo. As instruções que busca são as que a CPU está executando no momento.

Dentro da parte fora de ordem do núcleo, instruções (ou uops) são despachadas para portas de execução quando seus operandos de entrada estão prontos e há uma porta de execução livre. Isso não precisa acontecer na ordem do programa; portanto, é assim que uma CPU OOO pode explorar o paralelismo no nível de instrução em um único encadeamento .

Se você substituir "núcleo" por "unidade de execução" em sua ideia, estará quase correto. Sim, a CPU distribui instruções independentes / unidades para unidades de execução em paralelo. (Mas há uma confusão de terminologia, já que você disse "front-end" quando realmente é o agendador de instruções da CPU, também conhecido como Estação de Reserva, que escolhe as instruções prontas para execução).

A execução fora de ordem só pode encontrar o ILP em um nível muito local, apenas algumas centenas de instruções, não entre dois loops independentes (a menos que sejam curtos).


Por exemplo, o equivalente asm deste

int i=0,j=0;
do {
    i++;
    j++;
} while(42);

funcionará tão rápido quanto o mesmo loop, incrementando apenas um contador no Intel Haswell. i++depende apenas do valor anterior de i, enquanto j++depende apenas do valor anterior de j, portanto, as duas cadeias de dependência podem ser executadas em paralelo sem interromper a ilusão de que tudo está sendo executado na ordem do programa.

No x86, o loop seria algo como isto:

top_of_loop:
    inc eax
    inc edx
    jmp .loop

O Haswell possui 4 portas de execução inteira e todas elas possuem unidades somadoras, portanto, ele pode sustentar uma taxa de transferência de até 4 incinstruções por relógio, se todas forem independentes. (Com latência = 1, você precisa apenas de 4 registros para maximizar a taxa de transferência mantendo 4 incinstruções em voo. Compare isso com vetor FP-MUL ou FMA: latência = taxa de transferência 5 = 0,5 precisa de 10 acumuladores de vetor para manter 10 FMAs em vôo para maximizar a taxa de transferência e cada vetor pode ter 256b, mantendo 8 flutuadores de precisão única).

A ramificação obtida também é um gargalo: um loop sempre leva pelo menos um relógio inteiro por iteração, porque o rendimento da ramificação obtida é limitado a 1 por relógio. Eu poderia colocar mais uma instrução dentro do loop sem reduzir o desempenho, a menos que ele também lesse / gravasse eaxou edx, nesse caso, aumentaria a cadeia de dependência. Colocar mais duas instruções no loop (ou uma instrução multi-uop complexa) criaria um gargalo no front-end, pois ele só pode emitir 4 uops por relógio no núcleo fora de ordem. (Veja estas perguntas e respostas para obter mais detalhes sobre o que acontece com loops que não são múltiplos de 4 uops: o buffer de loop e o cache uop tornam as coisas interessantes.)


Em casos mais complexos, encontrar o paralelismo requer uma janela maior de instruções . (por exemplo, talvez exista uma sequência de 10 instruções que dependam uma da outra, depois algumas independentes).

A capacidade do buffer de reordenação é um dos fatores que limita o tamanho da janela fora de ordem. No Intel Haswell, são 192 uops. (E você pode até medi-lo experimentalmente , juntamente com a capacidade de renomeação de registros (tamanho do arquivo de registro).) Os núcleos de CPU de baixa potência, como o ARM, têm tamanhos de ROB muito menores, se executar de maneira fora de ordem.

Observe também que as CPUs precisam ser canalizadas e estar fora de ordem. Portanto, ele precisa buscar e decodificar instruções bem antes das que estão sendo executadas, de preferência com taxa de transferência suficiente para reabastecer os buffers depois de perder qualquer ciclo de busca. Os ramos são complicados, porque não sabemos de onde buscar, se não sabemos para que lado um ramo foi. É por isso que a previsão de ramificação é tão importante. (E por que as CPUs modernas usam a execução especulativa: elas adivinham o caminho que uma ramificação seguirá e começarão a buscar / decodificar / executar esse fluxo de instruções. Quando uma imprevisão é detectada, elas retornam ao último estado de bom estado e são executadas a partir daí.)

Se você quiser ler mais sobre os internos da CPU, existem alguns links no wiki da tag Stackoverflow x86 , incluindo o guia de microarquitetura de Agner Fog e os escritos detalhados de David Kanter com diagramas de CPUs Intel e AMD. A partir de sua descrição da microarquitetura Intel Haswell , este é o diagrama final de todo o pipeline de um núcleo Haswell (não o chip inteiro).

Este é um diagrama de blocos de um único núcleo de CPU . Uma CPU quad-core possui 4 delas em um chip, cada uma com seus próprios caches L1 / L2 (compartilhando um cache L3, controladores de memória e conexões PCIe aos dispositivos do sistema).

Haswell pipeline completo

Eu sei que isso é esmagadoramente complicado. O artigo de Kanter também mostra partes disso para falar sobre o frontend separadamente das unidades de execução ou dos caches, por exemplo.

Peter Cordes
fonte
2
"Encontrar e explorar o paralelismo (em nível de instrução) em um programa de thread único é feito exclusivamente em hardware" Observe que isso se aplica apenas aos ISAs convencionais, não aos VLIWs nos quais o ILP é determinado completamente pelo compilador ou programador ou em cooperação entre o hardware e software.
Hadi Brais
11
@ user7813604: sim. O Hyperthreading não pode paralelizar um único thread. Ele faz o inverso: executa vários threads em um núcleo, reduzindo o desempenho por thread, mas aumentando a taxa de transferência geral.
Peter Cordes
11
@ user7813604: O objetivo principal do ILP é descobrir quais instruções podem ser executadas em paralelo, mantendo a ilusão de que cada instrução foi executada em ordem, cada uma terminando antes do início da próxima. Às vezes, uma CPU escalonada em pipeline pode precisar paralisar as dependências se a latência for maior que 1. Mas é um negócio ainda maior para CPUs superescalares.
Peter Cordes
11
@ user7813604: sim, minha resposta literalmente usa isso como exemplo. Haswell, por exemplo, pode executar até 4 incinstruções no mesmo ciclo de clock, em suas 4 unidades de execução de ALU inteiras.
Peter Cordes
11
@ user7813604: Sim, ILP é o quanto pode ser executado em paralelo. Uma CPU real terá uma capacidade limitada de encontrar e explorar o ILP, executando-o em paralelo em um único núcleo, por exemplo, até quatro superescalares na Intel. Esta resposta tenta explicar isso com exemplos.
Peter Cordes