3 pontos por GN⁺ 2024-07-29 | 1 comentários | Compartilhar no WhatsApp
  • SMT é uma técnica em que um único núcleo de CPU emite instruções de várias threads no mesmo ciclo para preencher melhor os recursos de execução que ficariam ociosos apenas com paralelismo no nível de instruções
  • O Hyper-Threading da Intel implementa duas threads por núcleo e replica o estado arquitetural, fazendo com que para o sistema operacional um núcleo físico apareça como dois processadores lógicos
  • Na implementação real, nem todos os recursos são simplesmente duplicados: ponteiro de instrução, ITLB e RAT são replicados; trace cache e TLB são compartilhados; fila de uop, registradores físicos e reorder buffer são divididos igualmente
  • O ganho de desempenho varia conforme a carga de trabalho: se duas threads competirem pelo cache, o desempenho pode piorar; já threads cooperativas que trocam dados podem ficar mais rápidas graças ao cache compartilhado
  • Recursos compartilhados e execução especulativa podem levar a vulnerabilidades de segurança, então em ambientes sensíveis à segurança ou em cargas que exigem máximo desempenho e mínima latência, desativar o SMT pode ser uma escolha prática

Por que o SMT é necessário

  • Processadores modernos têm centenas de registradores, várias unidades de load/store e unidades aritméticas, e usam técnicas de paralelismo no nível de instruções como pipeline, arquitetura superscalar e execução fora de ordem para aproveitar esses recursos
  • O pipeline divide a execução de instruções em vários estágios e, a cada ciclo, passa a instrução ao estágio seguinte; em um pipeline de profundidade 5, depois do quinto ciclo até 5 instruções podem estar em andamento ao mesmo tempo
  • Processadores superscalar podem emitir várias instruções por ciclo, e processadores Intel Core i7 recentes podem emitir 4 instruções em um único ciclo
  • Em programas reais, é difícil encontrar instruções suficientemente independentes, então surgem períodos em que os recursos de execução ficam ociosos
    • Desperdício horizontal acontece quando não há instruções independentes suficientes dentro de uma única thread para preencher a largura de emissão
    • Desperdício vertical acontece quando as próximas instruções dependem das que já estão em execução e, por isso, nenhuma instrução pode ser emitida em um ciclo
  • O multithreading tradicional emite instruções de apenas uma thread por ciclo e troca para outra thread no ciclo seguinte, o que pode reduzir o desperdício vertical, mas o desperdício horizontal e o overhead de troca de contexto continuam
  • O SMT emite instruções de várias threads no mesmo ciclo, sem troca de contexto, preenchendo os recursos de execução em uma proporção maior
  • A implementação de SMT da Intel, o Hyper-Threading, é limitada a duas threads por núcleo

Estrutura básica do SMT ao estilo Intel

  • Um processador comum sem SMT só consegue executar instruções de uma thread por vez
  • Cada thread possui estado arquitetural, incluindo valores de registradores, contador de programa e registradores de controle
  • Para executar simultaneamente instruções de duas threads, é preciso representar ao mesmo tempo o estado das duas threads; por isso, implementações de SMT replicam o estado arquitetural do processador
  • Por causa dessa replicação, um único processador físico aparece para o sistema operacional como dois processadores lógicos, e o sistema pode agendar threads em cada um deles
  • Buffers e recursos de execução no nível de microarquitetura são replicados, compartilhados ou particionados conforme fatores como custo, consumo de energia e área de chip
  • O foco da discussão é principalmente a implementação de SMT da Intel, com base no whitepaper da Intel de 2002

