Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I was recently thinking that I would really like "field references" in go, e.g. via type.Field, like you can do for methods to get unbound methods. Partly because it might enable generics over "contains field(s)" instead of just "contains method(s)".

In the meantime I guess there's always anonymous functions (to use as accessors). They're not hard to be generic over.



What would you use such a field reference for that a https://pkg.go.dev/reflect#StructField isn't appropriate?


I haven't tried benchmarking a cached reflection-driven field reference, now that I think about it... I'll give that a try, very good point.

If that performs the same as a normal field access (or the equivalent behind a non-inlined function or something), yeah, that'd be completely fine. My main thought was that you can very easily reference and use `type.Method` (you just have to pass an instance as an additional first argument), so a straightforward `type.Field` equivalent would be convenient in a number of places. E.g. you could avoid the need to add accessor methods, which isn't possible on types you don't control, and no need to create anonymous functions everywhere to work around that.


From poking at this with benchmarks, results are basically:

Hiding an anonymous `return it.field` behind an interface{} in an attempt to stop inlining or other optimizations: 2x worse than directly accessing a field (same as direct access when not doing the interface dance, so it's preventing something at least). So "excellent performance", though there may be optimizations happening in a benchmark that aren't realistic in a larger system.

Using `reflect.ValueOf(it).Field(0).String()` each time: 60x worse than direct field reference. Go reflection remains pretty fast, but that's rather noticeable.

There does not appear to be any way to cache ^ that reflection operation. I.e. I don't see any way to go from `reflect.TypeOf(it).Field(0)` to "read that field from this instance".

---

And somewhat surprisingly: `m := instance.Method; benchmark{ m() }` performs ~10x worse than `benchmark{ instance.Method() }` and `m := type.Method; benchmark{ m(instance) }`. There's no intentional optimization-defeating here, so it is entirely possible this difference disappears in practice, but I'm surprised that they're not identical.

And I couldn't figure out a way to get a reference to a pointer-implemented method, e.g. `m := (&type).Method` or something. It only works on `m := type.Method` when the method is a value receiver. Possibly I'm missing something obvious.

---

I kinda want to redo ^ these and check compiler output closely, and try adding more packages / make more realistic optimizer-information-loss like you'd see in more normal go code. I suspect there are still some unrealistic ones occurring. But I've gotta drop this for now, so that's going on my ever-growing todo list.


I don't really know what you mean by "optimization-defeating" here or in your other comment; unless the language offers some heavy duty compile-time partial-evaluation, there are fundamental costs to certain indirections. It's not just the result of the optimizer deciding not to inline or whatever, a dynamic jump costs more than a static jump.

I’m also not sure about the `benchmark{ ... }` syntax, but:

    m := instance.Method; benchmark{ m() }
In the general case, this will require allocating a closure and making a dynamic call.

    m := type.Method; benchmark{ m(instance) }
This will require making a dynamic call.

    benchmark{ instance.Method() }
This is a normal static call. It will be fast, even if not inlined. (Inlining today is often more critical as a supporting optimization for devirtualization than avoiding the minimal cost of a static call.)

---

   reflect.ValueOf(it).Field(0).String()
`String()` is a bad case for a comparison here because it works and incurs a higher cost even if the field isn't a string.

60x sounds bad, but I really cannot stress enough: As soon as you're not doing a simple fixed-offset memory fetch from a base pointer, you might be paying 10x anyway no matter how good your optimizer is. That's just the cost of unpredictable memory access, even before we start talking about downstream impact on the code generator.

If you only want to support direct fields and not really the equivalent set of named fields you could type in a Go program, you can do some tricks with unsafe.Offsetof which will be faster, and probably cachable. But it's also called unsafe for a reason.


`benchmark {...}` is just some shorthand because the actual code is boilerplate-filled. It's not valid Go.

Optimization-defeating: go infers when to move things the heap vs keep it on the stack, and does somewhat aggressive in-function optimizations and inlining that it does not do cross-function. One common, very simple, and reasonably effective way to prevent some of that is to erase type information by passing a value through an interface{}. Even if you immediately unpack it and reuse the reference, Go has no jit, so it gets the job done alright. There are some other things that don't survive cross-package analysis, last I looked, so using multiple packages can also help make your benchmarks more realistic. It takes a lot more effort to be truly realistic in even one benchmark, much less the 9 various flavors that I tried.

And the String() piece is because I had it return a string because why not. Variable-sized data is extremely common so it's realistic enough, and using the correctly-typed reflection funcs usually avoids some reflection and boxing and allocation costs that I didn't feel like trying to address in more detail.

None of which was written out explicitly because there are tons of caveats regardless of the care I tried to take, so I just mentioned that they existed and moved on because I couldn't spend more time at that time.


It definitely won't match the performance of normal field access, just as passing an unbound method reference won't be as fast as calling it directly, or calling through an interface won't be as fast as calling on a concrete type.

The best performance you'll get is probably from putting the fields together into a substructure and passing around pointers to that. Which doesn't need any reflecting or additional language scaffolding.


Why would an unbound method be any different than a bound one? They both involve exactly the same function table lookup, and execute against data that's somewhere else.


> They both involve exactly the same function table lookup, and execute against data that's somewhere else.

It's not that it's bound vs. unbound, it's that you passed it somewhere else so the call is necessarily dynamic. It would happen with non-method functions also. (In neither case is there a "function table lookup" - that's rather if you call through an interface / generic dictionary.) A bound one would be even more expensive because you also have to allocate a closure for the receiver.

The point is: You want some non-compile-time behavior, whether that's a function or field access, you're going to pay more. That time, not regular field access, is what should be compared to reflect performance.


Ah, yeah, it's definitely optimization-defeating (or more-so in any case). Completely agreed on that.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: