Automatic variables in fork

hi,
I am having questions about how automatic variables work in fork statement in for loop. Referring to the code below, the simulator (VCS) is displaying 0,1,2 for program test (case1) however, for all other programs (test1-test3), it is displaying 3,3,3 though variable k is automatic in all cases. Can someone help me and explain this behaviour?

module automatic test;
initial begin
for (int j = 0; j < 3; j++)
fork //begin
automatic int k = j;
// k = j;
$write (k);
//end
join_none
#0 $display ("\n test");
end
endmodule

module automatic test1;
initial begin
for (int j = 0; j < 3; j++)
fork begin
automatic int k = j;
// k = j;
$write (k);
end
join_none
#0 $display ("\n test1");
end
endmodule
module automatic test2;
initial begin
for (int j = 0; j < 3; j++)
fork begin
automatic int k = j;
k = j;
$write (k);
end
join_none
#0 $display ("\n test2");
end
endmodule

module automatic test3;
initial begin
for (int j = 0; j < 3; j++)
fork //begin
automatic int k = j;
k = j;
$write (k);
//end
join_none
#0 $display ("\n test3");
end
endmodule

The thing to know here is that automatic variables have a lifetime based on an activation scope. They are created and initialized when entering that scope, and disappear when leaving that scope. The key thing to know is the a scope remains active until reaching the procedural end of that scope, or after all child process have completed, whichever comes last.

Cases 2 and 3 are essentially the same. The activation scope for k is the begin/end block inside the fork/join_none. There are actually 3 activations of k, one for each process forked off. None of those processes activate until after the parent initial process reaches the #0 delay, which is after the for-loop has reached its final value j==3. The only difference with case 3 is you initialize each of the k’s to 3 as in case 2, but then the first statement of the process is to assign it to 3 again. The variable k disappears when each process terminates.

In cases 1 and 4, the activation scope for k is entering the fork/join_none. As with cases 2 and 3, there will be 3 activations of k, but this time the activation starts immediately with each iteration of the for-loop. So k gets initialized with the current value of j; 0, 1, and 2. The lifetime of each k continues until the process spawn by the fork/join_none terminates.

The problem with case 4 is there are two processes spawned by each activation of the fork/join_none: the assignment k = j, and the $write(k). The assignment to each k will take the final value of j==3 since the process does not start until the initial process reaches the #0 delay. But now you have a race condition with the process with the $write(k) statement. If the write executes first, you will see the same results as case 1: the values 1, 2, and 3. But if the assignment statement executes first, k gets the final value of j==3, and you get the same results as case 3.

In reply to dave_59:

Hi Dave,
thanks a lot for the explanation. It is clear now. I have a related question:
If I move the automatic variable declaration and initialization outside fork statement, as given below:

case 5:

program automatic test4;
  initial begin
    for (int j = 0; j < 3; j++) begin
      automatic int k = j; 
      fork //begin
       
        // k = j;
 
        $write (k);
      //end
      join_none
    end
    #0 $display ("\n test");
  end
 
endprogram

Then the result it displays is 2 2 2 (since the final value of k after the for loop completes will be 2, which will be used by $write (k)). However, in the Chris Spear book (system verilog for verification, second edition) on page 226/227, it says case 1 and case 5 should give same result. This is confusing. Can you please comment on this.

regards,
-sunil

In reply to puranik.sunil@tcs.com:

BTW, please use code tags to format your code. I have done that for you in your post.

Yes, case 1 and 5 should work the same. The only difference would be visibility of the variable k after the fork statement if there were other statements. Your code works fine for me in ModelSim/Questa. It prints 2 1 0. (any order would be correct).

Also, I do not recommend the use of program blocks. Just use a module.

Dave

In reply to dave_59:

Hi Dave,

I have run this code (test4)on the VCS simulator and but it prints 2, 2, 2.
Now based on the explanation you have given earlier, the activation scope for k (in program test4) is the begin…end block of for loop. So k will get values 0,1,2 and these values will be used by $write(k) and it should print 0,1,2. This is unlike cases 2 and 3 above where k is activated in begin end block inside fork, which gets scheduled after #0 delay and by that time j would have reached 3. So it uses j ==3 for all 3 processes and prints 3,3,3 (let me know if this understanding is correct).
I am not clear why VCS is giving different result. I have tried replacing program with module statement, but it makes no difference.
Also you have said “The only difference would be visibility of the variable k after the fork statement if there were other statements.” in your comment. Can you please elaborate on this.

thanks
-sunil

In reply to puranik.sunil@tcs.com:

Your understanding is correct, but your tool’s understanding is not. Please contact your vendor for tool specific issues.

In reply to dave_59:

HI Dave,
I have one more question on automatic variables:
System verilog LRM 3.1 says “In a data_declaration that is not within the procedural context, it shall be illegal to use the automatic keyword”. Does it mean that automatic variables can be declared only in procedural blocks and any variable not within procedural block is static by default?
I understand procedural blocks are - initial, always,final, begin…end, fork…join, always_comb, always_latch, always_ff, task, function etc.
Are there any more of them?
rgs,
-sunil

In reply to puranik.sunil@tcs.com:

Please get a copy of the latest SystemVerilog 1800-2012 LRM

Yes, automatic variables can only be declared in a procedural block - those are the only blocks that have a potential lifetime shorter than the entire simulation.

I think etc. covers all of them. :)

So, I have a related question, primarily out of curiosity as I solved my issue. I had code similar to case 1 above that worked fine in a test class; however, the comp errored out in a task declared in the base test class - didn’t like the automatic int declaration. HOWEVER, I then realized that the fork in my test class was named so I added a name to the base test class task version. Bingo, compiles - we’ll see if it runs. Why does naming the fork fix the compile problem?

In reply to mchal9e3:

Without seeing the code or the specific error message it is difficult to comment on what the problem might be.

Well, this is a never mind, I went back to reproduce the error and FAILED. Must have been an env or moldy/corrupt build issue. This is a good thread to jog my memory about this issue tho, it’s pretty easy to get bitten by it. In fact, I got the original attempt to work in my test because I ‘forgot’ to put a begin after the fork as I had intended.

In reply to dave_59:

When you say “None of those processes activate until after the parent initial process reaches the #0 delay” for case 2 and 3, what is the reason for that? Does not all forked processes execute at zero time and the #0 delay line run at in the end?

In reply to Aashish Itani:

Because the LRM says so. To keep things more deterministic, section 9.3.2 Parallel blocks says the processes do not start until the parent process suspends or terminates.

In reply to dave_59:

Hi Dave, with regard to fork join_none, I have gone through some threads asked in the forum but I still don’t understand why in this code j = 0 is not getting printed. Would you please help me understand this?


module top;
	initial begin
		for (int j=0; j<3; j++) begin
			#1;
			fork
				$display($time, "  j = %0d",j);
			join_none
		end
	end
endmodule

Result:


                   1  j = 1
                   2  j = 2
                   3  j = 3

In reply to vickydhudashia:

This happens because the process inside the fork/join_none (the $display) does not start until the parent process suspends or terminates.

The first process does not start until the second iteration suspends at the #1.
The second process does not start until the third iteration suspends at the #1.
The third process does not start until exiting the loop and reaching the end of the initial.

In all three cases j has already advanced by +1. This is why we always recommend creating a separate automatic variable that has the loop iterator value at the point when the process gets scheduled, not when it starts executing.