As três partes da microarquitetura de CPU

  • O processador expõe ao programador a ISA como interface pública, e a ISA inclui o conjunto de instruções e os registradores que as instruções podem usar
  • A microarquitetura é a implementação interna, que pode variar mesmo entre modelos de processador que suportam a mesma ISA
  • A microarquitetura de processadores modernos se divide, em linhas gerais, em três partes
    • Frontend: inclui a unidade de controle de instruções que busca e decodifica as próximas instruções do programa
    • Backend: inclui recursos de execução como registradores físicos, unidades aritméticas e unidades de load/store, e aloca recursos às instruções decodificadas para agendar sua execução
    • retirement unit: aplica de forma definitiva os resultados das instruções executadas ao estado arquitetural do processador

SMT no frontend

  • O ponteiro de instrução rastreia o endereço da próxima instrução a ser buscada
    • Um processador com SMT tem dois conjuntos de ponteiros de instrução para rastrear de forma independente as próximas instruções de dois programas
  • O trace cache armazena traces de instruções decodificadas recentemente para reduzir o custo de decodificação e a latência de instruções executadas repetidamente
    • Ele é compartilhado dinamicamente pelos dois processadores lógicos conforme a necessidade
    • Se uma thread executar mais instruções, pode ocupar mais entradas do trace cache
    • Cada entrada recebe uma tag com informações da thread para distinguir instruções das duas threads
    • O acesso ao trace cache é arbitrado a cada ciclo entre os dois processadores lógicos
  • Quando ocorre um miss no trace cache, o frontend procura as instruções no cache de instruções L1 daquele endereço e, se houver miss também no cache L1, precisa buscá-las no próximo nível de cache ou na memória principal
  • O cache de instruções L1 armazena dados em cache com endereços virtuais, mas o acesso à memória principal exige endereços físicos
  • O ITLB guarda traduções recentes e converte endereços virtuais em físicos
    • Em processadores com SMT, cada processador lógico tem seu próprio cache de ITLB
    • A lógica de busca de instruções na memória principal funciona em esquema first come first served, mas reserva pelo menos um slot de requisição para cada processador lógico, para que ambos possam avançar
    • Instruções vindas da memória principal são armazenadas antes da decodificação em um pequeno streaming buffer, que em processadores com SMT também é replicado por processador lógico
  • Depois de buscadas, as instruções são decodificadas em uops menores e mais simples
    • As uops entram em uma fila de uops, que serve como fronteira entre o frontend e o backend da CPU
    • A fila de uops é compartilhada de forma igual entre os dois processadores lógicos, e essa partição estática permite que ambos avancem de forma independente

SMT no backend

  • O backend busca microinstruções na fila de uops e as executa fazendo execução fora de ordem, sem ficar preso apenas à ordem original do programa
  • Instruções próximas no programa costumam depender umas das outras e, se houver uma operação de alta latência como uma leitura da memória principal, até as instruções dependentes precisam esperar
  • O mecanismo de execução fora de ordem executa instruções posteriores antes da ordem original para reduzir desperdício de recursos
  • O allocator identifica os recursos necessários para cada microinstrução e os aloca conforme a disponibilidade
    • Em um ciclo, ele aloca recursos para microinstruções de um processador lógico; no ciclo seguinte, alterna para o outro
    • Se a fila de uops tiver microinstruções de apenas um processador lógico, ou se um deles esgotar toda a sua cota de recursos, o allocator usa todos os ciclos para o outro processador lógico
  • Os principais recursos do backend combinam replicação, compartilhamento e particionamento
    • No nível da ISA, o X86-64 tem apenas 16 registradores inteiros de uso geral, mas no nível microarquitetural há centenas de registradores físicos inteiros e um número semelhante de registradores de ponto flutuante
    • Em processadores com SMT, os registradores físicos são divididos igualmente entre os dois processadores lógicos
    • Os load buffers e store buffers usados em operações de leitura e escrita de memória também são divididos igualmente entre os dois processadores lógicos

