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.
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 in | Default lifetime |
|---|---|
| A module, interface or program | static |
An initial or always block | static |
| A task or function in a module | static: its arguments and locals exist once |
A task automatic or function automatic | automatic |
| A class method | automatic (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.
t=0: call A starts
report(1, 5)stores 1 inidand waits 5 time units.id 1t=1: call B starts
report(2, 1)stores 2 in the sameidand waits 1 time unit.id 2t=2: call B prints
Call B prints
id=2, which happens to be right.t=5: call A wakes
Call A prints
id=2as well. Its 1 was overwritten while it waited.printed 2, 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.
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
endfunctionEither 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.
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=1Check yourself
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_nonestart after the parent blocks; copy loop values with a declaration at the top of the fork.
Practice
- Invocation-local accumulator Fix · Easy