Low-power 3/3: microarquitetura e RTL

6 Abordagens para redução de consumo em RTL

As ferramentas de síntese e back-end detectam oportunidades para salvar energia diretamente do RTL, portanto a microarquitetura e, consequentemente o estilo de codificação tem um grande impacto no consumo. A medida que o fluxo avança, menores são as intervenções que podem ser feitas para reduzir a energia dinâmica e estática. Um bom design low-power começa nos níveis arquiteturais como já foi discutido e, ainda antes da síntese, durante a escrita do código em linguagem de descrição de hardware. É dito que 80% do consumo em um ASIC estará definido no momento em que o RTL é finalizado. Somente os outros 20% dependerão do back-end. [1]

6.1 Máquinas de estado – codificação e decomposição

Minimizar a inversão de bits nas transições entre um estado e outro, naturalmente minimiza o consumo de uma máquina de estados. Codificação Grey, onde somente um bit inverte durante uma transição entre estados, é um cliché para a redução de consumo.

Figura 6.1 Codificação Binária versus Grey em uma FSM de 5 estados (Figura retirada de [1])

Ainda, pode-se considerar uma abordagem mais ad hoc, ao identificar os caminhos mais frequentes exercitados pela máquina, e codificá-los de forma que menos bits transicionem entre um estado e outro.

Na Figura 6.1, numa máquina de 5 estados, há 32 transições possíveis. Neste caso, somente 6 podem acontecer de fato. Na codificação binária, 2 bits invertem nas transições B->C e E->B. Se 30% das transições na operação normal forem E->B (ou B->C), a economia de energia com o Grey code fica em 15% somente na inversão dos bits dos registradores, e ainda haverá redução de consumo na lógica combinatória, na mesma proporção ou ainda mais [1].

Uma outra forma de economizar energia é decompor uma FSM em diferentes sub-máquinas, garantindo que as submáquinas em conjunto tenham o mesmo STG (grafo de transição de estados) da máquina original. Durante a operação, exceto se houver transições entre uma submáquina e outra, somente uma precisa estar ativa. Um problema desta abordagem é que os STGs crescem exponencialmente com o número de registros e as técnicas para decompor a máquina também tornam-se mais complicadas [3].

6.2 Representação binária

Já foi discutido neste site a representação e operação de números negativos em circuitos digitais. Para a maioria das aplicações, complemento de 2 é mais adequada que sua representação por sinal-magnitude [1]. Entretanto, para algumas aplicações bastante especificas, pode ser vantajoso o uso de sinal-magnitude. Numa transição de “0” para “-1”, a representação em complemento de 2 inverte todos os bits, ao contrário da lógica sinal-magnitude:

// complemento de 2:
"0" : 00000000
"-1": 11111111

// sinal-magnitude
"0" : 00000000
"-1": 10000001

6.3 Inferência para clock-gating

Abaixo duas possíveis descrições em Verilog para um banco de flip-flops (reg_bank_ff) registrar um novo dado somente (reg_bank_nxt) quando o sinal de seleção load_sel estiver alto. O próximo valor ainda depende de um sinal de seleção deadbeef_sel:

Código 1. Inferência de clock-gating

No “mau exemplo” o sinal i_load_sel_w não foi colocado diretamente sob o domínio do clock, e sim em um bloco de lógica puramente combinacional. É ainda possível que a ferramenta infira o clock-gating após o flattening, mas não é boa prática contar com isso. No “bom exemplo” a ferramenta de síntese prontamente infere que i_load_sel_w é o controle da célula de clock-gating que será integrada (linha 47), economizando 32 multiplexadores que seriam controlados por i_load_sel_w, como descrito no código de “mau exemplo”.

6.4 Multiplexadores One-Hot

Multiplexadores são descritos/inferidos normalmente a partir de cases e ifs [1]:

