Pass by immutable reference

455 views
Skip to first unread message

Steven

unread,
Feb 21, 2010, 2:13:30 PM2/21/10
to golang-nuts
In Go, with the ease of use and safety of pointers, there is no need for references (such as in C++). However, there is not concept of an immutable reference, which is useful when you want to pass something along without copying it, but read only.

However, in Go, there isn't really a need for all the semantics surrounding mutator vs accessors and pass by immutable reference. There's already a way of (sort of) doing this. ie. a method with a value receiver is an accessor (func (t T) f(){}). Anywhere you pass a value and not a pointer, you're saying "here, you can look at it, but don't mess with it". However, as far as I'm aware, the compiler doesn't reflect this, as it always makes a copy (correct me if I'm wrong).

What is the potential for the compiler to determine, when a value is passed, whether it should be passed by value or by immutable reference? Such as, given a type of sufficient size, if the value is not altered, and doesn't have its address taken (including implicitly in calling pointer methods on it) then it is passed by reference.

This would also add some extra purpose to methods with value receivers, as they could be called without causing a copy to be made.

Are there any complications I'm not seeing? If not, is there already an intention for this behaviour? I can see some shortcomings: it wouldn't work with slices or maps and interfaces might present a problem. Are immutable references of any kind even seen as desirable behaviour for Go?

~Steven

Carlisle Perera

unread,
Feb 21, 2010, 3:16:14 PM2/21/10
to golang-nuts
On Feb 21, 8:13 pm, Steven <steven...@gmail.com> wrote:
> Are immutable references of any kind even seen as desirable behaviour for
> Go?

Definitely! At least by me. Besides generics I miss immutable
references the most (or a guarantee that the compiler optimizes away
needless copying the way you describe it).

-CP

Ryanne Dolan

unread,
Feb 21, 2010, 3:24:59 PM2/21/10
to Steven, golang-nuts

I think the reasoning is as follows:

If a reference shouldn't be used to manipulate an object... don't use it to manipulate an object.  You only need to follow your own rules within your own packages, since "private" data can be protected from other packages using interfaces and non-exported variables/fields.

If an object is too big to be copied efficiently... don't copy it. Most of the time receivers and parameters should be passed by pointer, unless they are small (near the size of pointers).

Ryanne
- from my phone -

chris dollin

unread,
Feb 21, 2010, 3:37:22 PM2/21/10
to Carlisle Perera, golang-nuts

Presumably if you add immutable reference types to the language,
let's say @T instead of *T, you allow a value of *T to be assigned
to a variable (ditto passed as a parameter) of type @T?

Do you let a value of type func (*T)W to be assigned to a variable
of type func (@T)W? How about assigning a func()(*T) to a
variable of type func()@W?

Does a func (X) Spoo (@T)W implement an interface method
Spoo(*T)W?

Is an []*T assignable to a []@T?

I'm not saying that you can't work out a clear, consistent set of
rules for immutable references. I am saying that the cost of adding
them may be more than expected.

--
Chris "allusive" Dollin

roger peppe

unread,
Feb 21, 2010, 4:39:31 PM2/21/10
to Ryanne Dolan, Steven, golang-nuts
On 21 February 2010 20:24, Ryanne Dolan <ryann...@gmail.com> wrote:
> If an object is too big to be copied efficiently... don't copy it. Most of
> the time receivers and parameters should be passed by pointer, unless they
> are small (near the size of pointers).

you might be surprised by the size of the overhead of copying
larger structures by value - it's lower than you might think.

i did a few tests, timing loops of this form:

func benchn(t *testing.B) {
var x Fn
for i := t.N; i > 0; i-- {
foon(x)
foon(x)
foon(x)
foon(x)
}
}

where Fn and foon are defined as follows (with the constant explicitly
written out):

type Fn struct {
x int[2 ** n]
}

func foon(x Fn) { }

here are my results (note that numbers are per-call,
not per iteration, to account for my loop unrolling):

bench0 100000000 5 ns/op
bench1 100000000 6.75 ns/op
bench2 50000000 16.5 ns/op
bench3 50000000 16.25 ns/op
bench4 20000000 19.25 ns/op
bench5 20000000 26 ns/op
bench6 10000000 40.25 ns/op

note that there's almost no increase in
call overhead between 4 and 32 ints in the struct.
in fact 4 and 8 are almost identical - i wonder why that is.

this is on an intel macbook.

NoiseEHC

unread,
Feb 21, 2010, 4:54:06 PM2/21/10
to roger peppe, Ryanne Dolan, Steven, golang-nuts
Because a modern Intel processor has multiple load/store units so passing something in the L1 cache consumes almost zero time. (But we all know that a lot of zeros add up to some measurable penalty...) The more important feature would be to define (or just implement in the compiler) some "sync" closure passing/calling where the function would not be scheduled bu the Go scheduler but would be called (or inlined) directly.

Esko Luontola

unread,
Feb 21, 2010, 5:12:05 PM2/21/10
to golang-nuts
On Feb 21, 11:39 pm, roger peppe <rogpe...@gmail.com> wrote:
> note that there's almost no increase in
> call overhead between 4 and 32 ints in the struct.
> in fact 4 and 8 are almost identical - i wonder why that is.

I'm guessing that the performance difference (or more like, the lack
of performance difference) has to do with cache misses. There is an
informative presentation at http://www.infoq.com/presentations/click-crash-course-modern-hardware
about what happens inside modern CPUs. During the time that one word
that is not cached is retrieved from memory, the CPU will execute
hundreds of instructions. So even if it takes many instructions to
copy a large structure by value, if the value is cached, there is no
significant performance difference.

