Automatic and static lifetime

Tell how long a variable lives, and give every concurrent call and every forked process its own copy of the values it needs.

Lesson
3.2
Time
20 min
Runs on
Verilator
Check
5 questions

A test starts one driver process per channel with for (ch = 0; ch < 4; ch++) fork drive_channel(ch); join_none. The log shows four drivers, and all four drive channel 4, which does not exist. Every process reads the same variable ch, and by the time the processes started, the loop had finished and left ch at 4.

One loop variable, four processes, all at time 0
012345ch (loop variable)01234processes forked1234processes running04channel they read-4 4 4 4parent blocks; children start

Each column is one step of the parent process, all in the same time step. The children run only after the loop has ended.

How long a variable lives

A variable has a scope, the part of the code where its name can be used, and a lifetime, how long its storage exists. A local variable of a static task is a good example of the difference. No code outside the task can name it, yet its storage lasts for the whole simulation.

A static variable is created once, before time 0, and lives for the whole simulation. Every use of its name, from any process, reaches the same storage. An automatic variable is created fresh each time its block or routine is entered, and it disappears when that call ends.

Declared inDefault lifetime
A module, interface or programstatic
An initial or always blockstatic
A task or function in a modulestatic: its arguments and locals exist once
A task automatic or function automaticautomatic
A class methodautomatic (it cannot be static)
The header of a for loop, as in for (int i = 0; ...)automatic, one variable for the whole loop

Where a variable is declared decides its lifetime unless you write static or automatic.

The third row surprises engineers who come from software. A task declared in a module is static unless you write automatic, so every call shares one set of arguments and local variables.

Two calls share one static task

Sharing is harmless while only one call is active. It breaks as soon as a task waits and a second process calls it during the wait, as with task report(input int id, input int dly) here.

  1. t=0: call A starts

    report(1, 5) stores 1 in id and waits 5 time units.

    id1
  2. t=1: call B starts

    report(2, 1) stores 2 in the same id and waits 1 time unit.

    id2
  3. t=2: call B prints

    Call B prints id=2, which happens to be right.

  4. t=5: call A wakes

    Call A prints id=2 as well. Its 1 was overwritten while it waited.

    printed2, 2

Declare the task task automatic report(...) and each call gets its own id, so call A prints 1. Any task that waits and might run in two processes at once must be automatic, and in a testbench that is nearly every task. Writing module automatic tb; makes automatic the default for every routine in the module.

The bug hides well, because the static task works whenever the calls do not overlap. A test with one active sequence passes for months. Then someone adds a second sequence that calls the same task, and the first one starts printing the second one's data.

Automatic storage is also what makes recursion work. A function that calls itself needs a fresh set of locals for each level, and a static function would overwrite them. The ref arguments from lesson 3.1 are also legal only in automatic routines.

Initializers run once for static variables

A declaration with an initializer, such as int n = 0;, sets the value when the storage is created. For an automatic variable that happens on every call. For a static variable it happens once, before time 0, and never again.

The same body, two lifetimes, two different results.
function int next_id();             // static
  int n = 0;   // set once, before time 0
  n++;
  return n;    // 1, 2, 3, ... across calls
endfunction

function automatic int next_tag();   // automatic
  int n = 0;   // set on every call
  n++;
  return n;    // 1 on every call
endfunction

Either behaviour can be what you want. To keep a running count inside an automatic routine, say so with static int n = 0;, and the counter survives between calls while everything else stays per call.

The trap is an initializer that reads a changing value. In a static task, time start = $time; records 0 at the start of the simulation, not the time of the call. Some compilers warn about an implicitly static variable with an initializer, or reject it, for this reason. Write static or automatic whenever you initialize a variable in a declaration.

Giving each forked process its own copy

The opening story combines lifetime with process start-up. A fork ... join_none inside a loop creates the processes during the loop, but they start running only after the parent blocks. The call drive_channel(ch) reads ch when a child runs it, and by then every child sees the final value.

The fix is a declaration at the top of the fork, before its statements. That declaration runs in the parent at the moment of the fork, so each child gets its own variable holding that iteration's value.

Children read the shared variable

int ch;   // static
for (ch = 0; ch < 4; ch++)
  fork
    drive_channel(ch);
  join_none

All four children call drive_channel(4).

A copy per child

int ch;   // static
for (ch = 0; ch < 4; ch++)
  fork
    automatic int k = ch;
    drive_channel(k);
  join_none

Each child gets its own k, so channels 0 to 3 are driven once each.

Placement matters. Putting automatic int k = ch; inside a begin ... end within the fork does not help, because that declaration runs in the child, after the loop has finished.

A second fix moves the fork into an automatic task, as in spawn(ch), where spawn forks drive_channel(c) on its own argument c. Each call to spawn creates a fresh c, and that variable stays alive for as long as the child that uses it is running.

Try it

The first loop sends all four drivers to channel 4, and the copy made at each fork fixes it. The static report prints id=2 twice, because call B overwrote call A's argument while call A waited. The automatic version prints 2, then 1.

Move automatic int k = ch; into a begin ... end inside the fork and predict the hits before you run it.

RUNVerilator · simulator service
Edit the code, then run it
Expected: shared ch: hits = 0 0 0 0 4 copy per fork: hits = 1 1 1 1 0 t=4 static id=2 t=7 static id=2 t=9 automatic id=2 t=12 automatic id=1

Check yourself

Predict the outputPredict the exact output. measure is a static task.
module tb;
  task measure(input int cycles);
    time start = $time;
    #cycles;
    $display("latency=%0t", $time - start);
  endtask
  initial begin
    #10 measure(3);
  end
endmodule
Spot the bugEach option replaces the body of a loop for (i = 0; i < 4; i++) begin ... end, where i is a module-level int. Every channel 0 to 3 must be driven exactly once. Which option drives channel 4 four times?
Read the waveformCalls A and B of the static task report(input int id, input int dly) overlap as shown, one column per time unit. What does call A print when it wakes at t=5?
Two overlapping calls of a static task
012345call A: report(1, 5)call B: report(2, 1)call A wakes and prints

Call A starts at t=0 and sleeps 5 units; call B starts at t=1 and returns at t=2.

ConceptTwo processes call the same routine at overlapping times. In which case do they share one copy of the local variable beat?
ConceptA helper function automatic void note_call(); must count how many times it has been called during the whole test. Which declaration inside it keeps the count?

Remember

  • Static variables exist once for the whole simulation; automatic variables are created on every call.
  • Tasks and functions in a module are static unless you write automatic; class methods are always automatic.
  • A static initializer runs once, before time 0, so never initialize a static variable from a changing value.
  • Children of fork ... join_none start after the parent blocks; copy loop values with a declaration at the top of the fork.

Practice