Quando è meglio usare le rappresentazioni VECTOR vs INTEGER?


11

Nel thread dei commenti su una risposta a questa domanda: output errati nell'entità VHDL è stato dichiarato:

"Con gli interi non hai il controllo o l'accesso alla rappresentazione della logica interna nell'FPGA, mentre SLV ti consente di fare trucchi come utilizzare la catena di trasporto in modo efficiente"

Quindi, in quali circostanze hai trovato più semplice codificare usando un vettore di rappresentazione dei bit piuttosto che usare numeri interi per accedere alla rappresentazione interna? E quali vantaggi hai misurato (in termini di area del chip, frequenza di clock, ritardo o altro)?


Penso che sia qualcosa di difficile da misurare, dal momento che apparentemente è solo una questione di controllo sull'implementazione di basso livello.
clabacchio

Risposte:


5

Ho scritto il codice suggerito da altri due poster in entrambi vectore in integerforma, avendo cura di far funzionare entrambe le versioni nel modo più simile possibile.

Ho confrontato i risultati nella simulazione e poi sintetizzati usando Synplify Pro prendendo di mira Xilinx Spartan 6. I seguenti esempi di codice sono incollati dal codice di lavoro, quindi dovresti essere in grado di usarli con il tuo sintetizzatore preferito e vedere se si comporta allo stesso modo.


Downcounters

Innanzitutto, il downcounter, come suggerito da 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;

Architettura vettoriale:

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;

Architettura intera

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;

risultati

Per quanto riguarda il codice, quello intero mi sembra preferibile in quanto evita le to_unsigned()chiamate. Altrimenti, non c'è molto da scegliere.

L'esecuzione attraverso Synplify Pro con top := 16#7fff_fffe#produce 66 LUT per la vectorversione e 64 LUT per la integerversione. Entrambe le versioni fanno molto uso della catena di trasporto. Entrambi riportano velocità di clock superiori a 280 MHz . Il sintetizzatore è abbastanza in grado di stabilire un buon uso della catena di trasporto - ho verificato visivamente con il visualizzatore RTL che una logica simile è prodotta con entrambi. Ovviamente un up-counter con comparatore sarà più grande, ma sarebbe lo stesso con numeri interi e vettori di nuovo.


Divisione per 2 ** n segnalini

Suggerito da 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;

Architettura vettoriale

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;

Architettura intera

Devi saltare attraverso alcuni cerchi per evitare di usare to_unsignede poi prelevare i pezzi che produrrebbe chiaramente lo stesso effetto di cui sopra:

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;

risultati

Per quanto riguarda il codice, in questo caso, la vectorversione è chiaramente migliore!

In termini di risultati di sintesi, per questo piccolo esempio, la versione intera (come previsto da ajs410) produce 3 LUT extra come parte dei comparatori, ero troppo ottimista riguardo al sintetizzatore, anche se funziona con un pezzo di codice terribilmente offuscato!


Altri usi

I vettori sono una chiara vittoria quando vuoi che l'aritmetica si avvolga (i contatori possono essere fatti come una singola linea uniforme):

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

vs

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

sebbene almeno da quel codice sia chiaro che l'autore intendeva concludere.


Qualcosa che non ho usato nel codice reale, ma ho riflettuto:

La funzione "avvolgimento naturale" può essere utilizzata anche per "elaborazione tramite overflow". Quando sai che l'output di una catena di addizioni / sottrazioni e moltiplicazioni è limitato, non è necessario memorizzare i bit alti dei calcoli intermedi poiché (nel complemento a 2 s) uscirà "nel lavaggio" quando arrivi all'output. Mi è stato detto che questo documento contiene una prova di ciò, ma mi è sembrato un po 'denso fare una rapida valutazione! Teoria dell'aggiunta e degli overflow del computer - HL Garner

L'utilizzo di integers in questa situazione provocherebbe errori di simulazione quando si concludevano, anche se sappiamo che alla fine verranno scartati.