// Mux 4:1 (linha de seleção em código binário)  
case (SEL)     
2'b00: out = a;    
2'b01: out = b;    
2'b10: out = c;    
2'b11: out = d; 
endcase  
//Mux 4:1  (linha de seleção em código one-hot)  
case (SEL)    
4'b0001: out = a;    
4'b0010: out = b;    
4'b0100: out = c;    
4'b1000: out = d;    
default: out = out;  
endcase

Se as linhas de seleção de um multiplexador são barramentos de vários bits, o chaveamento pode ser significante.

Na Figura 6.3, à direita, os barramentos não selecionados são prontamente mascarados pela lógica AND na entrada do gate OR, diminuindo ainda o atraso da entrada selecionada para a saída.

Figura 6.3 – Muxes N:1 – Binário x One Hot [1]

A maior parte da lógica em um design pode consistir em multiplexadores, portanto, evitar ou mascarar falsas transições deve diminuir o consumo significativamente [1, 2]. Note que a codificação one-hot também é um bom padrão de escolha para máquinas de estado.

6.5 Evitar transições redundantes

Analise a microarquitetura da Figura 6.4. O sinal load_op ativa os 4 operandos a_in, b_in, c_in e d_in. O sinal sel_in seleciona qual operação terá o resultado carregado na saída. Perceba que se o sinal load_op não precisa – ou não deveria – estar ativo se o sinal load_out não for ativado também, visto que as operações nas nuvens lógicas não terão efeito na saída.

Figura 6.4 – Microarquitetura que permite transições redundantes (inúteis) [1]

Na figura abaixo, a microarquitetura é modificada para evitar transições inúteis. A adição de portas AND na entrada dos operandos a_in e b_in faz com que estes sejam habilitados somente quando sel_in é ‘0’; c_in e d_in por sua vez, somente quando sel_in é ‘1’.

Figura 6.5 – Supressão das transições redudantes [1]

6.6 Compartilhamento de recursos

Observe as seguintes lógicas combinatórias descrita em Verilog [adaptado de 1]:

// Codigo #1 
// sem compartilhar recursos
always @(*) begin
 case(SEL) 
   3'b000: OUT = 1'b0;
   3'b001: OUT = 1'b1;
   3'b010: OUT = (value1 == value2); //equals
   3'b011: OUT = (value1 != value2);  //different
   3'b100: OUT = (value1 >= value2); // greater or equal
   3'b101: OUT = (value1 <= value2); // less or equal
   3'b110: OUT = (value1 < value2); // less
   3'b111: OUT = (value1 > value2); // greater
endcase
// Codigo #2
// compartilhando recursos
assign cmp_equal = (value1 == value2);
assign cmp_greater = (value1 > value2);
always @(*) begin
 case(SEL) 
   3'b000: OUT = 1'b0;
   3'b001: OUT = 1'b1;
   3'b010: OUT = cmp_equal;
   3'b011: OUT = !cmp_equal;
   3'b100: OUT = cmp_equal || cmp_greater;
   3'b101: OUT = !cmp_greater || cmp_equal ;
   3'b110: OUT = !cmp_greater;
   3'b111: OUT = cmp_greater;
endcase

O Código #1 faz uma operação aritmética para cada seleção de entrada SEL. Estas operações têm em comum a comparação entre 2 operandos, value1 e value2. No código #2, duas lógicas de comparação foram assinaladas às nets cmp_equal e cmp_greater. Estas, por sua vez, foram utilizadas nas saídas da seleção em operações lógicas para evitar a replicação de operações de comparação (mais custosas) que envolvem os mesmos operandos e operadores.

6.7 Inversão de barramento

Quando a Distância de Hamming entre o dado atual e o próximo é maior que N/2 (sendo N o tamanho do barramento) é possível invertê-los para minimizar o número de transições:

Figura 6.6 – Para um barramento de 8 bits, quando a distância de hamming entre um dado e o próximo é maior que 4, o número de transições é reduzido ao aplicarmos a inversão (Adaptado de [1]).

Note que um sinal de controle INV é necessário para indicar ao receptor que os dados foram invertidos. Note que mesmo que adicionemos um encoder em Tx e um decoder em Rx, o barramento e portanto as maiores capacitâncias transitaram menos. As maiores correntes são evitadas e a transmissão dos dados consome menos energia.

