Kiedy lepiej jest używać reprezentacji VECTOR vs. INTEGER?

11

W wątku komentarza do odpowiedzi na to pytanie: Niepoprawne dane wyjściowe w jednostce VHDL stwierdzono:

„Dzięki liczbom całkowitym nie masz kontroli ani dostępu do wewnętrznej reprezentacji logiki w FPGA, podczas gdy SLV pozwala na takie sztuczki, jak efektywne wykorzystanie łańcucha przenoszenia”

Więc w jakich okolicznościach łatwiej jest kodować za pomocą wektora reprezentacji bitów niż za pomocą liczb całkowitych w celu uzyskania dostępu do wewnętrznej reprezentacji? A jakie zalety mierzyłeś (pod względem powierzchni chipa, częstotliwości zegara, opóźnienia lub innych).

Martin Thompson
źródło
Myślę, że jest to coś trudnego do zmierzenia, ponieważ najwyraźniej jest to tylko kwestia kontroli nad implementacją niskiego poziomu.
clabacchio

Odpowiedzi:

5

Napisałem kod sugerowany przez dwa inne plakaty w obu vectori integerformie, dbając o to, aby obie wersje działały w podobny sposób, jak to możliwe.

Porównałem wyniki w symulacji, a następnie zsyntetyzowałem za pomocą narzędzia Synplify Pro ukierunkowanego na Xilinx Spartan 6. Poniższe próbki kodu zostały wklejone z działającego kodu, więc powinieneś być w stanie używać ich z ulubionym syntezatorem i sprawdzić, czy zachowuje się tak samo.


Downcounters

Po pierwsze, downcounter, jak zasugerował 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;

Architektura wektorowa:

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;

Architektura liczb całkowitych

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;

Wyniki

Pod względem kodowym liczba całkowita wydaje mi się lepsza, ponieważ pozwala uniknąć to_unsigned()połączeń. W przeciwnym razie nie wiele do wyboru.

Uruchomienie go przez Synplify Pro z top := 16#7fff_fffe#produkuje 66 LUT dla vectorwersji i 64 LUT dla integerwersji. Obie wersje w dużym stopniu wykorzystują łańcuch nośny. Obie prędkości zegara przekraczają 280 MHz . Syntezator jest w stanie zapewnić dobre wykorzystanie łańcucha przenoszenia - wizualnie zweryfikowałem za pomocą przeglądarki RTL, że z podobną logiką powstaje oba te elementy. Oczywiście licznik dodatni z komparatorem będzie większy, ale to samo byłoby znowu z liczbami całkowitymi i wektorami.


Dzielenie przez 2 ** n liczników

Sugerowane przez 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;

Architektura wektorowa

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;

Architektura liczb całkowitych

Musisz skakać przez kilka obręczy, aby uniknąć użycia, to_unsigneda następnie zerwania kawałków, co wyraźnie dałoby taki sam efekt jak powyżej:

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;

Wyniki

Pod względem kodowym vectorwersja jest w tym przypadku wyraźnie lepsza!

Jeśli chodzi o wyniki syntezy, w tym niewielkim przykładzie wersja całkowita (jak przewidywano ajs410) wytwarza 3 dodatkowe LUT jako część komparatorów, byłem zbyt optymistyczny co do syntezatora, chociaż działa on z okropnie zaciemnionym fragmentem kodu!


Inne zastosowania

Wektory są wyraźną wygraną, gdy chcesz, aby arytmetyka się zawijała (liczniki można wykonać nawet jako pojedynczą linię):

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

vs

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

chociaż przynajmniej z tego kodu jasno wynika, że ​​autor zamierzał zawinąć.


Coś, czego nie użyłem w kodzie rzeczywistym, ale zastanawiałem się:

Funkcja „owijania w sposób naturalny” może być również wykorzystana do „obliczeń poprzez przepełnienia”. Kiedy wiesz, że wynik łańcucha dodawania / odejmowania i mnożenia jest ograniczony, nie musisz przechowywać wysokich bitów obliczeń pośrednich, ponieważ (w uzupełnieniu 2-sekundowym) wyjdzie „w praniu” zanim dojdziesz do wyjścia. Powiedziano mi, że ten artykuł zawiera dowód na to, ale dokonanie szybkiej oceny wydawało mi się trochę gęste! Teoria dodawania i przepełnienia komputera - HL Garner

Użycie integers w tej sytuacji spowodowałoby błędy symulacji po ich zawinięciu, nawet jeśli wiemy, że ostatecznie się rozpakują.