E come ha sottolineato Philippe, quando hai bisogno di un numero maggiore di 2 ** 31 non hai altra scelta che usare i vettori.


Nel secondo blocco di codice hai variable c : unsigned(32 downto 0);... non è cuna variabile a 33 bit allora?
Clabacchio

@clabacchio: sì, che consente l'accesso al "carry-bit" per vedere il wrapping.
Martin Thompson,

5

Quando scrivo VHDL, consiglio vivamente di usare std_logic_vector (slv) invece di intero (int) per SIGNALS . (D'altra parte, usare int per generici, alcune costanti e alcune variabili può essere molto utile.) In poche parole, se dichiari un segnale di tipo int o devi specificare un intervallo per un numero intero, probabilmente stai facendo Qualcosa non va.

Il problema con int è che il programmatore VHDL non ha idea di quale sia la rappresentazione della logica interna di int, e quindi non possiamo trarne vantaggio. Ad esempio, se definisco un int compreso nell'intervallo da 1 a 10, non ho idea di come il compilatore codifichi questi valori. Speriamo che venga codificato come 4 bit, ma non sappiamo molto oltre. Se si potesse sondare i segnali all'interno dell'FPGA, potrebbe essere codificato da "0001" a "1010" o codificato da "0000" a "1001". È anche possibile che sia codificato in un modo che non ha assolutamente senso per noi umani.

Invece dovremmo semplicemente usare slv invece di int, perché allora abbiamo il controllo sulla codifica e abbiamo anche accesso diretto ai singoli bit. Avere accesso diretto è importante, come vedrai più avanti.

Potremmo semplicemente lanciare un int in slv ogni volta che abbiamo bisogno di accedere ai singoli bit, ma questo diventa davvero disordinato, molto veloce. È come ottenere il peggio di entrambi i mondi invece del meglio dei due mondi. Il codice sarà difficile da ottimizzare per il compilatore e quasi impossibile da leggere. Non lo consiglio.

Quindi, come ho detto, con slv hai il controllo sulle codifiche dei bit e l'accesso diretto ai bit. Quindi cosa puoi fare con questo? Ti mostrerò un paio di esempi. Diciamo che è necessario emettere un impulso una volta ogni 4.294.000.000 di orologi. Ecco come lo faresti con 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 lo stesso codice 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;

Gran parte di questo codice è identico tra int e slv, almeno nel senso delle dimensioni e della velocità della logica risultante. Naturalmente uno conta e l'altro conta, ma questo non è importante per questo esempio.

La differenza sta nella "linea importante".

Con l'esempio int, questo comporterà un comparatore a 32 input. Con i LUT a 4 input utilizzati dallo Xilinx Spartan-3, ciò richiederà 11 LUT e 3 livelli di logica. Alcuni compilatori potrebbero convertirlo in una sottrazione che utilizzerà la catena di trasporto e estenderà l'equivalente di 32 LUT ma potrebbe funzionare più velocemente di 3 livelli di logica.

Con l'esempio slv, non esiste un confronto a 32 bit, quindi è "zero LUT, zero livelli di logica". L'unica penalità è che il nostro segnalino è un bit in più. Poiché la temporizzazione aggiuntiva per questo ulteriore bit di contatore è tutta nella catena di trasporto, c'è un ritardo di temporizzazione "quasi zero" aggiuntivo.

Naturalmente questo è un esempio estremo, poiché la maggior parte delle persone non userebbe un contatore a 32 bit in questo modo. Si applica ai contatori più piccoli, ma la differenza sarà meno drammatica sebbene ancora significativa.

Questo è solo un esempio di come utilizzare slv su int per ottenere tempi più rapidi. Ci sono molti altri modi per utilizzare slv: basta solo un po 'di immaginazione.

Aggiornamento: aggiunte cose per rispondere ai commenti di Martin Thompson sull'uso di int con "if (count-1) <0"

(Nota: suppongo che volessi dire "if count <0", poiché ciò renderebbe più equivalente alla mia versione SLV e rimuoverebbe la necessità di quella sottrazione aggiuntiva.)

In alcune circostanze, ciò potrebbe generare l'implementazione della logica prevista, ma non è garantito che funzioni sempre. Dipenderà dal tuo codice e da come il tuo compilatore codifica il valore int.

A seconda del compilatore e di come si specifica l'intervallo del proprio int, è del tutto possibile che un valore int di zero non codifichi in un vettore di bit "0000 ... 0000" quando lo fa nella logica FPGA. Perché la tua variazione funzioni, deve essere codificata in "0000 ... 0000".

Ad esempio, supponiamo che tu definisca un int per avere un intervallo da -5 a +5. Ti aspetti che il valore 0 sia codificato in 4 bit come "0000" e +5 come "0101" e -5 come "1011". Questo è il tipico schema di codifica a due complementi.

Ma non dare per scontato che il compilatore utilizzerà due complementi. Sebbene insolito, un complemento potrebbe portare a una logica "migliore". In alternativa, il compilatore potrebbe utilizzare una sorta di codifica "distorta" in cui -5 è codificato come "0000", 0 come "0101" e +5 come "1010".

Se la codifica di int è "corretta", probabilmente il compilatore dedurrà cosa fare con il bit di riporto. Ma se non è corretto, la logica risultante sarà orribile.

È possibile che l'utilizzo di un int in questo modo possa determinare dimensioni e velocità logiche ragionevoli, ma non è una garanzia. Passare a un altro compilatore (ad esempio da XST a sinossi) o passare a una diversa architettura FPGA potrebbe causare l'esatto errore.

Unsigned / Signed vs. slv è l'ennesimo dibattito. Puoi ringraziare il comitato del governo degli Stati Uniti per averci fornito così tante opzioni in VHDL. :) Uso slv perché è lo standard per l'interfaccia tra moduli e core. A parte questo, e alcuni altri casi nelle simulazioni, non penso che ci sia un enorme vantaggio nell'utilizzare slv rispetto a firmato / non firmato. Non sono inoltre sicuro che segnali firmati / non firmati supportino.


4
David, quei frammenti di codice non sono equivalenti. Uno conta da zero a un numero arbitrario (con un costoso operatore di confronto); l'altro conta fino a zero da un numero arbitrario. Puoi scrivere entrambi gli algoritmi con numeri interi o vettori e otterrai risultati negativi quando conti verso un numero arbitrario e buoni risultati contano verso zero. Si noti che gli ingegneri del software farebbero anche il conto alla rovescia fino a zero se avessero bisogno di ottenere un po 'più di prestazioni da un hot loop.
Philippe

1
Come Philippe, non sono convinto che questo sia un confronto valido. Se l'esempio intero contasse e usasse if (count-1) < 0, penso che il sintetizzatore inferirà il bit di esecuzione e produrrà più o meno lo stesso circuito dell'esempio slv. Inoltre, non dovremmo usare il unsignedtipo in questi giorni :)
Martin Thompson,

2
@DavidKessner hai sicuramente fornito una risposta COMPLETA e ben motivata, hai il mio +1. Devo chiederti però ... perché sei preoccupato per l'ottimizzazione in tutto il design? Non sarebbe meglio concentrare i tuoi sforzi sulle aree di codice che lo richiedono o concentrarsi sugli SLV per i punti di interfaccia (porte entità) per la compatibilità? So che nella maggior parte dei miei progetti non mi interessa particolarmente che l'uso di LUT sia ridotto al minimo, purché rispetti i tempi e si adatti al pezzo. Se avessi vincoli particolarmente severi, sarei sicuramente più consapevole del design ottimale, ma non come regola generale.
akohlsmith il

2
Sono un po 'sorpreso dal numero di voti positivi su questa risposta. @ bit_vector @ è certamente il livello di astrazione corretto per la modellazione e l'ottimizzazione delle micro-architetture, ma una raccomandazione generale riguarda tipi di "alto livello" come @ integer @ per segnali e porte è qualcosa che trovo strano. Ho visto abbastanza codice contorto e illeggibile a causa della mancanza di astrazione per conoscere il valore fornito da queste funzionalità e sarebbe molto triste se dovessi lasciarle indietro.
trondd

2
@david Osservazioni eccellenti. È vero che siamo ancora nell'età medievale rispetto allo sviluppo del software in molti modi, ma dalla mia esperienza con la sintesi integrata di Quartus e Synplify non penso che le cose siano così brutte. Sono abbastanza in grado di gestire un sacco di cose come retiming dei registri e altre ottimizzazioni che migliorano le prestazioni mantenendo la leggibilità. Dubito che la maggior parte stia prendendo di mira diverse toolchain e dispositivi, ma per il tuo caso capisco il requisito per il minimo comune denominatore :-).
trond

