Quando é melhor usar representações de VETOR vs INTEIRO?

11

No tópico de comentário de uma resposta a esta pergunta: Saídas incorretas na entidade VHDL , foi afirmado:

"Com números inteiros, você não tem controle ou acesso à representação lógica interna no FPGA, enquanto o SLV permite fazer truques como utilizar a cadeia de transporte com eficiência"

Então, em que circunstâncias você achou mais legal codificar usando um vetor de representação de bits do que usar um número inteiro s para acessar a representação interna? E que vantagens você mediu (em termos de área de chip, frequência de clock, atraso ou outros fatores)?

Martin Thompson
fonte
Eu acho que é algo difícil de medir, pois aparentemente é apenas uma questão de controle sobre a implementação de baixo nível.
clabacchio

Respostas:

5

Escrevi o código sugerido por outros dois pôsteres, tanto na forma vectorquanto na integerforma, cuidando para que as duas versões operem da maneira mais semelhante possível.

Comparei os resultados na simulação e depois sintetizei usando o Synplify Pro, direcionando o Xilinx Spartan 6. As amostras de código abaixo são coladas a partir do código de trabalho, para que você possa usá-las com seu sintetizador favorito e ver se ele se comporta da mesma maneira.


Downcounters

Primeiro, o oponente, como sugerido por David Kessner:

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity downcounter is
    generic (top : integer);
    port (clk, reset, enable : in  std_logic; 
         tick   : out std_logic);
end entity downcounter;

Arquitetura vetorial:

