¿Cuándo es mejor usar representaciones VECTOR vs INTEGER?

11

En el hilo de comentarios sobre una respuesta a esta pregunta: Resultados incorrectos en la entidad VHDL se dijo:

"Con los enteros no tienes control o acceso a la representación lógica interna en el FPGA, mientras que SLV te permite hacer trucos como utilizar la cadena de transporte de manera eficiente"

Entonces, ¿en qué circunstancias ha sido más fácil codificar usando un vector de representación de bits que usando enteros para acceder a la representación interna? ¿Y qué ventajas midió (en términos de área de chip, frecuencia de reloj, retraso u otro)?

Martin Thompson
fuente
Creo que es algo difícil de medir, ya que aparentemente es solo una cuestión de control sobre la implementación de bajo nivel.
clabacchio

Respuestas:

5

He escrito el código sugerido por otros dos carteles en ambos vectory en integerforma, teniendo cuidado de que ambas versiones funcionen de la manera más similar posible.

Comparé los resultados en la simulación y luego los sinteticé usando Synplify Pro apuntando a Xilinx Spartan 6. Las muestras de código a continuación se pegan del código de trabajo, por lo que debería poder usarlas con su sintetizador favorito y ver si se comporta igual.


Contadores

En primer lugar, el conteo regresivo, como lo sugiere 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;

Arquitectura vectorial:

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;

Arquitectura entera

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

En cuanto al código, el número entero me parece preferible ya que evita las to_unsigned()llamadas. De lo contrario, no hay mucho para elegir.

Ejecutarlo a través de Synplify Pro top := 16#7fff_fffe#produce 66 LUT para la vectorversión y 64 LUT para la integerversión. Ambas versiones hacen mucho uso de la cadena de transporte. Ambos informan velocidades de reloj superiores a 280MHz . El sintetizador es bastante capaz de establecer un buen uso de la cadena de transporte: verifiqué visualmente con el visor RTL que se produce una lógica similar con ambos. Obviamente, un contador con comparador será más grande, pero eso sería lo mismo con los enteros y los vectores nuevamente.


Dividiendo por 2 ** n contadores

Sugerido 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;

Arquitectura vectorial

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;

Arquitectura entera

Tienes que saltar a través de algunos aros para evitar simplemente usar to_unsignedy luego extraer bits que claramente producirían el mismo efecto que el anterior:

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

En cuanto al código, en este caso, ¡la vectorversión es claramente mejor!

En términos de resultados de síntesis, para este pequeño ejemplo, la versión entera (como predijo ajs410) produce 3 LUT adicionales como parte de los comparadores, ¡yo era demasiado optimista sobre el sintetizador, aunque está trabajando con un código terriblemente ofuscado!


Otros usos

Los vectores son una clara victoria cuando desea que la aritmética se ajuste (incluso los contadores se pueden hacer como una sola línea):

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

vs

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

aunque al menos está claro a partir de ese código que el autor tenía la intención de terminar.


Algo que no he usado en código real, pero que he meditado:

La función de "envoltura natural" también se puede utilizar para "calcular a través de desbordamientos". Cuando sabe que la salida de una cadena de sumas / restas y multiplicaciones está limitada, no tiene que almacenar los bits altos de los cálculos intermedios ya que (en el complemento de 2 s) saldrá "en el lavado" para cuando llegues a la salida. Me han dicho que este documento contiene una prueba de esto, ¡pero me pareció un poco denso hacer una evaluación rápida! Teoría de la adición de computadoras y desbordamientos - HL Garner

El uso de integers en esta situación provocaría errores de simulación cuando se ajustan, aunque sabemos que al final se desenvolverán.


Y como señaló Philippe, cuando necesita un número mayor que 2 ** 31 no tiene más remedio que usar vectores.

Martin Thompson
fuente
En el segundo bloque de código que tienes variable c : unsigned(32 downto 0);... ¿no es cuna variable de 33 bits entonces?
clabacchio
@clabacchio: sí, eso permite el acceso al 'bit de acarreo' para ver la envoltura.
Martin Thompson
5

Al escribir VHDL, recomiendo usar std_logic_vector (slv) en lugar de entero (int) para SEÑALES . (Por otro lado, usar int para genéricos, algunas constantes y algunas variables pueden ser muy útiles). En pocas palabras, si declara una señal de tipo int o tiene que especificar un rango para un entero, probablemente esté haciendo algo mal.

El problema con int es que el programador VHDL no tiene idea de cuál es la representación lógica interna de int, por lo que no podemos aprovecharla. Por ejemplo, si defino un int de rango de 1 a 10, no tengo idea de cómo el compilador codifica esos valores. Esperemos que se codifique como 4 bits, pero no sabemos mucho más allá de eso. Si pudiera sondear las señales dentro del FPGA, podría estar codificado como "0001" a "1010", o codificado como "0000" a "1001". También es posible que esté codificado de una manera que no tiene absolutamente ningún sentido para nosotros los humanos.