Steven

unread,
Feb 21, 2010, 5:26:39 PM2/21/10
to Esko Luontola, golang-nuts
On Sun, Feb 21, 2010 at 3:24 PM, Ryanne Dolan <ryann...@gmail.com> wrote:

I think the reasoning is as follows:

If a reference shouldn't be used to manipulate an object... don't use it to manipulate an object.  You only need to follow your own rules within your own packages, since "private" data can be protected from other packages using interfaces and non-exported variables/fields.

If an object is too big to be copied efficiently... don't copy it. Most of the time receivers and parameters should be passed by pointer, unless they are small (near the size of pointers).

Ryanne


- from my phone -

Yes, I can see this. This approach works if there is a clear line between in-package and out-package manipulation of the data. The complexity arises when you have a large data type and there are different circumstances in which you want to allow different levels of access. You could define multiple unexported convertible types and put their pointers behind interfaces, but this can be quite complex when a simple guarantee that a pass-by-value would be optimized would suffice. I was just throwing this out there.


On Sun, Feb 21, 2010 at 3:37 PM, chris dollin <ehog....@googlemail.com> wrote:
Presumably if you add immutable reference types to the language,
let's say @T instead of *T, you allow a value of *T to be assigned
to a variable (ditto passed as a parameter) of type @T?

Do you let a value of type func (*T)W to be assigned to a variable
of type func (@T)W? How about assigning a func()(*T) to a
variable of type func()@W?

Does a func (X) Spoo (@T)W implement an interface method 
Spoo(*T)W?

Is an []*T assignable to a []@T?

I'm not saying that you can't work out a clear, consistent set of
rules for immutable references. I am saying that the cost of adding
them may be more than expected.

-- 
Chris "allusive" Dollin

I was thinking more in terms of smart optimization, whatever that would be (perhaps different from my example of dubious smartness) which would allow pass-by-value to be used (nearly?) as freely as one would use immutable references without the added linguistic complexity. Though if the gain were immaterial, or not worth added complexity, then certainly it wouldn't make sense to do it. I'm just throwing it out there.


2010/2/21 NoiseEHC <Nois...@freemail.hu>

Because a modern Intel processor has multiple load/store units so passing something in the L1 cache consumes almost zero time. (But we all know that a lot of zeros add up to some measurable penalty...) The more important feature would be to define (or just implement in the compiler) some "sync" closure passing/calling where the function would not be scheduled bu the Go scheduler but would be called (or inlined) directly.
So on a modern processor, copying large data types isn't such a bad thing after all? What about mobile platforms? Optimization that has little effect would hardly be worth it :P

~Steven

Carlisle Perera

unread,
Feb 21, 2010, 6:14:07 PM2/21/10
to golang-nuts
On Feb 21, 9:37 pm, chris dollin <ehog.he...@googlemail.com> wrote:
> On 21 February 2010 20:16, Carlisle Perera
> <carlisle.per...@googlemail.com>wrote:

>
> > On Feb 21, 8:13 pm, Steven <steven...@gmail.com> wrote:
> > > Are immutable references of any kind even seen as desirable behaviour for
> > > Go?
>
> > Definitely! At least by me. Besides generics I miss immutable
> > references the most (or a guarantee that the compiler optimizes away
> > needless copying the way you describe it).
>
> Presumably if you add immutable reference types to the language,
> let's say @T instead of *T, you allow a value of *T to be assigned
> to a variable (ditto passed as a parameter) of type @T?

Yes.

> Do you let a value of type func (*T)W to be assigned to a variable
> of type func (@T)W?

Yes.

> How about assigning a func()(*T) to a variable of type func()@W?

Allowed.

> Does a func (X) Spoo (@T)W implement an interface method
> Spoo(*T)W?

Yes.

> Is an []*T assignable to a []@T?

Yes.

> I'm not saying that you can't work out a clear, consistent set of
> rules for immutable references. I am saying that the cost of adding
> them may be more than expected.

I think there is just one general principle underlying immutable
references, which is that you allow a *T on the RHS for a
corresponding @T on the LHS but not vice versa.

Cheers,
C.P.

Esko Luontola

unread,
Feb 22, 2010, 5:25:26 AM2/22/10
to golang-nuts
On Feb 22, 1:14 am, Carlisle Perera <carlisle.per...@googlemail.com>
wrote:

> > Do you let a value of type func (*T)W to be assigned to a variable
> > of type func (@T)W?
>
> Yes.

I'm not sure whether I've understood the idea of immutable references
the right way, and how it relates to covariance and contravariance.

If we have function that uses a mutable reference:

func A( t *T) {
t.Mutate()
}

and then we assign that function to a variable:

var x func(@T) = A

will the following code mutate the parameter?

var immutableT @T = new(T)
x(immutableT)

chris dollin

unread,
Feb 22, 2010, 5:53:56 AM2/22/10
to Esko Luontola, golang-nuts
I think it would, which means that the assignment of A to
x would not be allowed.

(One has to be careful how one states the mutability rule;
it's not that an x-like function can't change it's t argument,
its that it can't change its t-argument /via t/. Other operations
might change t. That means that in

  func whatMeGuv(t @T) {
    a := T.alpha
    ...
    b := T.alpha

the compiler can't assume that a == b, the same as
for t *T, unless it can reason its way through the ...
)

--
Chris "allusive" Dollin
Reply all
Reply to author
Forward
0 new messages