1. What is pass-by-reference?
From section 13.5.2 in IEEE Std 1800-2012:
Arguments passed by reference are not copied into the subroutine area, rather, a reference to the original argument is passed to the subroutine. The subroutine can then access the argument data via the reference.
Pass-by-Reference是与Pass-by-Value相对的。
Pass-by-Value是指在function或者task(统称为子进程)调用时,将变量的值复制到子进程的本地空间。在子进程内部对该变量的值的修改不会影响到该子进程以外的变量,反之亦然。与之相对,Pass-by-Reference是指将变量的reference(相当于matlab中的handle,C/C++中的pointer)传入子进程内部,这样在子进程内部对该变量的值的修改会同步反映到该子进程以外的共享相同reference的变量。
在SystemVerilog语法中,pass-by-reference的子进程参量用关键词修饰ref。如以下例子所示,其中din_i/q被定义为pass-by-reference:
function automatic void DataRead(
input int num_data,
input string InputFileName,
ref logic [7:0] din_i[128],
ref logic [7:0] din_q[128]
);
顺便提一下,在C++中定义一个pass-by-reference的参量是使用&修饰符,如以下例子所示:
// A C++ function with pass-by-reference example
// function definition to swap the values.
void swap(int& x, int& y) {
int temp;
temp = x; /* save the value at address x */
x = y; /* put y into x */
y = temp; /* put x into y */
return;
}
在数组类参量的处理方面,SV与C++是有区别的。在C++中对于数组类参量,缺省地就是pass-by-reference,所以对于数组类参量不需要额外地加&修饰符。但是在SV中,数组是缺省地按pass-by-value的方式传递的,如果需要按照pass-by-reference的方式传递的话,则需要显式地追加ref修饰词。
以下例子用于对比pass-by-reference和pass-by-value的区别。
module pass_by_ref_vs_value;
initial begin
int a, b;
a = 2;
b = 8;
$display("@%0t: Before calling task_pass_by_value(): a = %0d, b = %0d",$time,a,b);
task_pass_by_value(a, b);
$display("@%0t: After calling task_pass_by_value(): a = %0d, b = %0d",$time,a,b);
#1000;
$display("@%0t: Before calling task_pass_by_ref(): a = %0d, b = %0d",$time,a,b);
task_pass_by_ref(a, b);
$display("@%0t: After calling task_pass_by_ref(): a = %0d, b = %0d",$time,a,b);
end
task task_pass_by_value(int a, int b);
a = a + b;
$display("@%0t: task_pass_by_value(): a = %0d, b = %0d",$time,a,b);
endtask
task automatic task_pass_by_ref(ref int a, int b); // automatic!
a = a + b;
$display("@%0t: task_pass_by_ref(): a = %0d, b = %0d",$time,a,b);
endtask
endmodule
运行以后得到以下结果:
@0: Before calling task_pass_by_value(): a = 2, b = 8
@0: task_pass_by_value(): a = 10, b = 8
@0: After calling task_pass_by_value(): a = 2, b = 8
@100000: Before calling task_pass_by_ref(): a = 2, b = 8
@100000: task_pass_by_ref(): a = 10, b = 8
@100000: After calling task_pass_by_ref(): a = 10, b = 8
可以看出,虽然两个task内部对修改了a的值,但是task_pass_by_value()内部的修改并没有影响主进程中的a值,而task_pass_by_ref()内部的修改则改变了主进程中的a值。
2. Why to use pass-by-reference?
使用pass-by-reference有什么好处,或者说有什么不得不用的理由?
对于数组类型的参数,出于性能的考虑,应该尽量采用pass-by-reference的方式传递。如果使用pass-by-value方式的话,每次子进程调用都需要将整个数组复制一份传入子进程的本地空间,对于很大的数组的话,这样是非常浪费计算资源的。
但是采用pass-by-reference的方式传递的话,子进程内部对数组中数据的修改是会直接影响到子进程外面的(反过来子进程外面对数据的修改也同样会影响到子进程内部的)。那有时候不希望子进程内部对数组数据进行篡改,即以read-only的方式使用数据的话,应该怎么办呢?答案如下:
- 如果希望子进程修改该数组中的数据并传递到外面的话,则使用ref修饰。如上面的DataRead就是从数据文件将数据读取后从din_i/q传递到外面使用的。
- 如果不希望子进程修改该数组中的数据,而只是使用数组中的数据的话,则可以使用const ref修饰
以下为一个以pass-by-reference传递数组参数的例子。
module pass_by_ref_for_array;
initial begin
int j;
byte data [] ;
data = new[5];
data = '{3,4,5,6,7};
$display("@%0t :: Before calling pass_by_ref_for_array data[%0d] = %0d",$time,j,data[j]);
pass_by_ref_for_array(data);
$display("@%0t :: After calling pass_by_ref_for_array data[%0d] = %0d",$time,j,data[j]);
//pass_by_const_ref_for_array(data);
end
task automatic pass_by_ref_for_array( ref byte data []);
for(int j = 0; j<data.size(); j++) begin
$display("@%0t :: inside the task data[%0d] = %0d",$time, j,data[j]);
data[0] = 2;
$display("@%0t :: inside the task data[%0d] = %0d",$time, j,data[j]);
end
endtask
//task automatic pass_by_const_ref_for_array( const ref byte data []);
// for(int j = 0; j<data.size(); j++) begin
// $display("@%0t :: inside the task data[%0d] = %0d",$time, j,data[j]);
// data[0] = 2;
// $display("@%0t :: inside the task data[%0d] = %0d",$time, j,data[j]);
// end
//endtask
endmodule
运行结果如下:
@0 :: Before calling pass_by_ref_for_array data[0] = 3
@0 :: inside the task data[0] = 3
@0 :: inside the task data[0] = 2
@0 :: inside the task data[1] = 4
@0 :: inside the task data[1] = 4
@0 :: inside the task data[2] = 5
@0 :: inside the task data[2] = 5
@0 :: inside the task data[3] = 6
@0 :: inside the task data[3] = 6
@0 :: inside the task data[4] = 7
@0 :: inside the task data[4] = 7
@0 :: After calling pass_by_ref_for_array data[0] = 2
以上代码例中还有一个将数组参数声明为const ref类型的task,该task内部试图修改该数组中某个元素的值。将注释符去掉后,(甚至主进程中并没有调用该task),重新运行会发生什么呢?请读者自行试一试。
简而言之,使用pass-by-reference的一个重要理由就是,在需要传递大的数组参数的时候,pass-by-reference的方式更加高效。使用pass-by-reference还在另外一个重要的理由,参见下一节的说明。
3. Difference between ref and inout
一般来说,task和function的参数可以被声明为以下几种情况:
- Input. 此时,参数在子进程被调用时是被复制一份传入,即所谓的pass-by-value
- Output. 用于从子进程调用返回时将子进程内部的处理结果传递给调用方
- Inout. 兼具input和output的功能,既用于将参数传入子进程,同时又用于将子进程的处理结果在子进程退出时返回给调用方
- Ref. 如前所述,参数以pass-by-reference的方式传递到子进程内部,同时子进程内部对该参数值的修改也会反映到子进程的外部空间。但是针对pass-by-reference的参数传递,有更严格的数据类型兼容性检查
如上所述,可以看出来,除了pass-by-reference vs pass-by-value的区别以外,ref和inout都有双向传递数据的功能,那它们之间有什么区别呢?确实有,这也是在有些场景下必须使用ref的重要理由。
Inout类型的子进程接口参量只在子进程退出时才将子进程的处理结果返回给调用方。而对于ref类型的参量,由于子进程内外的变量是共享同一个reference,所以该变量的值是实时同步地反映在子进程内部和外部的!对于没有时间流逝概念的function来说,ref和inout的这一差异无关紧要,但是对于有时间流逝的task来说,如果希望比如说task内部对于某个变量的处理结果能够(在task处理过过程中)实时地传递给task外部,则只能使用ref类型接口才能实现。
以下是一个这样的例子:
module ref_vs_inout;
logic var1,var2;
task automatic mytask(inout logic arg1, ref logic arg2);
#0 $display("%m @%0t arg1 = %b arg2 = %b",$time,arg1,arg2);
// actual arguments have been set to 0
#5 $display("%m @%0t arg1 = %b arg2 = %b",$time,arg1,arg2);
#0 arg1 = 1; arg2 = 1;
#5 $display("%m @%0t arg1 = %b arg2 = %b",$time,arg1,arg2);
endtask
initial #1 mytask(var1,var2);
initial begin
var1 = 'z; var2 ='z;
#2 var1 = 0; var2 = 0; // after call
// arguments have been set to 1
#5 $display("%m @%0t var1 = %b var2 = %b",$time,var1 ,var2);
#5 $display("%m @%0t var1 = %b var2 = %b",$time,var1 ,var2);
end
endmodule
运行这个例子可以得到以下结果,可以清楚地看出inout类型的arg1和ref类型的arg2在数据传递中的不同之处。
ref_vs_inout.mytask @100 arg1 = z arg2 = z
ref_vs_inout.mytask @600 arg1 = z arg2 = 0
ref_vs_inout @700 var1 = 0 var2 = 1
ref_vs_inout.mytask @1100 arg1 = 1 arg2 = 1
ref_vs_inout @1200 var1 = 1 var2 = 1
在仿真开始时,在主进程中,var1和var2被初始化为'z'。因此mytask中在1ns(注意不是0ns,看出为什么来了吗?)描述打印出来的arg1和arg2的值为‘z’;
在2ns时,主进程中将var1和var2值修改为0. mytask的arg1是inout类型,因此外部的修改不会影响到mytask内部,而arg2是ref类型,var2值的修改实时地反映到mytask内部了。因此在5ns处,mytask打印出来的arg1仍然为'z',而arg2的值则是0了。
然后在6ns时mytask内部将arg1和arg2更新为1,同理,input类型的arg1的修改不会实时地影响到主进程中的var1, 而ref类型的arg2的修改则实时地影响到主进程中的var2, 因此主进程中在7ns处打印出来的var1仍然为0,而var2则已经更新为1了。
以下依此类推,不再赘述。