Short-circuit operations in VHDL

A binary operator is an operator that takes exactly two arguments. Examples of such are addition or multiplication and, of course, the familiar Boolean logical operators we use in VHDL: and, or nand nor xor and xnor.

The two operands may be simple Boolean values on an operator’s left- and right-hand sides: op_a or op_b. Or op_a and op_b may be sub-expressions that must be calculated before the main OR expression. However, if you’ve done Boolean algebra manually, you may have discovered that we sometimes don’t need to calculate both operands to know that answer.

Consider the OR operator, which takes a left- and a right-hand operand: op_a or op_b. If we calculate one of the operand sides and it evaluates to true, there’s no need to look at the other side because the result will be true regardless of the other value. That’s how the OR operator works; if either of the expressions is true, the result is true.

Short-circuit evaluation as a performance optimization

Many programming languages use this information to improve performance. Where possible, they will only evaluate the right-hand operator if the left-hand operator doesn’t have a decisive value. It’s called short-circuit evaluation.

VHDL also uses short-circuit evaluation, but only for some operations and data types.

The operators are: and, or, nand, and nor.

And it will only use short-circuiting when operating on these two data types: bit and boolean. It will not short-circuit when using the std_logic type because it’s not predefined in VHDL (it’s defined in the ieee.std_logic_1164 package).

Function side effects

In most cases, you won’t notice that short-circuit evaluation is taking place. And that’s a good thing! It’s speeding up the simulator without causing any issues.

The problems only surface if the right-hand operator is a function that changes a variable or signal. And when that happens, it can lead to strange bugs that will leave you scratching your head unless you are aware of short-circuiting VHDL operators.

We say that a function has side effects if it can access objects not coming through the parameter list. If we declare a function in the declarative region of a process, it can read or write to any variable or signal that the process can access, but only if we declare it as an impure function.

As soon as we write the VHDL keyword impure before the function declaration, we can manipulate objects not coming through the function’s parameter list. That’s opposed to a regular pure function, which is guaranteed to return the same value whenever you call it with a given set of arguments.

Using boolean type

To demonstrate short-circuit evaluation in action, I’ve created the example process below that we will try out. It has an impure function that updates a value as a side effect (line 10). The function also prints a line of text every time it’s called that we can observe in the simulator’s transcript.

Finally, we call the function twice in the process. First, with a left-hand argument that won’t cause the right-hand argument not to run (line 20), and then with a left-hand argument is_even(2) (line 24) that evaluates to true, meaning that the simulator can short-circuit the operation and omit running the right-hand argument is_even(3).

TEST_PROC : process

  variable check_count : integer := 0;

  impure function is_even(number : integer) return boolean is
    variable r : boolean;
  begin
    r := number rem 2 = 0;

    check_count := check_count + 1;
    report "Check #" & integer'image(check_count) &
      " is_even(" & integer'image(number) & ") = " & boolean'image(r);
    
    return r;
  end function;

begin

  report "Test_A = " & boolean'image( 
    is_even(1) or is_even(3)
  );

  report "Test_B = " & boolean'image( 
    is_even(2) or is_even(3)
  );

  wait;
end process;

As we can see from the printout below, the function was only called three times. The last right-hand argument never got evaluated due to short-circuiting of the OR operator.

# ** Note: Check #1 is_even(1) = false
#    Time: 0 ns  Iteration: 0  Instance: /test_tb
# ** Note: Check #2 is_even(3) = false
#    Time: 0 ns  Iteration: 0  Instance: /test_tb
# ** Note: Test_A = false
#    Time: 0 ns  Iteration: 0  Instance: /test_tb
# ** Note: Check #3 is_even(2) = true
#    Time: 0 ns  Iteration: 0  Instance: /test_tb
# ** Note: Test_B = true
#    Time: 0 ns  Iteration: 0  Instance: /test_tb

Using bit type

As mentioned, VHDL’s short-circuit operations only apply to boolean and bit types. For completeness, I’ve created an example that uses bit instead of boolean. The highlighted lines in the code below are the ones I have modified.

TEST_PROC : process

  variable check_count : integer := 0;

  impure function is_even(number : integer) return bit is
    variable r : boolean;
  begin
    r := number rem 2 = 0;

    check_count := check_count + 1;
    report "Check #" & integer'image(check_count) &
      " is_even(" & integer'image(number) & ") = " & boolean'image(r);
    
    if r then return '1'; else return '0'; end if;
  end function;

