Quand est-il plus approprié d'utiliser des représentations VECTOR vs INTEGER?

11

Dans le fil de commentaires sur une réponse à cette question: Sorties incorrectes dans l'entité VHDL, il a été déclaré:

"Avec des entiers, vous n'avez pas le contrôle ou l'accès à la représentation logique interne dans le FPGA, tandis que SLV vous permet de faire des trucs comme l'utilisation efficace de la chaîne de transport"

Alors, dans quelles circonstances avez-vous trouvé plus pratique de coder en utilisant un vecteur de représentation de bits qu'en utilisant des entiers pour accéder à la représentation interne? Et quels avantages avez-vous mesurés (en termes de surface de puce, de fréquence d'horloge, de retard ou autre)?

Martin Thompson
la source
Je pense que c'est quelque chose de difficile à mesurer, car apparemment, c'est juste une question de contrôle sur la mise en œuvre de bas niveau.
clabacchio

Réponses:

5

J'ai écrit le code suggéré par deux autres affiches à la fois vectoret integersous forme, en veillant à ce que les deux versions fonctionnent de la manière la plus similaire possible.

J'ai comparé les résultats en simulation, puis synthétisé à l'aide de Synplify Pro ciblant Xilinx Spartan 6. Les exemples de code ci-dessous sont collés à partir du code de travail, vous devriez donc pouvoir les utiliser avec votre synthétiseur préféré et voir s'il se comporte de la même manière.


Décomptes

Tout d'abord, le downcounter, comme l'a suggéré 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;

Architecture vectorielle:

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;

Architecture entière

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;

Résultats

Côté code, l'entier me semble préférable car il évite les to_unsigned()appels. Sinon, pas grand chose à choisir.

Son exécution via Synplify Pro avec top := 16#7fff_fffe#produit 66 LUT pour la vectorversion et 64 LUT pour la integerversion. Les deux versions utilisent beaucoup la chaîne de transport. Les deux signalent des vitesses d'horloge supérieures à 280 MHz . Le synthétiseur est tout à fait capable d'établir une bonne utilisation de la chaîne de transport - J'ai vérifié visuellement avec le visualiseur RTL qu'une logique similaire est produite avec les deux. De toute évidence, un compteur avec comparateur sera plus grand, mais ce serait la même chose avec les entiers et les vecteurs.


Division par 2 ** n compteurs

Suggéré par 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;

Architecture vectorielle

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;

Architecture entière

Vous devez sauter à travers certains cerceaux pour éviter de simplement utiliser to_unsignedpuis de retirer des bits qui produiraient clairement le même effet que ci-dessus:

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;

Résultats

Côté code, dans ce cas, la vectorversion est nettement meilleure!