I jak zauważył Philippe, kiedy potrzebujesz liczby większej niż 2 ** 31, nie masz wyboru, jak korzystać z wektorów.

Martin Thompson
źródło
W drugim bloku kodu, który masz variable c : unsigned(32 downto 0);... nie jest więc czmienna 33-bitowa?
clabacchio
@clabacchio: tak, to pozwala na dostęp do „carry-bit”, aby zobaczyć zawijanie.
Martin Thompson
5

Pisząc VHDL, zdecydowanie polecam użycie std_logic_vector (slv) zamiast liczby całkowitej (int) dla SIGNALS . (Z drugiej strony, użycie int dla ogólnych, niektóre stałe i niektóre zmienne mogą być bardzo przydatne.) Mówiąc prosto, jeśli deklarujesz sygnał typu int lub musisz określić zakres dla liczby całkowitej, prawdopodobnie robisz to coś źle.

Problem z int polega na tym, że programista VHDL nie ma pojęcia, czym jest wewnętrzna logiczna reprezentacja int, więc nie możemy z tego skorzystać. Na przykład, jeśli zdefiniuję liczbę całkowitą z zakresu od 1 do 10, nie mam pojęcia, jak kompilator koduje te wartości. Mam nadzieję, że będzie zakodowany jako 4 bity, ale nie wiemy zbyt wiele poza tym. Jeśli możesz sondować sygnały wewnątrz układu FPGA, może być zakodowany jako „0001” na „1010” lub zakodowany jako „0000” na „1001”. Możliwe jest również, że jest zakodowany w sposób, który nie ma dla nas żadnego sensu.

Zamiast tego powinniśmy po prostu użyć slv zamiast int, ponieważ wtedy mamy kontrolę nad kodowaniem, a także bezpośredni dostęp do poszczególnych bitów. Posiadanie bezpośredniego dostępu jest ważne, jak zobaczycie później.

Moglibyśmy po prostu rzucić int na slv, ilekroć potrzebujemy dostępu do poszczególnych bitów, ale robi się to naprawdę bałagan, bardzo szybko. To tak, jakby uzyskać najgorsze z obu światów zamiast najlepszych z obu światów. Twój kod będzie trudny do zoptymalizowania przez kompilator i prawie niemożliwy do odczytania. Nie polecam tego.

Tak więc, jak powiedziałem, przy pomocy slv masz kontrolę nad kodowaniem bitów i bezpośredni dostęp do bitów. Co możesz z tym zrobić? Pokażę ci kilka przykładów. Powiedzmy, że musisz wyprowadzać impuls raz na 4 294 000 000 zegarów. Oto jak zrobiłbyś to z 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;

I ten sam kod przy użyciu 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;

Większość tego kodu jest identyczna między int i slv, przynajmniej w sensie wielkości i szybkości wynikowej logiki. Oczywiście jeden się odlicza, a drugi odlicza, ale to nie jest ważne w tym przykładzie.

Różnica polega na „ważnej linii”.

W przykładzie int spowoduje to 32-wejściowy komparator. W przypadku 4-wejściowych LUT, których używa Xilinx Spartan-3, będzie to wymagało 11 LUT i 3 poziomów logiki. Niektóre kompilatory mogą konwertować to na odejmowanie, które wykorzysta łańcuch przenoszenia i obejmie równowartość 32 LUT, ale może działać szybciej niż 3 poziomy logiki.

W przykładzie SLV nie ma porównania 32-bitowego, więc jest to „zero LUT, zero poziomów logiki”. Jedyną karą jest to, że nasz licznik to jeden dodatkowy bit. Ponieważ dodatkowe taktowanie dla tego dodatkowego bitu licznika znajduje się w łańcuchu przenoszenia, występuje dodatkowe „prawie zerowe” opóźnienie taktowania.

Oczywiście jest to skrajny przykład, ponieważ większość ludzi nie używałaby 32-bitowego licznika w ten sposób. Dotyczy to mniejszych liczników, ale różnica będzie mniej dramatyczna, choć nadal znacząca.

Jest to tylko jeden przykład wykorzystania slv w stosunku do int, aby uzyskać szybsze synchronizowanie. Istnieje wiele innych sposobów wykorzystania slv - potrzeba tylko trochę wyobraźni.

Aktualizacja: Dodano elementy, aby odpowiedzieć na komentarze Martina Thompsona dotyczące używania int z „if (count-1) <0”