architecture vec of downcounter is
begin
    count: process (clk) is
        variable c : unsigned(32 downto 0);  -- don't inadvertently not allocate enough bits here... eg if "integer" becomes 64 bits wide
    begin  -- process count
        if rising_edge(clk) then  
            tick <= '0';
            if reset = '1' then
                c := to_unsigned(top-1, c'length);
            elsif enable = '1' then
                if c(c'high) = '1' then
                    tick <= '1';
                    c := to_unsigned(top-1, c'length);
                else
                    c := c - 1;
                end if;
            end if;
        end if;
    end process count;
end architecture vec;

Arquitetura inteira

architecture int of downcounter is
begin
    count: process (clk) is
        variable c : integer;
    begin  -- process count
        if rising_edge(clk) then  
            tick <= '0';
            if reset = '1' then
                c := top-1;
            elsif enable = '1' then
                if c < 0 then
                    tick <= '1';
                    c := top-1;
                else
                    c := c - 1;
                end if;
            end if;
        end if;
    end process count;
end architecture int;

Resultados

Em termos de código, o número inteiro parece preferível para mim, pois evita as to_unsigned()chamadas. Caso contrário, não há muito a escolher.

A execução no Synplify Pro top := 16#7fff_fffe#produz 66 LUTs para a vectorversão e 64 LUTs para a integerversão. Ambas as versões fazem muito uso da cadeia de transporte. Ambos relatam velocidades de clock superiores a 280 MHz . O sintetizador é capaz de estabelecer um bom uso da cadeia de transporte - verifiquei visualmente com o visualizador de RTL que lógica semelhante é produzida com ambos. Obviamente, um contador com comparador será maior, mas seria o mesmo com números inteiros e vetores novamente.


Dividindo por 2 ** n contadores

Sugerida por ajs410:

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity clkdiv is
    port (clk, reset : in     std_logic;
        clk_2, clk_4, clk_8, clk_16  : buffer std_logic);
end entity clkdiv;

Arquitetura vetorial

architecture vec of clkdiv is

begin  -- architecture a1

    process (clk) is
        variable count : unsigned(4 downto 0);
    begin  -- process
        if rising_edge(clk) then  
            if reset = '1' then
                count  := (others => '0');
            else
                count := count + 1;
            end if;
        end if;
        clk_2 <= count(0);
        clk_4 <= count(1);
        clk_8 <= count(2);
        clk_16 <= count(3);
    end process;

end architecture vec;

Arquitetura inteira

Você precisa pular algumas etapas para evitar o uso to_unsignede a retirada de bits que claramente produziriam o mesmo efeito acima:

architecture int of clkdiv is
begin
    process (clk) is
        variable count : integer := 0;
    begin  -- process
        if rising_edge(clk) then  
            if reset = '1' then
                count  := 0;
                clk_2  <= '0';
                clk_4  <= '0';
                clk_8  <= '0';
                clk_16 <= '0';
            else
                if count < 15 then
                    count := count + 1;
                else
                    count := 0;
                end if;
                clk_2 <= not clk_2;
                for c4 in 0 to 7 loop
                    if count = 2*c4+1 then
                        clk_4 <= not clk_4;
                    end if;
                end loop; 
                for c8 in 0 to 3 loop
                    if count = 4*c8+1 then
                        clk_8 <= not clk_8;
                    end if;
                end loop; 
                for c16 in 0 to 1 loop
                    if count = 8*c16+1 then
                        clk_16 <= not clk_16;
                    end if;
                end loop; 
            end if;
        end if;
    end process;
end architecture int;

Resultados

Em termos de código, neste caso, a vectorversão é claramente melhor!

Em termos de resultados de síntese, para este pequeno exemplo, a versão inteira (como o ajs410 previu) produz 3 LUTs extras como parte dos comparadores; fiquei otimista demais com o sintetizador, embora ele esteja trabalhando com um pedaço de código muito ofuscado!


Outros usos

Os vetores são uma vitória clara quando você deseja aritmética (os contadores podem ser feitos como uma única linha):

vec <= vec + 1 when rising_edge(clk);

vs

if int < int'high then 
   int := int + 1;
else
   int := 0;
end if;

embora pelo menos fique claro a partir desse código que o autor pretendia desvendar.


Algo que não usei em código real, mas ponderei:

O recurso "empacotamento natural" também pode ser utilizado para "computação através de estouros". Quando você sabe que a saída de uma cadeia de adições / subtrações e multiplicações é limitada, você não precisa armazenar os bits altos dos cálculos intermediários, pois (no complemento de 2 s) sairá "na lavagem" no momento em que você chegar à saída. Disseram-me que este artigo contém uma prova disso, mas me pareceu um pouco denso fazer uma avaliação rápida! Teoria da adição e do excesso de computador - HL Garner

Usar integers nessa situação causaria erros de simulação quando eles fossem quebrados, mesmo sabendo que eles serão desembrulhados no final.


E, como Philippe apontou, quando você precisa de um número maior que 2 ** 31, não tem escolha a não ser usar vetores.

Martin Thompson
fonte
No segundo bloco de código você tem variable c : unsigned(32 downto 0);... então não é cuma variável de 33 bits?
clabacchio
@clabacchio: sim, isso permite o acesso ao 'carry-bit' para ver o contorno.
Martin Thompson
5

Ao escrever VHDL, eu recomendo usar std_logic_vector (slv) em vez de inteiro (int) para SIGNALS . (Por outro lado, usar int para genéricos, algumas constantes e algumas variáveis ​​pode ser muito útil.) Simplificando, se você declarar um sinal do tipo int ou precisar especificar um intervalo para um número inteiro, provavelmente está fazendo algo errado.

O problema com int é que o programador VHDL não tem idéia de qual é a representação lógica interna do int e, portanto, não podemos tirar proveito disso. Por exemplo, se eu definir um int do intervalo de 1 a 10, não faço ideia de como o compilador codifica esses valores. Espero que seja codificado como 4 bits, mas não sabemos muito além disso. Se você pudesse detectar os sinais dentro do FPGA, ele poderia ser codificado como "0001" para "1010" ou codificado como "0000" para "1001". Também é possível que seja codificado de uma maneira que não faça absolutamente nenhum sentido para nós, seres humanos.

Em vez disso, devemos apenas usar slv em vez de int, porque temos controle sobre a codificação e também temos acesso direto aos bits individuais. Ter acesso direto é importante, como você verá mais adiante.

Poderíamos simplesmente lançar um int para slv sempre que precisarmos acessar os bits individuais, mas isso fica muito confuso, muito rápido. É como ter o pior dos dois mundos em vez do melhor dos dois mundos. Seu código será difícil para o compilador otimizar e quase impossível para você ler. Eu não recomendo isso.

Então, como eu disse, com slv você tem controle sobre as codificações de bits e acesso direto aos bits. Então, o que você pode fazer com isso? Vou mostrar alguns exemplos. Digamos que você precise emitir um pulso uma vez a cada 4.294.000.000 de relógios. Aqui está como você faria isso com int:

signal count :integer range 0 to 4293999999;  -- a 32 bit integer

process (clk)
begin
  if rising_edge(clk) then
    if count = 4293999999 then  -- The important line!
      count <= 0;
      pulse <= '1';
    else
      count <= count + 1;
      pulse <= '0';
    end if;
  end if;
end process;

E o mesmo código usando slv:

use ieee.numeric_std.all;
signal count :std_logic_vector (32 downto 0);  -- a 33 bit integer, one extra bit!

process (clk)
begin
  if rising_edge(clk) then
    if count(count'high)='1' then   -- The important line!
      count <= std_logic_vector(4293999999-1,count'length);
      pulse <= '1';
    else
      count <= count - 1;
      pulse <= '0';
    end if;
  end if;
end process;

A maior parte desse código é idêntica entre int e slv, pelo menos no sentido do tamanho e velocidade da lógica resultante. É claro que um está contando e o outro está contando, mas isso não é importante para este exemplo.

A diferença está na "linha importante".

Com o exemplo int, isso resultará em um comparador de 32 entradas. Com LUTs de 4 entradas que o Xilinx Spartan-3 usa, isso exigirá 11 LUTs e 3 níveis de lógica. Alguns compiladores podem converter isso em uma subtração que usará a cadeia de transporte e abrangerá o equivalente a 32 LUTs, mas poderá correr mais rápido que 3 níveis de lógica.

Com o exemplo slv, não há comparação de 32 bits, portanto, são "zero LUTs, zero níveis de lógica". A única penalidade é que nosso contador é um bit extra. Como o tempo adicional para esse bit extra de contador está na cadeia de transporte, há um atraso de tempo adicional "quase zero".

Claro que este é um exemplo extremo, pois a maioria das pessoas não usaria um contador de 32 bits dessa maneira. Aplica-se a contadores menores, mas a diferença será menos dramática, embora ainda significativa.

Este é apenas um exemplo de como utilizar slv over int para obter um tempo mais rápido. Existem muitas outras maneiras de utilizar o slv - basta apenas um pouco de imaginação.

Atualização: Adicionado itens para tratar dos comentários de Martin Thompson sobre o uso de int com "if (count-1) <0"

(Nota: Eu suponho que você quis dizer "se conte <0", pois isso tornaria mais equivalente à minha versão slv e eliminaria a necessidade dessa subtração extra).

Sob algumas circunstâncias, isso pode gerar a implementação lógica pretendida, mas não é garantido que funcione o tempo todo. Depende do seu código e de como o seu compilador codifica o valor int.

Dependendo do seu compilador, e como você especifica o intervalo de seu int, é perfeitamente possível que um valor int de zero não codifique para um vetor de bits de "0000 ... 0000" quando ele entra na lógica do FPGA. Para que sua variação funcione, ela deve codificar para "0000 ... 0000".

Por exemplo, digamos que você defina um int para ter um intervalo de -5 a +5. Você espera que um valor de 0 seja codificado em 4 bits como "0000" e +5 como "0101" e -5 como "1011". Este é o esquema de codificação típico de dois complementos.

Mas não assuma que o compilador usará complemento duplo. Embora incomum, o complemento de um pode resultar em uma lógica "melhor". Ou, o compilador pode usar uma espécie de codificação "tendenciosa" onde -5 é codificado como "0000", 0 como "0101" e +5 como "1010".

Se a codificação do int estiver "correta", o compilador provavelmente inferirá o que fazer com o bit de transporte. Mas se estiver incorreto, a lógica resultante será horrível.

É possível que o uso de um int dessa maneira possa resultar em tamanho e velocidade lógicos razoáveis, mas isso não é uma garantia. Mudar para um compilador diferente (XST para Sinopse, por exemplo), ou ir para uma arquitetura FPGA diferente pode causar a coisa errada.

Não assinado / assinado vs. slv é mais um debate. Você pode agradecer ao comitê do governo dos EUA por nos dar tantas opções em VHDL. :) Eu uso slv porque esse é o padrão para interface entre módulos e núcleos. Fora isso, e alguns outros casos em simulações, acho que não há um grande benefício em usar slv em vez de assinado / não assinado. Também não tenho certeza se os sinais assinados / não assinados são compatíveis.

Martin Thompson
fonte
4
David, esses fragmentos de código não são equivalentes. Conta-se de zero a um número arbitrário (com um caro operador de comparação); o outro conta até zero a partir de um número arbitrário. Você pode escrever ambos os algoritmos com números inteiros ou vetores e obterá resultados ruins ao contar para um número arbitrário e bons resultados para zero. Observe que os engenheiros de software também contariam até zero se precisassem obter um pouco mais de desempenho de um hot loop.
Philippe
1
Como Philippe, não estou convencido de que essa seja uma comparação válida. Se o exemplo inteiro for contado e usado if (count-1) < 0, acho que o sintetizador inferirá o bit de execução e produzirá o mesmo circuito do seu exemplo slv. Além disso, não deveríamos estar usando o unsignedtipo estes dias :)
Martin Thompson
2
@DavidKessner você certamente forneceu uma resposta completa e bem fundamentada, você recebeu o meu +1. Mas tenho que perguntar ... por que você está preocupado com a otimização em todo o design? Não seria melhor concentrar seus esforços nas áreas de código que o exigem ou focar em SLVs para pontos de interface (portas de entidade) para compatibilidade? Sei que, na maioria dos meus projetos, não me importo particularmente que o uso da LUT seja minimizado, desde que atenda ao tempo e seja adequado à peça. Se eu tiver restrições particularmente rígidas, certamente estaria mais consciente do design ideal, mas não como regra geral.
akohlsmith
2
Estou um pouco surpreso com o número de votos positivos para esta resposta. @ bit_vector @ é certamente o nível de abstração correto para modelar e otimizar micro-arquiteturas, mas uma recomendação geral refere-se a tipos de "alto nível", como @ integer @ para sinais e portas, é algo que acho estranho. Eu já vi códigos complicados e ilegíveis suficientes devido à falta de abstração para saber o valor que esses recursos fornecem e ficaria muito triste se eu tivesse que deixá-los para trás.
trondd
2
@david Excelentes comentários. É verdade que ainda estamos na era medieval em comparação com o desenvolvimento de software de várias maneiras, mas pela minha experiência com a síntese integrada Quartus e o Synplify, não acho que as coisas sejam tão ruins. Eles são capazes de lidar com várias coisas, como a retimulação de registros e outras otimizações que melhoram o desempenho enquanto mantêm a legibilidade. Duvido que a maioria tenha como alvo várias cadeias de ferramentas e dispositivos, mas para o seu caso eu entendo o requisito para o denominador menos comum :-).
21813 trondd
2

Meu conselho é tentar os dois e depois analisar os relatórios de síntese, mapa e localização e rota. Esses relatórios informarão exatamente quantas LUTs cada abordagem está consumindo; também informarão a velocidade máxima na qual a lógica pode operar.

Eu concordo com David Kessner que você está à mercê de sua cadeia de ferramentas e não há uma resposta "certa". A síntese é magia negra e a melhor maneira de saber o que aconteceu é ler com cuidado e profundidade os relatórios produzidos. As ferramentas Xilinx até permitem ver dentro do FPGA, até como cada LUT é programada, como a cadeia de transporte está conectada, como a estrutura do comutador conecta todos os LUTs, etc.

Para outro exemplo dramático da abordagem do Sr. Kessner, imagine que você deseja ter várias frequências de clock em 1/2, 1/4, 1/8, 1/16 etc. Você pode usar um número inteiro que conta constantemente todos os ciclos, e, em seguida, tenha vários comparadores em relação a esse valor inteiro, com cada saída do comparador formando uma divisão de relógio diferente. Dependendo do número de comparadores, o fanout pode se tornar excessivamente grande e começar a consumir LUTs extras apenas para buffer. A abordagem SLV levaria apenas cada bit individual do vetor como saída.

ajs410
fonte
1

Uma razão óbvia é que assinados e não assinados permitem valores maiores que o número inteiro de 32 bits. Essa é uma falha no design da linguagem VHDL, que não é essencial. Uma nova versão do VHDL poderia corrigir isso, exigindo valores inteiros para suportar tamanho arbitrário (semelhante ao BigInt do Java).

Fora isso, estou muito interessado em saber sobre benchmarks com desempenho diferente para números inteiros em comparação com vetores.

Por outro lado, Jan Decaluwe escreveu um belo ensaio sobre o assunto: Estas entradas são feitas para a Countin '

Philippe
fonte
Graças Philippe (embora isso não é uma "melhor através do acesso à representação interna" do aplicativo, que é o que eu sou realmente depois ...)
Martin Thompson
Esse ensaio é bom, mas ignora completamente a implementação subjacente e a velocidade e tamanho lógicos resultantes. Concordo com a maior parte do que Decaluwe diz, mas ele não diz nada sobre os resultados da síntese. Às vezes, os resultados da síntese não importam, e às vezes importam. Portanto, é uma decisão judicial.
1
@ David, eu concordo que Jan não entra em detalhes completos sobre como as ferramentas de síntese reagem aos números inteiros. Mas, não, não é um julgamento. Você pode medir os resultados da síntese e determinar os resultados da sua ferramenta de síntese. Penso que o OP significou sua pergunta como um desafio para produzirmos fragmentos de código e resultados de síntese que demonstrem uma diferença (se houver) no desempenho.
Philippe
@ Philippe Não, eu quis dizer que é um julgamento se você se importa com os resultados da síntese. Não é que os resultados da síntese sejam um julgamento.
@DavidKessner OK. Eu entendi errado.
Philippe