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 inc
instruçõ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 inc
instruçõ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 eax
ou 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).
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.