En su lugar, deberíamos usar slv en lugar de int, porque entonces tenemos control sobre la codificación y también tenemos acceso directo a los bits individuales. Tener acceso directo es importante, como verá más adelante.

Podríamos lanzar un int a slv siempre que necesitemos acceso a los bits individuales, pero eso se vuelve muy complicado, muy rápido. Eso es como obtener lo peor de ambos mundos en lugar de lo mejor de ambos mundos. Su código será difícil de optimizar para el compilador y casi imposible de leer. No recomiendo esto

Entonces, como dije, con slv tienes control sobre las codificaciones de bits y acceso directo a los bits. Entonces, ¿qué puedes hacer con esto? Te mostraré un par de ejemplos. Digamos que necesita emitir un pulso una vez cada 4,294,000,000 relojes. Así es como haría esto 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;

Y el mismo 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;

La mayor parte de este código es idéntico entre int y slv, al menos en el sentido del tamaño y la velocidad de la lógica resultante. Por supuesto, uno está contando y el otro está contando, pero eso no es importante para este ejemplo.

La diferencia está en "la línea importante".

Con el ejemplo int, esto dará como resultado un comparador de 32 entradas. Con las LUT de 4 entradas que usa el Xilinx Spartan-3, esto requerirá 11 LUT y 3 niveles de lógica. Algunos compiladores pueden convertir esto en una resta que utilizará la cadena de transporte y abarcará el equivalente de 32 LUT, pero podría ejecutarse más rápido que 3 niveles de lógica.

Con el ejemplo de slv, no hay comparación de 32 bits, por lo que es "cero LUT, cero niveles de lógica". La única pena es que nuestro contador es un bit extra. Debido a que el tiempo adicional para este bit adicional de contador está en la cadena de transporte, hay un retraso de tiempo adicional "casi cero".

Por supuesto, este es un ejemplo extremo, ya que la mayoría de las personas no usarían un contador de 32 bits de esta manera. Se aplica a contadores más pequeños, pero la diferencia será menos dramática aunque aún significativa.

Este es solo un ejemplo de cómo utilizar slv sobre int para obtener una sincronización más rápida. Hay muchas otras formas de utilizar slv: solo se necesita un poco de imaginación.

Actualización: Se agregaron elementos para abordar los comentarios de Martin Thompson sobre el uso de int con "if (count-1) <0"

(Nota: supongo que quiso decir "if count <0", ya que eso lo haría más equivalente a mi versión slv y eliminaría la necesidad de esa resta adicional).

En algunas circunstancias, esto podría generar la implementación lógica prevista, pero no se garantiza que funcione todo el tiempo. Dependerá de su código y de cómo su compilador codifica el valor int.

Dependiendo de su compilador y de cómo especifique el rango de su int, es completamente posible que un valor int de cero no se codifique en un vector de bits de "0000 ... 0000" cuando se convierte en la lógica FPGA. Para que su variación funcione, debe codificar a "0000 ... 0000".

Por ejemplo, supongamos que define un int para tener un rango de -5 a +5. Espera que un valor de 0 se codifique en 4 bits como "0000", y +5 como "0101" y -5 como "1011". Este es el típico esquema de codificación de dos complementos.

Pero no asuma que el compilador usará dos complementos. Aunque inusual, los complementos podrían resultar en una "mejor" lógica. O bien, el compilador podría usar una especie de codificación "sesgada" donde -5 se codifica como "0000", 0 como "0101" y +5 como "1010".

Si la codificación del int es "correcta", entonces el compilador probablemente inferirá qué hacer con el bit de acarreo. Pero si es incorrecto, entonces la lógica resultante será horrible.

Es posible que usar un int de esta manera pueda dar como resultado un tamaño lógico y una velocidad razonables, pero no es una garantía. Cambiar a un compilador diferente (XST a Sinopsis, por ejemplo), o ir a una arquitectura FPGA diferente podría causar que ocurra exactamente lo incorrecto.

Unsigned / Signed vs. slv es otro debate más. Puede agradecer al comité del gobierno de los Estados Unidos por darnos tantas opciones en VHDL. :) Uso slv porque ese es el estándar para la interfaz entre módulos y núcleos. Aparte de eso, y algunos otros casos en simulaciones, no creo que haya un gran beneficio al usar slv sobre firmado / no firmado. Tampoco estoy seguro de si las señales firmadas / no compatibles son compatibles con las señales de tres estados.