begin

  report "Test_A = " & bit'image( 
    is_even(1) or is_even(3)
  );

  report "Test_B = " & bit'image( 
    is_even(2) or is_even(3)
  );

  wait;
end process;

The printout below shows that the last and redundant right-hand argument is never evaluated, as in the previous example.

# ** Note: Check #1 is_even(1) = false
#    Time: 0 ns  Iteration: 0  Instance: /test_tb
# ** Note: Check #2 is_even(3) = false
#    Time: 0 ns  Iteration: 0  Instance: /test_tb
# ** Note: Test_A = '0'
#    Time: 0 ns  Iteration: 0  Instance: /test_tb
# ** Note: Check #3 is_even(2) = true
#    Time: 0 ns  Iteration: 0  Instance: /test_tb
# ** Note: Test_B = '1'
#    Time: 0 ns  Iteration: 0  Instance: /test_tb

Using std_logic type

Now let’s try changing the previous example to use std_logic instead of bit types. VHDL doesn’t support short-circuit operations for std_logic because it’s not a predefined type.I have highlighted the changed lines in the code below.

TEST_PROC : process

  variable check_count : integer := 0;

  impure function is_even(number : integer) return std_logic is
    variable r : boolean;
  begin
    r := number rem 2 = 0;

    check_count := check_count + 1;
    report "Check #" & integer'image(check_count) &
      " is_even(" & integer'image(number) & ") = " & boolean'image(r);
    
    if r then return '1'; else return '0'; end if;
  end function;

begin

  report "Test_A = " & std_logic'image( 
    is_even(1) or is_even(3)
  );

  report "Test_B = " & std_logic'image( 
    is_even(2) or is_even(3)
  );

  wait;
end process;

The printout below shows an additional line we haven’t seen in any previous runs: Check #4 is_even(3) = false. It’s evident that the right-hand argument of the second OR statement got evaluated. The is_even function was called four times because there was no short-circuiting of the OR statement.

# ** Note: Check #1 is_even(1) = false
#    Time: 0 ns  Iteration: 0  Instance: /test_tb
# ** Note: Check #2 is_even(3) = false
#    Time: 0 ns  Iteration: 0  Instance: /test_tb
# ** Note: Test_A = '0'
#    Time: 0 ns  Iteration: 0  Instance: /test_tb
# ** Note: Check #3 is_even(2) = true
#    Time: 0 ns  Iteration: 0  Instance: /test_tb
# ** Note: Check #4 is_even(3) = false
#    Time: 0 ns  Iteration: 0  Instance: /test_tb
# ** Note: Test_B = '1'
#    Time: 0 ns  Iteration: 0  Instance: /test_tb

How about synthesis?

Now that you know how short-circuit evaluation works in VHDL, you may wonder if it has any implications for synthesis. The answer is: no. It’s only a simulation optimization.

However, the synthesis tool will optimize away any redundant logic it discovers in your design, regardless of short-circuit evaluation rules.

Consider the example below where the rightmost AND operator relies on the result from a sub-expression which is a tautology (always true). The simulator would never have looked at the right-hand argument with short-circuit evaluation. But what will the synthesis tool do?

entity operation is
  port (
    a, b, c : in bit;
    y : out bit
  );
end operation;

architecture rtl of operation is
begin

  -- ((b and c) or (b nand c)) = always true
  y <= ((b and c) or (b nand c)) and a;

end architecture;

After elaboration, we get the circuit we described, also containing the redundant logic that always evaluates to true:

Elaborated netlist

But after synthesis, which is where optimization takes place, we see that the tool has correctly identified and removed the tautology. The simplified expression is simply y <= a:

Synthesized netlist

Optimization saved resources, but it wasn't due to short-circuit evaluation. In fact, we will get the same result if we swap the operands and even if we use std_logic or any other similar data type:

library ieee;
use ieee.std_logic_1164.all;

entity operation is
  port (
    a, b, c : in std_logic;
    y : out std_logic
  );
end operation;

architecture rtl of operation is
begin

  -- ((b and c) or (b nand c)) = always true
  y <= a and ((b and c) or (b nand c));

end architecture;

Final thoughts

Short-circuit evaluation isn't something I think about when coding VHDL. It was more so while I was working as a C++ programmer. Then I would always place the most complex part of the expression on the right-hand side to benefit from any potential short-circuiting.

When coding VHDL, my focus is on writing correct and readable code. And since this optimization only applies to simulations using boolean and bit types, I don't give it much thought.

However, it's good to know about it if you stumble on one of those strange bugs caused by the right-hand operand not calling a function because of short-circuit evaluation.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *