Wann ist es ordentlicher, VECTOR-Darstellungen gegen INTEGERs zu verwenden?

11

Im Kommentarthread zu einer Antwort auf diese Frage: Falsche Ausgaben in der VHDL-Entität wurde angegeben:

"Mit ganzen Zahlen haben Sie keine Kontrolle oder keinen Zugriff auf die interne Logikdarstellung im FPGA, während Sie mit SLV Tricks wie die effiziente Nutzung der Übertragskette ausführen können."

Unter welchen Umständen war es für Sie einfacher, mit einem Vektor der Bitdarstellung zu codieren als mit Ganzzahlen, um auf die interne Darstellung zuzugreifen? Und welche Vorteile haben Sie gemessen (in Bezug auf Chipfläche, Taktfrequenz, Verzögerung oder auf andere Weise)?

Martin Thompson
quelle
Ich denke, es ist schwer zu messen, da es anscheinend nur um die Kontrolle über die Implementierung auf niedriger Ebene geht.
Clabacchio

Antworten:

5

Ich habe den von zwei anderen Postern vorgeschlagenen Code in beiden vectorund geschriebeninteger Form, wobei darauf geachtet beiden Versionen in so ähnliche Weise wie möglich arbeiten zu lassen.

Ich habe die Ergebnisse in der Simulation verglichen und dann mit Synplify Pro für Xilinx Spartan 6 synthetisiert. Die folgenden Codebeispiele werden aus dem Arbeitscode eingefügt, sodass Sie sie mit Ihrem bevorzugten Synthesizer verwenden können und prüfen können, ob er sich gleich verhält.


Downcounters

Erstens der Downcounter, wie von David Kessner vorgeschlagen:

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;

Vektorarchitektur:

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;

Ganzzahlige Architektur

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;

Ergebnisse

Code-weise scheint mir die Ganzzahl vorzuziehen, da sie das vermeidet to_unsigned() Aufrufe . Ansonsten nicht viel zu wählen.

Wenn Sie es über Synplify Pro ausführen, top := 16#7fff_fffe#werden 66 LUTs für die vectorVersion und 64 LUTs für die integerVersion erzeugt. Beide Versionen machen viel Gebrauch von der Tragekette. Beide melden Taktraten über 280 MHz . Der Synthesizer ist durchaus in der Lage, die Übertragungskette gut zu nutzen - ich habe visuell mit dem RTL-Viewer überprüft, dass mit beiden eine ähnliche Logik erzeugt wird. Natürlich wird ein Aufwärtszähler mit Komparator größer sein, aber das wäre sowohl bei ganzen Zahlen als auch bei Vektoren wieder dasselbe.


Teilen durch 2 ** n Zähler

Vorgeschlagen von 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;

Vektorarchitektur

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;

Ganzzahlige Architektur

Sie müssen durch einige Reifen springen, um zu vermeiden, dass Sie nur to_unsignedBits verwenden und dann auswählen, was eindeutig den gleichen Effekt wie oben erzeugen würde:

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;

Ergebnisse

Code-weise ist in diesem Fall die vectorVersion deutlich besser!

In Bezug auf die Syntheseergebnisse erzeugt die ganzzahlige Version (wie von ajs410 vorhergesagt) für dieses kleine Beispiel 3 zusätzliche LUTs als Teil der Komparatoren. Ich war zu optimistisch in Bezug auf den Synthesizer, obwohl er mit einem schrecklich verschleierten Code funktioniert!


Andere Verwendungen

Vektoren sind ein klarer Gewinn, wenn die Arithmetik umbrochen werden soll (Zähler können sogar als einzelne Zeile ausgeführt werden):

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

vs.

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

obwohl zumindest aus diesem Code hervorgeht, dass der Autor eine Umhüllung beabsichtigt hat.


Etwas, das ich nicht in Real-Code verwendet habe, sondern überlegt habe:

Die Funktion "Natürlich einwickeln" kann auch zum "Berechnen durch Überläufe" verwendet werden. Wenn Sie wissen, dass die Ausgabe einer Kette von Additionen / Subtraktionen und Multiplikationen begrenzt ist, müssen Sie die hohen Bits der Zwischenberechnungen nicht speichern, da sie (im 2-s-Komplement) "in der Wäsche" herauskommen. bis Sie zum Ausgang kommen. Mir wurde gesagt, dass dieses Papier einen Beweis dafür enthält, aber es sah für mich etwas dicht aus, um eine schnelle Einschätzung vorzunehmen! Theorie der Computeraddition und -überläufe - HL Garner

Die Verwendung von integers in dieser Situation würde Simulationsfehler verursachen, wenn sie verpackt werden, obwohl wir wissen, dass sie am Ende entpackt werden.


Und wie Philippe betonte, haben Sie keine andere Wahl, als Vektoren zu verwenden, wenn Sie eine Zahl größer als 2 ** 31 benötigen.