Renomeação de registradores, agendamento e commit

  • Para permitir execução fora de ordem, o backend faz renomeação de registradores
    • Como no nível da ISA há poucos registradores arquiteturais, instruções do programa reutilizam o mesmo registrador em várias instruções independentes
    • O mecanismo de execução fora de ordem troca cada registrador lógico original por um dos registradores físicos, permitindo execução mais adiantada e paralela
    • Esse mapeamento é mantido na register alias table, ou RAT
    • Como os dois processadores lógicos têm seus próprios conjuntos de registradores arquiteturais, cada um também tem sua própria cópia da RAT
  • As instruções que passam pelas etapas de register renaming e allocator entram em filas de pronto
    • Uma fila é para instruções de leitura/escrita de memória e a outra é para instruções gerais
    • Em um núcleo com SMT, essas filas são divididas igualmente entre os dois processadores lógicos
  • O processador possui vários instruction schedulers em paralelo
    • A cada ciclo de CPU, parte das instruções das filas de pronto é enviada aos schedulers
    • Em um ciclo, as filas enviam instruções de um processador lógico; no seguinte, alternam para o outro
    • O scheduler não se preocupa com o processador lógico e envia imediatamente para execução as microinstruções cujos operandos e unidades de execução necessárias estejam prontos
    • Para garantir justiça, há um limite para o número de entradas ativas que um processador lógico pode manter dentro da fila do scheduler
  • Os resultados das instruções executadas entram no reorder buffer
    • Mesmo que instruções sejam executadas fora de ordem, elas precisam ser committed ao estado arquitetural do processador na ordem original do programa
    • Em núcleos com SMT, o reorder buffer é dividido igualmente entre os dois processadores lógicos
  • A retirement unit acompanha quando as instruções estão prontas para serem committed ao estado arquitetural e faz o retire na ordem correta do programa
    • Em núcleos com SMT, ela alterna o processamento entre as microinstruções de cada processador lógico
    • Se um processador lógico não tiver microinstruções para retirar, toda a largura de banda é usada pelo outro
    • Depois do retire, pode ser necessário escrever no cache L1, e essa lógica de seleção de escrita também alterna entre os dois processadores lógicos a cada ciclo

Subsistema de memória e efeito do cache

  • O TLB que converte endereços virtuais de requisições de dados em endereços físicos é compartilhado dinamicamente entre os dois processadores lógicos conforme a necessidade
  • Entradas do TLB recebem tag com o id do processador lógico para distinguir os itens dos dois processadores lógicos
  • Cada núcleo de CPU tem seu próprio cache L1 privado
  • O cache L2 pode ser privado ou compartilhado entre núcleos, dependendo da microarquitetura
  • Se houver cache L3, ele é compartilhado entre os núcleos
  • O cache não tem consciência da existência de processadores lógicos
  • Como o cache L1 e, em alguns casos, o cache L2 são privados do núcleo, eles passam a conter juntos os dados dos dois processadores lógicos conforme a necessidade
    • Se duas threads usarem o cache de forma agressiva, podem ocorrer conflitos de dados e eviction, reduzindo o desempenho
    • Se duas threads trabalharem sobre o mesmo conjunto de dados, o cache compartilhado pode melhorar o desempenho

Critérios de escolha entre desempenho e segurança

  • Mesmo com apenas uma thread rodando em um núcleo com SMT, muitos buffers e recursos de execução continuam compartilhados ou particionados entre os dois processadores lógicos, o que pode reduzir o desempenho potencial de thread única
  • Em um processador lógico não utilizado, o sistema operacional executa um idle loop, e esse loop também pode consumir recursos que poderiam ser usados pelo outro processador lógico para obter desempenho máximo
  • Em processadores Intel Core, parece que quando apenas uma thread roda no núcleo não há compartilhamento nem particionamento de recursos, e a Intel trata isso como uma melhoria introduzida naquela geração
  • Quando duas threads executam nos dois processadores lógicos de um núcleo com SMT, o padrão de acesso ao cache passa a determinar o desempenho
    • Se duas threads competirem pelo cache, uma pode provocar eviction dos dados da outra e o desempenho cai
    • Se forem cooperativas, como quando uma thread consome dados produzidos pela outra, o compartilhamento de dados em cache pode melhorar o desempenho
    • Se as duas threads não competirem agressivamente pelo cache, é possível aumentar a utilização dos recursos do núcleo sem prejudicar o desempenho uma da outra
  • Muitos especialistas consideram melhor desligar o SMT em programas que exigem desempenho absoluto máximo, para que uma única thread possa usar todos os recursos
  • O SMT também traz questões de segurança
    • Recursos compartilhados e execução especulativa podem abrir caminho para vazamento de dados sensíveis a um atacante
    • A documentação do Oracle Linux e da Red Hat traz exemplos de problemas de segurança relacionados ao SMT
    • A recomendação geral é caminhar na direção de desativar o SMT no sistema
    • Também há rumores de que a Intel pode remover o Hyper-Threading no Arrow Lake, sua próxima geração de processadores