O texto desta publicação é original. As seguintes fontes foram consultadas: [1] The Art of Hardware Architecture, Mohit Ahora (ISBN 978-1-4614-0397-5)􀀤
[2] Ultra-Low Power Integrated Circuit Design, Niaxiong Nick Tan et al (ISBN 978-1-4419-9973-3)
[3] FSM decomposition by direct circuit manipulation applied to low power design, JC Monteiro et. al. DOI: 10.1109/ASPDAC.2000.835123

Low-power 1/3: eficiência, sistema e software

iphone-battery

1 Eficiência

Nesta publicação sugeri que a Compaq teria percebido a portabilidade como requisito mais que desejável para a computação pessoal. Apesar de os primeiros computadores da companhia terem ~12 kg (!) e serem considerados “portáteis” simplesmente por serem apresentados em uma maleta com alças, ainda precisavam de uma tomada de corrente para funcionar.

Um pouco mais tarde, com a popularização das tecnologias multimídias e dos PDAs (personal digital assistants, ou “palmtops”), a computação portátil começa a precisar de cada vez mais performance, e aí a otimização de consumo passou a ser critério determinante em um projeto. O objetivo é ser eficiente: realizando tanto ou mais trabalho com menos energia, as baterias duram mais. Não sei dizer exatamente quando e nem quem começou as pesquisas no assunto, mas um dos trabalhos acadêmicos mais relevantes na área é datado de 1992 (Low-power CMOS Digital Design, CHANDRAKASAN et. al).

Nesta publicação quero falar um pouco sobre técnicas de baixo consumo e a oportunidade de aplicá-las nas diferentes fases e camadas de abstração de um projeto de um circuito digital.

2 Consumo estático e dinâmico

Dois componentes gerais de consumo são o consumo estático e o consumo dinâmico. Este refere-se ao consumo ocasionado pelas transições de uma porta lógica e é proporcional à corrente circulante, à frequência de transições e às capacitâncias que são carregadas e descarregadas nessas transições. O estático se refere às correntes que circulam entre Vdd e Gnd mesmo quando os módulos de um dispositivo estão desligados. Com os comprimentos de canais dos transístores cada vez menores, as correntes de leakage passaram a ser uma significativa parcela do consumo total, quando não a componente dominante. Quanto menor o canal de um transístor, menor será a sua tensão de threshold e menor será a diferença entre a corrente de operação e a corrente de leakage. Na verdade, a corrente de leakage aumenta exponencialmente com a diminuição da tensão de threshold.

De maneira geral, para um gate lógico:

Potência total = Potência dinâmica + Potência estática [W]

Potência dinamica = Vdd^2 * Freq * Cl* p [W]

onde p é a probabilidade de uma transição lógica ocorrer.

3 Níveis de abstração de um projeto digital

Fazendo uma analogia com carros, uma Lamborghini Diablo 1991 é um sistema igual a um Renault Clio 2010. Ambos são automóveis, do tipo “carro”, compostos por carroceria, 4 rodas, motor, diferencial, volante, transmissão e etc. Porém arquiteturalmente são projetos radicalmente distintos.

Um sistema é concebido para prover uma solução. No caso dos automóveis o problema a ser resolvido é como deslocar algo de um lugar a outro. Se eu preciso levar uma pessoa de um ponto ao outro, eu posso escolher construir um carro, uma moto ou um patinete (que não é um automóvel!). Enfim, as escolhas em nível de sistema, são aquelas que irão definir o que é o meu projeto e quais são suas características. Estas escolhas terão como ponto de partida os requisitos do produto. Se o propósito é deslocar até 5 pessoas de um ponto ao outro e com economia de energia, o Clio faz muito mais sentido que a Lamborghini, se o requisito for um carro.

De maneira geral, podemos representar a estrutura de um projeto, nos seguintes níveis de abstração:

Nível de Sistema: refere-se a definição do conjunto de módulos de um projeto e suas conexões (microprocessador+RAM+NVM+I/O, etc.)

Nivel Arquitetural: refere-se a forma como são construído os módulos definidos no sistema e como eles interagem: a definição de interfaces, dos protocolos de controle, comunicação, etc. (ex.: microprocessador RISC-V 32-bit, 8KB de RAM, 64KB de memória NAND Flash, transreceptor compatível com NFC, camada MAC comunica-se com a PHY através de uma interface AMBA-PB, etc.)

Nível de Registradores (Register Transfer Level): representação do circuito digital como um conjunto de registros, ULAs, Muxes, contadores, etc. Pode ser chamado de “microarquitetura”.

Nível lógico: mapeamento do RTL como um conjunto de portas lógicas, latches e flip-flops.

Nível de Circuito: a representação elétrica do sistema, através de um esquemático de transistores e outros componentes elétricos.

4 Abordagens low-power no nível de sistema

Quando pensamos em sistemas digitais o consumo geralmente estará relacionado à área e performance. Uma maior performance exige uma operação em alta freqüência e com suficiente força de drive. A área relaciona-se com o tamanho dos dispositivos que por sua vez dita o tamanho das capacitâncias a serem carregadas/descarregadas..

O advento dos SoCs, sistemas inteiramente construídos em um único circuito integrado, possibilitou drástico aumento na eficiência energética. A oportunidade do co-projeto hardware software permite a concepção de um sistema com muitos graus de liberdade de design.

Software eficiente

Idealmente o seu compilador deve conseguir produzir um código objeto otimizado, mas ele não tem nenhuma outra informação a não ser o código que você entrega a ele, puro e duro. Compiladores “system aware” só amanhã.

Quantas instruções tem a task mais executada do sistema? Quantos ciclos de clock cada instrução consome? Se um sistema rodando a 5 MHz acorda o processador a cada segundo para executar 500 instruções, admitindo 1 instrução/ciclo, ao diminuírmos somente uma instrução desta task, estaremos dando uma sobrevida a bateria de ~ 6,5 segundos por ano, num sistema 24/7. Escalone isso para mais MHz de operação e mais instruções economizadas, e veja por si só.

A complexidade no tempo e espaço de um código está relacionada às suas primitivas.

Código original

Código otimizado

while(1) {
 if ((i % 10) == 0 )
 {
   // faça algo
 }
 i++;
}
while(1) {
if (counter == 10)
{
   // faça algo
   counter=0;
}
 i++;
 counter++;
}

A operação ‘módulo’ usualmente toma mais ciclos de instrução. É mais econômico reproduzir o mesmo efeito com operações mais baratas.

Código original

Código otimizado

for(i=0;i<10;i++)
{
   // faça algo 1
}
for(i=0;i<10;i++)
{
   // faça algo 2
}
for(i=0;i<10;i++)
{
   // faça algo 1
   // faça algo 2
}

Uma chamada de loop com inicialização, incremento e comparação é economizada.

Código original

Código otimizado

unsigned int x;
 for (x = 0; x < 100; x++)
 {
     A[x] = B[x];
 }
unsigned int x; 
 for (x = 0; x < 100; x += 5 )
 {
     A[x]   = B[x];
     A[x+1] = B[x+1];
     A[x+2] = B[x+2];
     A[x+3] = B[x+3];
     A[x+4] = B[x+4];
     
 }

No código acima, 100 elementos do vetor A serão copiados para as primeiras 100 posições do vetor B. A segunda implementação faz com que o loop precise ser rodado 20 vezes, ao invés de 100.

(Link externo: este AN da Atmel indica no capítulo 9 algumas formas de otimizar tamanho e tempo de execução para AVRs 8-bit)


Na parte 2 vou falar sobre técnicas aplicadas no nível arquitetural (power-gating, clock gating, multi Vdd, multi Vth, DVFS…).

O texto desta publicação é original. As seguintes fontes foram consultadas:
The Art of Hardware Architecture, Mohit Ahora
Ultra-Low Power Integrated Circuit Design, Niaxiong Nick Tan et al.