Martin Thompson
quelle
Im zweiten Codeblock haben Sie variable c : unsigned(32 downto 0);... ist dann keine c33-Bit-Variable?
Clabacchio
@clabacchio: Ja, das ermöglicht den Zugriff auf das 'Carry-Bit', um den Wrap-Around zu sehen.
Martin Thompson
5

Beim Schreiben von VHDL empfehle ich dringend, std_logic_vector (slv) anstelle von integer (int) für SIGNALS zu verwenden . (Andererseits kann die Verwendung von int für Generika, einige Konstanten und einige Variablen sehr nützlich sein.) Einfach ausgedrückt, wenn Sie ein Signal vom Typ int deklarieren oder einen Bereich für eine Ganzzahl angeben müssen, tun Sie dies wahrscheinlich etwas stimmt nicht.

Das Problem mit int ist, dass der VHDL-Programmierer keine Ahnung hat, wie die interne Logik des int aussieht, und wir sie daher nicht nutzen können. Wenn ich beispielsweise ein Int im Bereich von 1 bis 10 definiere, habe ich keine Ahnung, wie der Compiler diese Werte codiert. Hoffentlich wird es als 4 Bit codiert, aber darüber hinaus wissen wir nicht viel. Wenn Sie die Signale im FPGA prüfen könnten, könnten sie als "0001" bis "1010" oder als "0000" bis "1001" codiert sein. Es ist auch möglich, dass es auf eine Weise codiert ist, die für uns Menschen absolut keinen Sinn ergibt.

Stattdessen sollten wir nur slv anstelle von int verwenden, da wir dann die Kontrolle über die Codierung haben und auch direkten Zugriff auf die einzelnen Bits haben. Ein direkter Zugriff ist wichtig, wie Sie später sehen werden.

Wir könnten einfach ein int in slv umwandeln, wenn wir Zugriff auf die einzelnen Bits benötigen, aber das wird sehr chaotisch, sehr schnell. Das ist, als würde man das Schlimmste aus beiden Welten bekommen, anstatt das Beste aus beiden Welten. Ihr Code ist für den Compiler schwer zu optimieren und für Sie fast unmöglich zu lesen. Ich empfehle das nicht.

Wie gesagt, mit slv haben Sie die Kontrolle über die Bitcodierungen und den direkten Zugriff auf die Bits. Was können Sie damit machen? Ich zeige Ihnen einige Beispiele. Angenommen, Sie müssen alle 4.294.000.000 Takte einmal einen Impuls ausgeben. So würden Sie dies mit int machen:

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;

Und der gleiche Code mit 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;

Der größte Teil dieses Codes ist zwischen int und slv identisch, zumindest im Sinne der Größe und Geschwindigkeit der resultierenden Logik. Natürlich zählt einer hoch und der andere runter, aber das ist für dieses Beispiel nicht wichtig.

Der Unterschied liegt in "der wichtigen Linie".

Mit dem Beispiel int wird dies zu einem Komparator mit 32 Eingängen führen. Bei 4-Eingangs-LUTs, die der Xilinx Spartan-3 verwendet, sind 11 LUTs und 3 Logikstufen erforderlich. Einige Compiler konvertieren dies möglicherweise in eine Subtraktion, die die Übertragskette verwendet und das Äquivalent von 32 LUTs umfasst, jedoch möglicherweise schneller als 3 Logikebenen ausgeführt wird.

Im SLV-Beispiel gibt es keinen 32-Bit-Vergleich, also "Null LUTs, Null Logikpegel". Die einzige Strafe ist, dass unser Zähler ein zusätzliches Bit ist. Da das zusätzliche Timing für dieses zusätzliche Zählerbit alle in der Übertragskette liegt, gibt es eine zusätzliche Timing-Verzögerung "fast Null".

Dies ist natürlich ein extremes Beispiel, da die meisten Leute auf diese Weise keinen 32-Bit-Zähler verwenden würden. Dies gilt zwar für kleinere Zähler, der Unterschied ist jedoch weniger dramatisch, obwohl er immer noch signifikant ist.

Dies ist nur ein Beispiel für die Verwendung von slv over int, um ein schnelleres Timing zu erzielen. Es gibt viele andere Möglichkeiten, slv zu nutzen - es bedarf nur einiger Vorstellungskraft.

Update: Es wurden Inhalte hinzugefügt, um Martin Thompsons Kommentare zur Verwendung von int mit "if (count-1) <0" zu adressieren.

(Hinweis: Ich gehe davon aus, dass Sie "if count <0" gemeint haben, da dies meiner SLV-Version mehr entspricht und die Notwendigkeit dieser zusätzlichen Subtraktion beseitigt.)

Unter bestimmten Umständen kann dies zu der beabsichtigten Logikimplementierung führen, es kann jedoch nicht garantiert werden, dass sie immer funktioniert. Dies hängt von Ihrem Code ab und davon, wie Ihr Compiler den int-Wert codiert.

Abhängig von Ihrem Compiler und der Art und Weise, wie Sie den Bereich Ihres int angeben, ist es durchaus möglich, dass ein int-Wert von Null nicht in einen Bitvektor von "0000 ... 0000" codiert, wenn er in die FPGA-Logik aufgenommen wird. Damit Ihre Variation funktioniert, muss sie in "0000 ... 0000" codiert sein.

Angenommen, Sie definieren ein Int mit einem Bereich von -5 bis +5. Sie erwarten, dass ein Wert von 0 in 4 Bits wie "0000" und +5 als "0101" und -5 als "1011" codiert wird. Dies ist das typische Zweikomplement-Codierungsschema.

Aber nehmen Sie nicht an, dass der Compiler Zweierkomplement verwenden wird. Obwohl ungewöhnlich, könnte das Einsen-Komplement zu einer "besseren" Logik führen. Oder der Compiler könnte eine Art "voreingenommene" Codierung verwenden, wobei -5 als "0000", 0 als "0101" und +5 als "1010" codiert wird.

Wenn die Codierung des int "korrekt" ist, wird der Compiler wahrscheinlich ableiten, was mit dem Übertragsbit zu tun ist. Aber wenn es falsch ist, wird die resultierende Logik schrecklich sein.

Es ist möglich, dass die Verwendung eines int auf diese Weise zu einer angemessenen logischen Größe und Geschwindigkeit führt, dies ist jedoch keine Garantie. Wenn Sie zu einem anderen Compiler wechseln (z. B. XST zu Synopsis) oder zu einer anderen FPGA-Architektur wechseln, kann genau das Falsche passieren.

Unsigned / Signed vs. slv ist eine weitere Debatte. Sie können dem Ausschuss der US-Regierung dafür danken, dass er uns so viele Optionen für VHDL gegeben hat. :) Ich benutze slv, weil dies der Standard für die Schnittstelle zwischen Modulen und Kernen ist. Abgesehen davon und in einigen anderen Fällen in Simulationen glaube ich nicht, dass die Verwendung von slv gegenüber signierten / nicht signierten einen großen Vorteil hat. Ich bin mir auch nicht sicher, ob signierte / nicht signierte Unterstützung dreifach angegebene Signale unterstützen.

Martin Thompson
quelle
4
David, diese Codefragmente sind nicht gleichwertig. Man zählt von Null bis zu einer beliebigen Zahl (mit einem teuren Vergleichsoperator); der andere zählt von einer beliebigen Zahl auf Null herunter. Sie können beide Algorithmen entweder mit ganzen Zahlen oder Vektoren schreiben, und Sie erhalten schlechte Ergebnisse, wenn Sie auf eine beliebige Zahl zählen, und gute Ergebnisse, wenn Sie auf Null zählen. Beachten Sie, dass Softwareentwickler auch bis Null zählen würden, wenn sie etwas mehr Leistung aus einem Hot-Loop herausholen müssten.
Philippe
1
Wie Philippe bin ich nicht davon überzeugt, dass dies ein gültiger Vergleich ist. Wenn das ganzzahlige Beispiel heruntergezählt und verwendet if (count-1) < 0würde, würde der Synthesizer wahrscheinlich auf das Übertragsbit schließen und ungefähr die gleiche Schaltung wie Ihr SLV-Beispiel erzeugen. Auch sollten wir den unsignedTyp heutzutage nicht verwenden :)
Martin Thompson
2
@ DavidKessner Sie haben sicherlich eine gründliche und gut begründete Antwort gegeben, Sie haben meine +1. Ich muss allerdings fragen ... warum machst du dir Sorgen um die Optimierung während des gesamten Designs? Wäre es nicht besser, Ihre Bemühungen auf die Codebereiche zu konzentrieren, die dies erfordern, oder sich aus Kompatibilitätsgründen auf SLVs für Schnittstellenpunkte (Entity-Ports) zu konzentrieren? Ich weiß, dass es mir bei den meisten meiner Designs nicht besonders wichtig ist, dass der LUT-Einsatz minimiert wird, solange er dem Timing entspricht und zum Teil passt. Wenn ich besonders enge Einschränkungen habe, wäre ich mir des optimalen Designs sicherlich bewusster, aber in der Regel nicht.
Akohlsmith
2
Ich bin etwas überrascht über die Anzahl der Stimmen zu dieser Antwort. @ bit_vector @ ist sicherlich die richtige Abstraktionsebene für die Modellierung und Optimierung von Mikroarchitekturen, aber eine allgemeine Empfehlung gegen "High-Level" -Typen wie @ integer @ für Signale und Port finde ich seltsam. Ich habe aufgrund der fehlenden Abstraktion genug verschlungenen und unlesbaren Code gesehen, um den Wert dieser Funktionen zu kennen, und wäre sehr traurig, wenn ich sie zurücklassen müsste.
Trondd
2
@ David Hervorragende Bemerkungen. Es ist wahr, dass wir im Vergleich zur Softwareentwicklung in vielerlei Hinsicht noch im Mittelalter sind, aber aufgrund meiner Erfahrung mit der integrierten Synthese und Synplifizierung von Quartus denke ich nicht, dass die Dinge so schlecht sind. Sie sind durchaus in der Lage, viele Dinge wie Register-Retiming und andere Optimierungen zu handhaben, die die Leistung verbessern und gleichzeitig die Lesbarkeit gewährleisten. Ich bezweifle, dass die Mehrheit auf mehrere Toolchains und Geräte abzielt, aber für Ihren Fall verstehe ich die Anforderung für den kleinsten gemeinsamen Nenner :-).
Trondd
2