En termes de résultats de synthèse, pour ce petit exemple, la version entière (comme l'a annoncé ajs410) produit 3 LUT supplémentaires dans le cadre des comparateurs, j'étais trop optimiste quant au synthétiseur, bien qu'il fonctionne avec un morceau de code terriblement obscurci!


Autres utilisations

Les vecteurs sont clairement gagnants lorsque vous voulez que l'arithmétique se termine (les compteurs peuvent être effectués sur une seule ligne):

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

contre

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

bien qu'au moins il ressorte clairement de ce code que l'auteur avait l'intention de boucler.


Quelque chose que je n'ai pas utilisé en code réel, mais que j'ai réfléchi:

La fonction «naturellement enveloppante» peut également être utilisée pour «calculer par débordements». Lorsque vous savez que la sortie d'une chaîne d'additions / soustractions et de multiplications est limitée, vous n'avez pas à stocker les bits élevés des calculs intermédiaires car (en complément de 2 s), ils sortiront "au lavage" au moment où vous arrivez à la sortie. On me dit que ce document en contient une preuve, mais il m'a paru un peu dense pour faire une évaluation rapide! Théorie de l'addition et des débordements informatiques - HL Garner

L'utilisation de integers dans cette situation entraînerait des erreurs de simulation lors de leur encapsulation, même si nous savons qu'ils se dérouleront à la fin.


Et comme Philippe l'a souligné, lorsque vous avez besoin d'un nombre supérieur à 2 ** 31, vous n'avez pas d'autre choix que d'utiliser des vecteurs.

Martin Thompson
la source
Dans le deuxième bloc de code que vous avez variable c : unsigned(32 downto 0);... n'est-ce pas cune variable de 33 bits?
clabacchio
@clabacchio: oui, cela permet d'accéder au 'carry-bit' pour voir le bouclage.
Martin Thompson
5

Lors de l'écriture de VHDL, je recommande fortement d'utiliser std_logic_vector (slv) au lieu de integer (int) pour SIGNALS . (D'un autre côté, utiliser int pour les génériques, certaines constantes et certaines variables peuvent être très utiles.) Autrement dit, si vous déclarez un signal de type int, ou devez spécifier une plage pour un entier, vous faites probablement Quelque chose ne va pas.

Le problème avec int est que le programmeur VHDL n'a aucune idée de la représentation logique interne de l'int, et nous ne pouvons donc pas en profiter. Par exemple, si je définis un entier compris entre 1 et 10, je n'ai aucune idée de la façon dont le compilateur code ces valeurs. Espérons que ce serait codé en 4 bits, mais nous ne savons pas grand-chose au-delà. Si vous pouviez sonder les signaux à l'intérieur du FPGA, il pourrait être codé de "0001" à "1010", ou codé de "0000" à "1001". Il est également possible qu'il soit codé d'une manière qui n'a absolument aucun sens pour nous, les humains.

Au lieu de cela, nous devrions simplement utiliser slv au lieu de int, car nous contrôlons alors le codage et avons également un accès direct aux bits individuels. Avoir un accès direct est important, comme vous le verrez plus tard.

Nous pourrions simplement lancer un int en slv chaque fois que nous avons besoin d'accéder aux bits individuels, mais cela devient vraiment désordonné, très rapide. C'est comme obtenir le pire des deux mondes au lieu du meilleur des deux mondes. Votre code sera difficile à optimiser pour le compilateur et presque impossible à lire. Je ne le recommande pas.

Donc, comme je l'ai dit, avec slv, vous avez le contrôle sur les codages de bits et l'accès direct aux bits. Alors, que pouvez-vous faire avec ça? Je vais vous montrer quelques exemples. Disons que vous devez émettre une impulsion toutes les 4 294 000 000 d'horloges. Voici comment procéder avec 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;

Et le même code en utilisant 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;

La plupart de ce code est identique entre int et slv, au moins dans le sens de la taille et de la vitesse de la logique résultante. Bien sûr, l'un compte et l'autre compte à rebours, mais ce n'est pas important pour cet exemple.

La différence réside dans "la ligne importante".

Avec l'exemple int, cela va aboutir à un comparateur à 32 entrées. Avec les LUT à 4 entrées utilisées par le Xilinx Spartan-3, cela nécessitera 11 LUT et 3 niveaux de logique. Certains compilateurs peuvent convertir cela en une soustraction qui utilisera la chaîne de transport et s'étendra sur l'équivalent de 32 LUT mais pourrait fonctionner plus rapidement que 3 niveaux de logique.

Avec l'exemple slv, il n'y a pas de comparaison 32 bits, c'est donc "zéro LUT, zéro niveau de logique". La seule pénalité est que notre compteur est un bit supplémentaire. Étant donné que la synchronisation supplémentaire pour ce bit supplémentaire de compteur est entièrement dans la chaîne de transport, il y a un retard de synchronisation supplémentaire "presque nul".

Bien sûr, c'est un exemple extrême, car la plupart des gens n'utiliseraient pas un compteur 32 bits de cette manière. Cela s'applique aux compteurs plus petits, mais la différence sera moins dramatique, mais toujours significative.

Ce n'est qu'un exemple de la façon d'utiliser slv over int pour obtenir un timing plus rapide. Il existe de nombreuses autres façons d'utiliser slv - cela ne prend que de l'imagination.

Mise à jour: ajout de trucs pour répondre aux commentaires de Martin Thompson sur l'utilisation de int avec "if (count-1) <0"

(Remarque: je suppose que vous vouliez dire "si count <0", car cela le rendrait plus équivalent à ma version slv et éliminerait le besoin de cette soustraction supplémentaire.)

Dans certaines circonstances, cela peut générer l'implémentation logique prévue, mais il n'est pas garanti de fonctionner tout le temps. Cela dépendra de votre code et de la façon dont votre compilateur code la valeur int.

En fonction de votre compilateur et de la façon dont vous spécifiez la plage de votre int, il est tout à fait possible qu'une valeur int de zéro ne soit pas codée en un vecteur binaire "0000 ... 0000" lorsqu'elle est intégrée dans la logique FPGA. Pour que votre variation fonctionne, elle doit coder en "0000 ... 0000".

Par exemple, disons que vous définissez un int pour avoir une plage de -5 à +5. Vous vous attendez à ce qu'une valeur de 0 soit codée en 4 bits comme "0000", et +5 en "0101" et -5 en "1011". Il s'agit du schéma de codage typique à deux compléments.

Mais ne supposez pas que le compilateur va utiliser deux compléments. Bien que inhabituel, le complément à un pourrait entraîner une "meilleure" logique. Ou bien, le compilateur pourrait utiliser une sorte de codage "biaisé" où -5 est codé comme "0000", 0 comme "0101" et +5 comme "1010".

Si le codage de l'int est "correct", le compilateur déduira probablement quoi faire avec le bit de retenue. Mais si elle est incorrecte, la logique résultante sera horrible.

Il est possible que l'utilisation d'un int de cette manière puisse entraîner une taille et une vitesse logiques raisonnables, mais ce n'est pas une garantie. Le passage à un autre compilateur (XST vers Synopsis par exemple) ou le passage à une architecture FPGA différente pourrait provoquer la mauvaise chose exacte.

Non signé / signé vs slv est encore un autre débat. Vous pouvez remercier le comité du gouvernement américain de nous avoir donné autant d'options en VHDL. :) J'utilise slv car c'est la norme d'interface entre les modules et les cœurs. En dehors de cela, et de certains autres cas dans les simulations, je ne pense pas qu'il y ait un énorme avantage à utiliser slv sur signé / non signé. Je ne suis pas sûr non plus que les signaux à trois états pris en charge signés / non signés.

Martin Thompson
la source
4
David, ces fragments de code ne sont pas équivalents. On compte de zéro à un nombre arbitraire (avec un opérateur de comparaison coûteux); l'autre décompte jusqu'à zéro à partir d'un nombre arbitraire. Vous pouvez écrire les deux algorithmes avec des entiers ou des vecteurs, et vous obtiendrez de mauvais résultats en comptant vers un nombre arbitraire et de bons résultats en comptant vers zéro. Notez que les ingénieurs logiciels décomptent également jusqu'à zéro s'ils ont besoin d'extraire un peu plus de performances d'une boucle chaude.
Philippe
1
Comme Philippe, je ne suis pas convaincu que cette comparaison soit valable. Si l'exemple entier compte à rebours et utilisé, if (count-1) < 0je pense que le synthétiseur déduira le bit de fin et produira à peu près le même circuit que votre exemple slv. De plus, ne devrions-nous pas utiliser le unsignedtype de nos jours :)
Martin Thompson
2
@DavidKessner vous avez certainement fourni une RÉPONDRE et une réponse bien raisonnée, vous avez mon +1. Je dois cependant demander ... pourquoi vous inquiétez-vous de l'optimisation tout au long de la conception? Ne serait-il pas préférable de concentrer vos efforts sur les domaines de code qui en ont besoin ou de vous concentrer sur les SLV pour les points d'interface (ports d'entité) pour la compatibilité? Je sais que dans la plupart de mes conceptions, je ne me soucie pas particulièrement que l'utilisation de LUT soit minimisée, tant qu'elle respecte le calendrier et s'adapte à la pièce. Si j'ai des contraintes particulièrement strictes, je serais certainement plus conscient de la conception optimale, mais pas en règle générale.
akohlsmith
2
Je suis un peu surpris par le nombre de votes positifs sur cette réponse. @ bit_vector @ est certainement le niveau d'abstraction correct pour modéliser et optimiser les micro-architectures, mais une recommandation générale contre les types "de haut niveau" tels que @ integer @ pour les signaux et le port est quelque chose que je trouve étrange. J'ai vu suffisamment de code alambiqué et illisible en raison du manque d'abstraction pour connaître la valeur de ces fonctionnalités, et je serais très triste si je devais les laisser derrière.
trondd
2
@david Excellentes remarques. Il est vrai que nous sommes encore à l'ère médiévale par rapport au développement de logiciels à bien des égards, mais d'après mon expérience avec la synthèse intégrée de Quartus et Synplify, je ne pense pas que les choses soient si mauvaises. Ils sont tout à fait capables de gérer de nombreuses choses comme la resynchronisation des registres et d'autres optimisations qui améliorent les performances tout en maintenant la lisibilité. Je doute que la majorité cible plusieurs chaînes d'outils et appareils, mais pour votre cas, je comprends l'exigence du dénominateur le moins commun :-).
trondd
2

Mon conseil est d'essayer les deux, puis d'examiner la synthèse, la carte et les rapports de lieu et itinéraire. Ces rapports vous indiqueront exactement combien de LUT consomment chaque approche, ils vous indiqueront également la vitesse maximale à laquelle la logique peut fonctionner.

Je suis d'accord avec David Kessner que vous êtes à la merci de votre chaîne d'outils et qu'il n'y a pas de "bonne" réponse. La synthèse est de la magie noire et la meilleure façon de savoir ce qui s'est passé est de lire attentivement et attentivement les rapports produits. Les outils Xilinx vous permettent même de voir à l'intérieur du FPGA, jusqu'à la façon dont chaque LUT est programmée, comment la chaîne de transport est connectée, comment la structure du commutateur connecte toutes les LUT, etc.

Pour un autre exemple dramatique de l'approche de M. Kessner, imaginez que vous voulez avoir plusieurs fréquences d'horloge à 1/2, 1/4, 1/8, 1/16, etc. Vous pouvez utiliser un entier qui compte constamment à chaque cycle, puis avoir plusieurs comparateurs par rapport à cette valeur entière, chaque sortie de comparateur formant une division d'horloge différente. Selon le nombre de comparateurs, le fanout peut devenir déraisonnablement important et commencer à consommer des LUT supplémentaires juste pour la mise en mémoire tampon. L'approche SLV prendrait simplement chaque bit individuel du vecteur comme sortie.

ajs410
la source
1

Une raison évidente est que les signés et les non signés autorisent des valeurs plus grandes que l'entier 32 bits. C'est une faille dans la conception du langage VHDL, qui n'est pas essentielle. Une nouvelle version de VHDL pourrait résoudre ce problème, nécessitant des valeurs entières pour prendre en charge la taille arbitraire (semblable à BigInt de Java).

En dehors de cela, je suis très intéressé d'entendre parler de benchmarks qui fonctionnent différemment pour les entiers par rapport aux vecteurs.

BTW, Jan Decaluwe a écrit un bel essai à ce sujet: Ces Ints sont faits pour Countin '

Philippe
la source
Merci Philippe (bien que ce ne soit pas une application "mieux grâce à l'accès à la représentation interne", ce que je recherche vraiment ...)
Martin Thompson
Cet essai est agréable, mais ignore complètement l'implémentation sous-jacente et la vitesse et la taille logiques résultantes. Je suis d'accord avec la plupart des propos de Decaluwe, mais il ne dit rien sur les résultats de la synthèse. Parfois, les résultats de la synthèse n'ont pas d'importance, et parfois ils le sont. C'est donc un appel au jugement.
1
@David, je suis d'accord que Jan n'entre pas dans tous les détails sur la façon dont les outils de synthèse réagissent aux nombres entiers. Mais non, ce n'est pas un jugement. Vous pouvez mesurer les résultats de la synthèse et déterminer les résultats de votre outil de synthèse donné. Je pense que l'OP signifiait sa question comme un défi pour nous de produire des fragments de code et des résultats de synthèse qui démontrent une différence (le cas échéant) dans les performances.
Philippe
@Philippe Non, je voulais dire que c'est une question de jugement si vous vous souciez du tout des résultats de la synthèse. Ce n'est pas que les résultats de la synthèse eux-mêmes soient un jugement.
@DavidKessner OK. J'ai mal compris.
Philippe