2

Il mio consiglio è di provare entrambi, quindi guardare i rapporti di sintesi, mappa e luogo e percorso. Questi rapporti ti diranno esattamente quanti LUT consuma ogni approccio, ti diranno anche la massima velocità alla quale la logica può operare.

Concordo con David Kessner che sei in balia della tua toolchain e non esiste una risposta "giusta". La sintesi è magia nera e il modo migliore per sapere cosa è successo è leggere attentamente e accuratamente i rapporti che vengono prodotti. Gli strumenti Xilinx ti consentono persino di vedere all'interno dell'FPGA, fino a come è programmata ogni LUT, come è collegata la catena di trasporto, come il tessuto dell'interruttore collega tutte le LUT, ecc.

Per un altro esempio drammatico dell'approccio di Mr. Kessner, immagina di voler avere più frequenze di clock a 1/2, 1/4, 1/8, 1/16, ecc. Puoi usare un numero intero che conti costantemente ogni ciclo, e quindi hanno più comparatori rispetto a quel valore intero, con ogni uscita del comparatore che forma una diversa divisione di clock. A seconda del numero di comparatori, il fanout potrebbe diventare irragionevolmente grande e iniziare a consumare LUT extra solo per il buffering. L'approccio SLV prenderebbe semplicemente ogni singolo bit del vettore come output.


1

Un ovvio motivo è che con segno e senza segno consentono valori più grandi dell'intero a 32 bit. Questo è un difetto nel design del linguaggio VHDL, che non è essenziale. Una nuova versione di VHDL potrebbe risolverlo, richiedendo valori interi per supportare dimensioni arbitrarie (simile a BigInt di Java).

A parte questo, sono molto interessato a conoscere i benchmark che funzionano diversamente per i numeri interi rispetto ai vettori.

A proposito, Jan Decaluwe ha scritto un bel saggio su questo: Questi Ints sono fatti per Countin '


Grazie Philippe (anche se non è un'applicazione "migliore attraverso l'accesso alla rappresentazione interna", che è quello che sto davvero cercando ...)
Martin Thompson,

Questo saggio è carino, ma ignora completamente l'implementazione sottostante e la velocità e le dimensioni della logica risultanti. Sono d'accordo con la maggior parte di ciò che dice Decaluwe, ma non dice nulla sui risultati della sintesi. A volte i risultati della sintesi non contano, a volte lo fanno. Quindi è una chiamata di giudizio.

1
@ David, sono d'accordo che Jan non approfondisce in dettaglio il modo in cui gli strumenti di sintesi reagiscono agli interi. Ma no, non è un giudizio. Puoi misurare i risultati di sintesi e determinare i risultati del tuo strumento di sintesi dato. Penso che l'OP intendesse la sua domanda come una sfida per noi per produrre frammenti di codice e risultati di sintesi che dimostrano una differenza (se presente) nelle prestazioni.
Philippe

@Philippe No, intendevo dire che è un appello al giudizio se ti preoccupi affatto dei risultati della sintesi. Non che i risultati della sintesi stessi siano un appello al giudizio.

@DavidKessner OK. Ho frainteso.
Philippe
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.