(Uwaga: Zakładam, że miałeś na myśli „if count <0”, ponieważ dzięki temu byłby bardziej równoważny z moją wersją slv i usunąłby potrzebę dodatkowego odejmowania.)

W niektórych okolicznościach może to generować zamierzoną implementację logiki, ale nie gwarantuje się, że będzie działać przez cały czas. Będzie to zależeć od twojego kodu i tego, jak kompilator koduje wartość int.

W zależności od kompilatora i tego, jak określisz zakres wartości int, jest całkiem możliwe, że wartość int równa zero nie koduje wektora bitowego „0000 ... 0000”, kiedy przekształca się w logikę FPGA. Aby Twoja odmiana zadziałała, musi zakodować ją jako „0000 ... 0000”.

Załóżmy na przykład, że definiujesz wartość int w zakresie od -5 do +5. Oczekujesz, że wartość 0 zostanie zakodowana w 4 bitach, takich jak „0000”, i +5 jako „0101” i -5 jako „1011”. Jest to typowy schemat kodowania z uzupełnieniem dwójkowym.

Ale nie zakładaj, że kompilator będzie używał komplementu dwóch. Chociaż jest to niezwykłe, uzupełnienie jednego może skutkować „lepszą” logiką. Lub kompilator może zastosować rodzaj „stronniczego” kodowania, w którym -5 jest kodowane jako „0000”, 0 jako „0101”, a +5 jako „1010”.

Jeśli kodowanie int jest „poprawne”, kompilator prawdopodobnie wywnioskuje, co zrobić z bitem przeniesienia. Ale jeśli jest niepoprawny, wynikowa logika będzie okropna.

Możliwe, że użycie int w ten sposób może spowodować rozsądną wielkość logiczną i szybkość, ale nie jest to gwarancją. Przejście na inny kompilator (na przykład XST na Synopsis) lub przejście na inną architekturę FPGA może spowodować, że coś złego się stanie.

Unsigned / Signed vs. slv to kolejna debata. Możesz podziękować komitetowi rządu USA za udostępnienie nam tak wielu opcji w VHDL. :) Używam slv, ponieważ jest to standard interfejsu między modułami i rdzeniami. Poza tym i niektórymi innymi przypadkami symulacji, nie sądzę, aby korzystanie z SLV nad podpisanymi / niepodpisanymi było ogromną korzyścią. Nie jestem również pewien, czy podpisane / niepodpisane obsługują potrójne sygnały.

Martin Thompson
źródło
4
David, te fragmenty kodu nie są równoważne. Zlicza się od zera do dowolnej liczby (z drogim operatorem porównania); drugi odlicza do zera z dowolnej liczby. Możesz pisać oba algorytmy za pomocą liczb całkowitych lub wektorów, a otrzymasz złe wyniki, licząc do dowolnej liczby, i dobre wyniki licząc do zera. Zauważ, że inżynierowie oprogramowania również odliczaliby do zera, gdyby musieli wycisnąć nieco więcej wydajności z gorącej pętli.
Philippe
1
Podobnie jak Philippe, nie jestem przekonany, że jest to prawidłowe porównanie. Jeśli odliczony zostanie przykład liczby całkowitej i użyty if (count-1) < 0, pomyślałbym, że syntezator wyprowadzi bit przeprowadzania i wytworzy taki sam obwód jak twój przykład slv. Nie powinniśmy też używać tego unsignedtypu w dzisiejszych czasach :)
Martin Thompson,
2
@DavidKessner z pewnością podałeś DOKŁADNIE i dobrze uzasadnioną odpowiedź, masz moją +1. Muszę jednak zapytać ... dlaczego martwisz się o optymalizację w całym projekcie? Czy nie lepiej byłoby skoncentrować swoje wysiłki na obszarach kodu, które tego wymagają, lub skupić się na SLV dla punktów interfejsu (portów encji) w celu zapewnienia zgodności? Wiem, że w większości moich projektów nie dbam szczególnie o to, aby wykorzystanie LUT było zminimalizowane, o ile spełnia ono czas i pasuje do części. Gdybym miał szczególnie wąskie ograniczenia, z pewnością byłbym bardziej świadomy optymalnego projektu, ale nie z reguły.
akohlsmith
2
Jestem nieco zaskoczony liczbą głosów pozytywnych na tę odpowiedź. @ bit_vector @ jest z pewnością poprawnym poziomem abstrakcji do modelowania i optymalizacji mikroarchitektur, ale ogólne zalecenie dotyczy typów „wysokiego poziomu”, takich jak @ liczba całkowita @ dla sygnałów i portu, co wydaje mi się dziwne. Widziałem wystarczająco skomplikowany i nieczytelny kod z powodu braku abstrakcji, aby poznać wartość, jaką zapewniają te funkcje, i byłoby bardzo smutne, gdybym musiał je zostawić.
trondd
2
@david Doskonałe uwagi. To prawda, że ​​wciąż jesteśmy w średniowieczu w porównaniu do tworzenia oprogramowania na wiele sposobów, ale z mojego doświadczenia ze zintegrowaną syntezą Quartus i Synplify nie sądzę, że wszystko jest tak źle. Są w stanie obsłużyć wiele rzeczy, takich jak ponowne rejestrowanie rejestru i inne optymalizacje, które poprawiają wydajność przy jednoczesnym zachowaniu czytelności. Wątpię, czy większość jest ukierunkowana na kilka łańcuchów narzędzi i urządzeń, ale w twoim przypadku rozumiem wymaganie dotyczące najmniej wspólnego mianownika :-).
trondd
2

Radzę wypróbować jedno i drugie, a następnie przejrzeć raporty podsumowujące, mapę i miejsca i trasy. Te raporty pokażą dokładnie, ile LUT zużywa każde podejście, a także maksymalną prędkość, z jaką logika może działać.

Zgadzam się z Davidem Kessnerem, że jesteś na łasce swojego łańcucha narzędzi i nie ma „właściwej” odpowiedzi. Synteza to czarna magia, a najlepszym sposobem, aby dowiedzieć się, co się stało, jest uważne i dokładne przeczytanie opracowanych raportów. Narzędzia Xilinx pozwalają nawet zajrzeć do wnętrza układu FPGA, aż do zaprogramowania każdej LUT, sposobu połączenia łańcucha nośnego, sposobu, w jaki tkanina przełączająca łączy wszystkie LUT itp.

Kolejny dramatyczny przykład podejścia pana Kessnera, wyobraź sobie, że chcesz mieć wiele częstotliwości taktowania na 1/2, 1/4, 1/8, 1/16 itd. Możesz użyć liczby całkowitej, która stale zlicza każdy cykl, a następnie mają wiele komparatorów w stosunku do tej liczby całkowitej, przy czym każde wyjście komparatora tworzy inny podział zegara. W zależności od liczby komparatorów fanout może stać się nadmiernie duży i zacząć zużywać dodatkowe LUT tylko do buforowania. Podejście SLV po prostu bierze każdy pojedynczy bit wektora jako wynik.

ajs410
źródło
1

Jednym oczywistym powodem jest to, że podpisane i niepodpisane dopuszczają większe wartości niż 32-bitowa liczba całkowita. Jest to wada w konstrukcji języka VHDL, która nie jest niezbędna. Nowa wersja VHDL mogłaby to naprawić, wymagając wartości całkowitych do obsługi dowolnego rozmiaru (podobnie jak BigInt Javy).

Poza tym jestem bardzo zainteresowany usłyszeniem o testach porównawczych, które działają inaczej dla liczb całkowitych w porównaniu do wektorów.

BTW, Jan Decaluwe napisał fajny esej na ten temat: Te Ints są stworzone do Countin '

Philippe
źródło
Dzięki Philippe (chociaż nie jest to „lepszy dostęp do aplikacji do reprezentacji wewnętrznej”, o co tak naprawdę chodzi mi po…)
Martin Thompson
Ten esej jest fajny, ale całkowicie ignoruje podstawową implementację oraz wynikającą z tego szybkość i rozmiar logiki. Zgadzam się z większością tego, co mówi Decaluwe, ale nie mówi nic o wynikach syntezy. Czasami wyniki syntezy nie mają znaczenia, a czasem mają. Więc to jest wyrok sądu.
1
@David, zgadzam się, że Jan nie opisuje szczegółowo, jak narzędzia syntezy reagują na liczby całkowite. Ale nie, to nie jest wyrok sądu. Możesz zmierzyć wyniki syntezy i określić wyniki danego narzędzia do syntezy. Myślę, że OP miał na myśli jego pytanie jako wyzwanie dla nas, aby stworzyć fragmenty kodu i wyniki syntezy, które wykażą różnicę (jeśli w ogóle) w wydajności.
Philippe
@Filippe Nie, miałem na myśli, że jest to osąd, jeśli w ogóle zależy ci na wynikach syntezy. Nie same wyniki syntezy są wezwaniem do osądu.
@DavidKessner OK. Źle zrozumiałem.
Philippe