To begin, lets understand basic concept for pass by value vs pass by reference. In verilog, method arguments takes as pass by value (this is default). The inputs are copied when the method is called and the outputs are assigned to relevant outputs when exiting the method.
In system verilog, methods can also have "pass by reference". In this case, arguments passed by reference are not copied into subroutine area instead, a reference to the original arguments are passed to subroutine. In this case subroutines can access the arguments data via reference.
This is very efficient way of passing arguments like class objects or arrays of objects. Scenario like these where you are dealing with class objects and arrays of objects, if you use pass by value it would create a consume lot of memory on the stack because it has to copy the values and then use it for subroutine. Another advantage of using pass by reference is, since the caller and the function/tasks shares the same reference, any change done inside function using reference would also be visible to the caller.
Example:
function automatic int my_crc (ref byte data [10:1]);
for (int j =1; j<=10; j++) begin
my_crc ^= data[j];
end
endfunction
In this example, you can see data is declared with "ref" meaning, each call to CRC in for loop, my_crc function does to need to create a copy of the data on stack memory. Memory would have been consumed more if you would not use "ref" and use it as pass by value (because in that case as mentioned, every time my_crc function calls, it would need to create a copy on stack memory)
Now, an obvious question!
What if user wants to make sure that the ref argument is not modified by the function?
Answer to this question is "const ref"!! We need to use const key word if you want to make sure that ref argument is not modified by the function.
For same example:
Same function "my_crc" the argument can be declared as "const ref" to make sure the original data contents are not modified accidently by my_crc function. Very very useful feature to understand in the case where you want to make sure engineers don't modified original content accidently.
function automatic int my_crc (const ref byte data [10:1]);
for (int j =1; j<=10; j++) begin
my_crc ^= data[j];
end
endfunction
Now another obvious question!
Do we need to declare each arguments as "ref" if you have more than one argument in your task/function.
Answer is "NO".
For example:
task my_task (ref int data[10], bit a, b)
In declaration like this, one needs to clearly understand each arguments to function/task can have direction which can be input, output or inout or ref. If no direction specified the default value of inputs are selected. If one argument is specifies the direction then all following arguments hold on to the same direction unless explicitly changed.
In above example, my_task, first argument is specifies "ref" direction and following variables does not explicitly specifies direction, a and b would be considered as pass by reference.
Hope this clears out some basic understanding of pass by value vs reference.
Thanks,