Referências

1 comentários

 
GN⁺ 2024-07-29
Opiniões do Hacker News
  • Simplificando bastante o entendimento de SMT, o que fez sentido para mim foi que, enquanto uma thread fica parada por causa de um cache miss, a ALU preciosa pode continuar ocupada
    O LPDDR de notebooks antigos era mais lento e havia menos núcleos, então isso provavelmente tinha mais valor antes, mas hoje em dia muitas vezes há mais núcleos do que trabalho escalável, então o valor parece menor
    Também se evita contenção de cache ao não colocar trabalho no mesmo núcleo que uma thread importante, porque se sabe que o gargalo é o desempenho de thread única
    Testei núcleos Efficient/Performance e núcleos com SMT em renderização multithread no DirectX 12, e no i7-12700K o tempo para renderizar cenas complexas foi quase o mesmo usando só P-cores, P+SMT e P+E+SMT. Já no Xbox Series X, o mesmo teste ficou um pouco mais rápido quando também se colocava trabalho no SMT

    • Renderização sempre foi um dos cenários em que SMT dava igual ou até era mais lento. Já há muita matemática envolvida, então a FPU fica sempre ocupada, e especialmente porque a unidade de divisão é a operação mais cara do processador
      SMT brilha quando se está esperando I/O ou fazendo trabalho simples com inteiros. Se as duas threads conseguem saturar a FPU, SMT geralmente fica mais lento por causa da marcação extra necessária para indicar pertencimento nos dados internos da CPU
    • O Hyper-Threading da Intel é, na prática, quase um hack no pipeline de escrita
      O ponto principal não é tanto o cache miss, mas permitir que o núcleo execute outra coisa enquanto uma escrita é concluída
      Por isso alguns códigos não escalam bem, enquanto outros obtêm ganhos de velocidade quase lineares
    • Hoje em dia, especialmente considerando até alimentação traseira de energia, fico curioso sobre o quanto uma parada de cache em um processador reduz o thermal throttling desse processador e dos processadores vizinhos
      Talvez seja até melhor simplesmente deixar esses processadores cochilarem por instantes
    • Sobre a parte de que LPDDR era mais lento antigamente: curiosamente, a latência não melhorou tanto assim. A latência CAS em DDR2/3/4/5 costuma ficar na faixa de 5 a 10 ns
      Melhoraram largura do barramento, transferências por segundo, enfileiramento e energia por bit transferido/armazenado, mas se o programa precisa de dados fora do cache e a predição também falhou, no fim a latência da RAM continua sendo o problema
    • Em vez de SMT, fico me perguntando se não daria para desligar temporariamente ALU/FPU que não estiverem sendo usadas enquanto se espera por algo na parte da frente do pipeline, indo mais na direção de reduzir calor e consumo de energia do que maximizar utilização
  • Dizem que a próxima geração de CPUs Arrow Lake da Intel vai remover completamente o Hyper-Threading, ou seja, SMT
    Como o ganho de desempenho sempre dependeu muito da aplicação, talvez simplificar seja melhor
    Há uma discussão recente sobre quando e onde isso faz sentido aqui: https://news.ycombinator.com/item?id=39097124

    • A maioria dos programas acaba tendo um limite de quantas threads consegue usar de forma razoável. Quando há muito menos núcleos do que isso, SMT faz sentido para aproveitar melhor os recursos da CPU, mas a partir do ponto em que há núcleos suficientes, talvez SMT deixe de fazer sentido
      Ainda não tenho certeza de que já chegamos necessariamente a esse ponto, mas os P/E-cores da Intel são uma alternativa com objetivo parecido e parecem bem razoáveis em desktops com muito trabalho single-thread ou com poucas threads. Também parece haver valor no fato de não precisar lidar, na otimização de aplicações, com a distinção entre SMT e E-core
      Já a AMD pretende, por enquanto, manter núcleos em geral homogêneos e continuar usando SMT. Qual estratégia é melhor na prática provavelmente varia muito de aplicação para aplicação, então não parece algo simples de julgar
    • No meu caso de uso pessoal, desenvolvendo jogos e engines, foi mais rápido rezar para o deus do agendamento de threads da CPU colocar cada thread no seu próprio núcleo do que usar Hyper-Threading
      Por isso decidi limitar a quantidade de threads a std::thread::hardware_concurrency() / 2 - 1, ou seja, número de núcleos - 1. Estou lidando com std::vector
    • Pelos benchmarks comuns do setor, o Hyper-Threading da Intel foi mais lento do que deixá-lo desativado em pelo menos uma geração sim, outra não
      Mesmo quando funcionava bem, o ganho mal chegava a percentuais de dois dígitos, e houve períodos em gerações seguidas em que ficou pior; não sei por que continuam insistindo nisso
    • Isso também vale para peças de servidor?
  • Sempre me surpreendo ao ler como essas funções de CPU de baixo nível funcionam
    Na faculdade fiz uma disciplina que seria algo como “Introdução ao hardware de computadores”, mas na prática deveria se chamar “Introdução ao projeto de CPU”. Construíamos somadores, latches, flip-flops etc. com portas lógicas e, no fim do semestre, já dava para projetar um processador bem básico no nível de portas
    Mas imaginar coisas como renomeação de registradores ou execução fora de ordem e de fato inventá-las está além da minha imaginação. Será que isso também é projetado no nível de portas? Ou existe um “compilador” que, a partir da linguagem usada, organiza portas e transistores?

    • Fiz a disciplina seguinte e aprendi SMT e mais algumas coisas
      Todos os trabalhos eram em Verilog, uma linguagem de descrição de hardware, o que permitia escrever abstraindo vários elementos
  • Um dos grandes equívocos que usuários costumam ter sobre SMT é o modelo mental de imaginar um “núcleo de verdade” e um outro núcleo inferior
    Em todos os aspectos observáveis, as duas threads são equivalentes

    • Acho que essa percepção vem do desempenho. As duas threads podem executar o mesmo trabalho, mas não se obtém 2x de desempenho como aconteceria com uma “segunda thread de verdade”, isto é, um segundo núcleo
      No fim, olhando só para desempenho, conceitualmente isso fica mais perto de algo como 1,25 núcleo single-thread, ou dessa proporção dependendo da aplicação
    • Ao rodar tarefas altamente otimizadas e muito intensivas em computação, como compressão de vídeo, os coolers do computador soam como turbinas de avião, mas o gerenciador de tarefas mostra 50% de uso de CPU, então é compreensível que essa percepção surja
    • As CPUs novas da Intel realmente têm tanto um “P-core”, que é um núcleo de verdade, quanto um “E-core”, que é um núcleo inferior
      Acho que a principal razão para introduzir E-cores não foi consumo de energia nem desempenho, mas sim calor e área de die. Por isso eu sempre compro chips sem E-core; acho que eles são melhores
  • Fico curioso sobre como buscar esse tipo de texto técnico detalhado
    Procurei por esse tópico exato, mas, como esperado, por ser uma tecnologia voltada ao usuário final, os resultados de busca eram só textos para usuários que não explicavam nada direito

    • Dá para usar https://hn.algolia.com. Partindo da premissa de que a maioria dos textos desse tipo aparece ou é mencionada no HN
    • Acho que LLMs com acesso à web combinam bem com esse tipo de busca. Pelo menos ajudam a apontar uma direção
      Só que as URLs que fornecem na maioria das vezes são alucinações
    • Não sei se o Google está rastreando o aumento repentino de interesse neste artigo por causa deste post no HN, mas, ao buscar por “how does simultaneous multi threading work”, este post de blog aparece para mim mais ou menos como o 5º resultado
      Verifiquei em uma nova aba privativa do Firefox em outro dispositivo; isso não impede totalmente rastreamento ou cache, mas me parece uma aproximação bem razoável
  • A explicação de que “em um núcleo de CPU com SMT ativado, muitos buffers e recursos de execução precisam ser compartilhados entre dois processadores lógicos, então, mesmo que só uma thread esteja em execução no núcleo SMT, esses recursos não podem ser usados por essa thread e o desempenho potencial diminui” hoje já não é verdade
    No modo SMT, divide-se ROB, largura de banda de fetch/decode etc., mas já vi vários núcleos SMT que permitem usar tudo isso quando não estão em SMT

    • Os processadores Phi da série x200 funcionam exatamente assim. No modo sem SMT, obtêm muito mais recursos por thread do que no modo SMT de 4 vias
  • O principal objetivo do SMT é maximizar a utilização do motor de execução superscalar
    Isso me faz pensar se esse tipo de discussão significa que as pessoas acham que superscalar já não é tão importante quanto antes

  • No geral, é um bom resumo, mas em alguns pontos pareceu um pouco misturado
    Gostaria de saber mais dos macetes práticos que o pessoal de dentro usa, mesmo que seja fora do escopo relacionado à segurança

  • A pobre arquitetura Bulldozer da AMD foi tão criticada por não ter SMT, e agora todo mundo está se afastando do SMT
    Claro, eu sei que o Bulldozer tinha problemas muito maiores além da ausência de SMT. Na prática, era quase a estrutura oposta, em que vários núcleos compartilhavam coisas como a mesma ALU. Ainda assim, se tivessem conseguido extrair só um pouco mais de desempenho, talvez desse para dizer que viram algo antes dos outros

    • A arquitetura PowerXX não está se afastando do SMT
      O Power10 atualmente dá suporte efetivo a SMT8, com 8 threads por núcleo, e considerando o esforço de anos desenvolvendo projetos centrados em SMT, não parece que eles vão abandonar isso
  • Vale notar que as unidades de computação de GPU também costumam usar um nível de SMT na faixa de 7 a 10 threads por CU
    Isso ajuda a esconder a latência

    • A maioria das GPUs usa não SMT, mas seu predecessor, o multithreading de granularidade fina
      A cada ciclo de clock, escolhe-se entre várias threads disponíveis a instrução de uma thread que precise de recursos que não estejam ocupados, e ela é iniciada. A maioria das GPUs não inicia várias instruções por clock, embora várias instruções possam avançar simultaneamente depois de iniciadas. Mesmo quando iniciam várias instruções por clock, elas podem ter de pertencer a classes separadas de instruções que usam recursos de execução diferentes, como instruções escalares e vetoriais
      SMT, isto é, multithreading simultâneo, é quando, a cada ciclo de clock, muitas instruções de todas as threads são iniciadas ao mesmo tempo, e essas instruções competem pelas várias unidades de execução de uma CPU superscalar para manter o maior número possível delas ocupado. Para cada unidade de execução paralela — como cada um dos 6 somadores inteiros de uma CPU moderna — decide-se separadamente qual instrução executar a partir de uma fila que contém instruções de todas as threads simultâneas