Martin Thompson
fuente
44
David, esos fragmentos de código no son equivalentes. Uno cuenta desde cero hasta un número arbitrario (con un operador de comparación costoso); la otra cuenta regresiva a cero desde un número arbitrario. Puede escribir ambos algoritmos con enteros o vectores, y obtendrá malos resultados al contar hacia un número arbitrario y buenos resultados al cero. Tenga en cuenta que los ingenieros de software también realizarían una cuenta regresiva a cero si necesitan exprimir un poco más el rendimiento de un circuito dinámico.
Philippe
1
Al igual que Philippe, no estoy convencido de que sea una comparación válida. Si el ejemplo entero cuenta atrás y se usa if (count-1) < 0, creo que el sintetizador inferirá el bit de ejecución y producirá el mismo circuito que su ejemplo slv. Además, ¿no deberíamos estar usando el unsignedtipo en estos días :)
Martin Thompson
2
@DavidKessner ciertamente ha proporcionado una respuesta COMPLETA y bien razonada, tiene mi +1. Sin embargo, tengo que preguntar ... ¿por qué te preocupa la optimización en todo el diseño? ¿No sería mejor enfocar sus esfuerzos en las áreas de código que lo requieren o enfocarse en SLV para puntos de interfaz (puertos de entidad) para compatibilidad? Sé que en la mayoría de mis diseños no me importa particularmente que el uso de LUT se minimice, siempre que cumpla con el tiempo y se ajuste a la pieza. Si tengo restricciones particularmente estrictas, ciertamente sería más consciente del diseño óptimo, pero no como una regla general.
akohlsmith
2
Estoy un poco sorprendido por el número de votos positivos en esta respuesta. @ bit_vector @ es ciertamente el nivel de abstracción correcto para modelar y optimizar microarquitecturas, pero una recomendación general contra los tipos de "alto nivel" como @ integer @ para señales y puertos es algo que encuentro extraño. He visto suficiente código intrincado e ilegible debido a la falta de abstracción para conocer el valor que proporcionan estas características, y sería muy triste si tuviera que dejarlas atrás.
trondd
2
@david Excelentes comentarios. Es cierto que todavía estamos en la era medieval en comparación con el desarrollo de software de muchas maneras, pero desde mi experiencia con la síntesis integrada Quartus y Synplify, no creo que las cosas estén tan mal. Son bastante capaces de manejar muchas cosas, como el reajuste de registros y otras optimizaciones que mejoran el rendimiento y mantienen la legibilidad. Dudo que la mayoría esté dirigida a varias cadenas de herramientas y dispositivos, pero para su caso entiendo el requisito del mínimo común denominador :-).
trondd
2

Mi consejo es probar ambos y luego mirar los informes de síntesis, mapa y lugar y ruta. Estos informes le dirán exactamente cuántas LUT consume cada enfoque, también le indicarán la velocidad máxima a la que puede operar la lógica.

Estoy de acuerdo con David Kessner en que estás a merced de tu cadena de herramientas, y no hay una respuesta "correcta". La síntesis es magia negra y la mejor manera de saber qué sucedió es leer cuidadosa y minuciosamente los informes que se producen. Las herramientas Xilinx incluso le permiten ver dentro del FPGA, hasta cómo se programa cada LUT, cómo se conecta la cadena de transporte, cómo la tela del interruptor conecta todas las LUT, etc.

Para otro ejemplo dramático del enfoque del Sr. Kessner, imagine que desea tener múltiples frecuencias de reloj en 1/2, 1/4, 1/8, 1/16, etc. Podría usar un número entero que cuente constantemente cada ciclo, y luego tener múltiples comparadores contra ese valor entero, con cada salida del comparador formando una división de reloj diferente. Dependiendo de la cantidad de comparadores, el despliegue podría ser excesivamente grande y comenzar a consumir LUT adicionales solo para el almacenamiento en búfer. El enfoque SLV simplemente tomaría cada bit individual del vector como salida.

ajs410
fuente
1

Una razón obvia es que con signo y sin signo permiten valores mayores que el entero de 32 bits. Esa es una falla en el diseño del lenguaje VHDL, que no es esencial. Una nueva versión de VHDL podría solucionarlo, requiriendo valores enteros para admitir un tamaño arbitrario (similar al BigInt de Java).

Aparte de eso, estoy muy interesado en escuchar sobre los puntos de referencia que funcionan de manera diferente para los enteros en comparación con los vectores.

Por cierto, Jan Decaluwe escribió un buen ensayo sobre esto: Estas mentiras están hechas para contar

Philippe
fuente
Gracias Philippe (aunque eso no es una aplicación de "mejor acceso a la representación interna", que es lo que realmente busco ...)
Martin Thompson
Ese ensayo es bueno, pero ignora por completo la implementación subyacente y la velocidad y el tamaño de la lógica resultante. Estoy de acuerdo con la mayoría de lo que dice Decaluwe, pero él no dice nada sobre los resultados de la síntesis. Algunas veces los resultados de la síntesis no importan, y otras veces sí. Entonces es una decisión judicial.
1
@David, estoy de acuerdo en que Jan no entra en detalles sobre cómo reaccionan las herramientas de síntesis a los enteros. Pero no, no es una decisión judicial. Puede medir los resultados de síntesis y determinar los resultados de su herramienta de síntesis dada. Creo que el OP significó su pregunta como un desafío para nosotros para producir fragmentos de código y resultados de síntesis que demuestren una diferencia (si la hay) en el rendimiento.
Philippe
@Philippe No, quise decir que es un juicio decisivo si te importan los resultados de la síntesis. No es que los resultados de la síntesis en sí mismos sean una decisión judicial.
@DavidKessner OK. Entendí mal.
Philippe