Mein Rat ist, beides zu versuchen und dann die Synthese-, Karten- und Orts- und Routenberichte zu betrachten. In diesen Berichten erfahren Sie genau, wie viele LUTs jeder Ansatz verbraucht. Außerdem erfahren Sie, mit welcher maximalen Geschwindigkeit die Logik arbeiten kann.

Ich stimme David Kessner zu, dass Sie Ihrer Toolchain ausgeliefert sind und es keine "richtige" Antwort gibt. Synthese ist schwarze Magie und der beste Weg zu wissen, was passiert ist, besteht darin, die erstellten Berichte sorgfältig und gründlich zu lesen. Mit den Xilinx-Tools können Sie sogar im FPGA sehen, wie jede LUT programmiert ist, wie die Übertragskette verbunden ist, wie die Switch Fabric alle LUTs verbindet usw.

Stellen Sie sich für ein weiteres dramatisches Beispiel für Herrn Kessners Ansatz vor, Sie möchten mehrere Taktfrequenzen bei 1/2, 1/4, 1/8, 1/16 usw. haben. Sie könnten eine ganze Zahl verwenden, die jeden Zyklus ständig hochzählt. und dann mehrere Komparatoren gegen diesen ganzzahligen Wert haben, wobei jeder Komparatorausgang eine andere Taktteilung bildet. Abhängig von der Anzahl der Komparatoren kann der Fanout unangemessen groß werden und zusätzliche LUTs nur zum Puffern verbrauchen. Der SLV-Ansatz würde nur jedes einzelne Bit des Vektors als Ausgabe nehmen.

ajs410
quelle
1

Ein offensichtlicher Grund ist, dass vorzeichenbehaftete und vorzeichenlose Werte größere Werte als die 32-Bit-Ganzzahl zulassen. Dies ist ein Fehler im VHDL-Sprachdesign, der nicht unbedingt erforderlich ist. Eine neue Version von VHDL könnte dies beheben und erfordert ganzzahlige Werte, um eine beliebige Größe zu unterstützen (ähnlich wie bei Java's BigInt).

Abgesehen davon bin ich sehr interessiert an Benchmarks, die für Ganzzahlen im Vergleich zu Vektoren eine andere Leistung erbringen.

Übrigens, Jan Decaluwe hat einen schönen Aufsatz dazu geschrieben: Diese Ints sind für Countin gemacht

Philippe
quelle
Danke Philippe (obwohl das keine Anwendung ist, die "besser durch Zugriff auf die interne Repräsentation" ist, was ich wirklich will ...)
Martin Thompson
Dieser Aufsatz ist nett, ignoriert jedoch die zugrunde liegende Implementierung und die daraus resultierende logische Geschwindigkeit und Größe vollständig. Ich stimme den meisten Aussagen von Decaluwe zu, aber er sagt nichts über die Ergebnisse der Synthese. Manchmal spielen die Ergebnisse der Synthese keine Rolle, manchmal auch. Es ist also ein Urteilsspruch.
1
@ David, ich stimme zu, dass Jan nicht im Detail darauf eingeht, wie Synthesewerkzeuge auf ganze Zahlen reagieren. Aber nein, es ist kein Urteilsspruch. Sie können die Syntheseergebnisse messen und die Ergebnisse Ihres gegebenen Synthesewerkzeugs bestimmen. Ich denke, das OP bedeutete seine Frage als Herausforderung für uns, Codefragmente und Syntheseergebnisse zu erstellen, die einen Leistungsunterschied (falls vorhanden) aufweisen.
Philippe
@Philippe Nein, ich meinte, es ist ein Urteilsspruch, wenn Sie sich überhaupt um die Syntheseergebnisse kümmern. Nicht dass die Syntheseergebnisse selbst ein Urteilsspruch sind.
@ DavidKessner OK. Ich habe es falsch verstanden.
Philippe