I am trying to understand the behavior of ref
and const ref
applied to the members of a composite type (e.g. record). Here, is it OK to assume that the behavior of them is similar to auto&
and const auto&
in C++? I.e., no copy will be created even for the const ref
case? I wonder if there might be cases where the compiler can possibly create a temporary variable for const ref
as a result of optimization (though I guess it will not happen).
To get some experience, I have tried the following code and confirmed that the addresses are the same for this simple case.
use CTypes;
record Foo {
var n = 100;
}
var foo: Foo;
writeln("foo.n : addr = ", c_ptrToConst(foo.n));
ref n_ref = foo.n;
writeln("n_ref : addr = ", c_ptrToConst(n_ref));
const ref n_cref = foo.n;
writeln("n_cref : addr = ", c_ptrToConst(n_cref));
(Result)
foo.n : addr = 0x654816445110
n_ref : addr = 0x654816445110
n_cref : addr = 0x654816445110
Also, to check the address of data, is it reasonable to use c_ptrToConst()
in the CTypes module to make a code similar to the following one in C++?
#include <iostream>
using namespace std;
struct Foo {
int n = 100;
};
int main() {
Foo foo;
cout << "foo.n : addr = " << &foo.n << endl;
auto& n_ref = foo.n;
cout << "n_ref : addr = " << &n_ref << endl;
const auto& n_cref = foo.n;
cout << "n_cref : addr = " << &n_cref << endl;
}
A related question is: given that ref
and const ref
makes no copy, is there any performance difference between using an array component directly (say foo.arr
) or using a ref
variable to it (ref arr = foo.arr
) for performing the same array calculation? (Here, foo
is supposed to have an array component arr
.)
It sounds to me like you've got the right idea. I think of ref
and const ref
in Chapel as being like a C pointer, but one that doesn't require a *
to dereference or an &
to establish. Initializing it points it to something (and it can't be re-pointed to something else later), and subsequent references to it are like dereferences of that pointer.
Since Chapel supports distributed memory programming and a global namespace, Chapel references can refer to data stored in remote memories (e.g., on remote compute nodes) as well variables stored locally, and this makes them quite a bit more powerful than C/C++.
Note that while your examples are using Chapel's type inference, which makes them similar to the auto
cases you mention in C++, ref
declarations in Chapel can be typed. For example:
var x = 42;
ref y: int = x;
Here, Chapel is inferring x
to be an int
since it is initialized with an int
literal. While y
can similarly be inferred from x's type, here I'm asserting that it's a ref
to an int
. While this is obvious in a small case like this, such type annotations can be helpful as documentation or an assertion for code safety in cases where the initializing expression is more complex.
I am reasonably certain that, today, the Chapel compiler will not create deep copies of variables being referred to by a const ref
and would be surprised if we were to change this in the future. Specifically, doing so would seem to contradict what the user explicitly asked for. If Chapel were to do so in the future, I expect it would only be in cases where it served as an optimization where there'd be no way for a user to determine that we had done so (like your use of c_ptrToConst()
.
To your other questions:
For a record, using c_ptrTo()
/c_ptrToConst()
or c_addrOf()
/c_addrOfConst()
are reasonable and equivalent ways to get a C pointer/const pointer to a Chapel variable. As you may know, for other types, the two can vary. For example, for a local, contiguous array, c_ptrTo()
will return a pointer to the first element in the array's buffer while c_addrOf()
will return a pointer to the array's metadata.
For a ref
to a record field, I generally wouldn't expect there to be much peformance difference between accessing the field using foo.field
vs. refToField
regardless of the field's type. The one case where I could imagine a potential difference would be if the record were statically allocated such that the compiler knew the exact address of foo.field
but had to dereference the logical pointer of refToField
to get to it. But in practice, I'd expect those cases to not come up often and for the difference to be negligible. Specifically, I often use ref
or const ref
as a way to create a shorthand for a more complex expression.
Where refs can be quite
valuable is when an expression's location can be expensive to
compute. For example, a complex array expression like A[i, j][k]
will
tend to involve address arithmetic and pointer dereferences, such that storing a
reference to the element can be valuable if it's going to be used
multiple times in a section of code. Similarly, a ref
can be useful
when accessing a class through multiple other class fields (e.g., root.left.right.right
in a binary tree) and needing to access that
field several times. In both cases, the ref
would give you both a
shorthand for the expression and a cheaper way to point directly at its
location in memory.