[generics] Print[T Stringer](s []T) vs Print(s []Stringer)

571 views
Skip to first unread message

wilk

unread,
Dec 23, 2020, 12:54:27 PM12/23/20
to golan...@googlegroups.com
Hi,

https://go2goplay.golang.org/p/fTW3hJYNgfU

type Stringer interface {
String() string
}

Print[T Stringer](s []T)

Print(s []Stringer)

Both forms works.
How to prevent double way to do the same things that can be confusing ?

--
wilk

Ian Lance Taylor

unread,
Dec 23, 2020, 1:03:40 PM12/23/20
to wilk, golang-nuts
Both forms work but they mean two different things.

Print(s []Stringer) takes a slice of the type Stringer.

Print[T Stringer](s []T) takes a slice of some type T, where T
implements Stringer.

For example, if MyInt implements Stringer, and I have a []MyInt, then
I can call Print[T Stringer](s []T) but I can't call Print(s
[]Stringer), because a []Stringer is not a []MyInt.

Ian

Sebastien Binet

unread,
Dec 23, 2020, 2:59:04 PM12/23/20
to Ian Lance Taylor, wilk, golang-nuts
to illustrate what Ian wrote, with your example:

https://go2goplay.golang.org/p/YTqF-WS0m6O

func main() {
var ps = []*Person{
&Person{"Arthur Dent", 42},
&Person{"Zaphod Beeblebrox", 9001},
}
Print(ps) // ok.
Printi(ps) // compilation error.
}

‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐
> ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
>
> You received this message because you are subscribed to the Google Groups "golang-nuts" group.
>
> To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
>
> To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/CAOyqgcUALDVBWJwhHYxT6T5%3Dz2tvSKp7yMy%3DF4HSJc_uTZZKGQ%40mail.gmail.com.

wilk

unread,
Dec 23, 2020, 3:20:48 PM12/23/20
to golan...@googlegroups.com
I understand the differences. But i'm affraid someone who never used
Go before will use type parameters instead of interface which is more
idiomatic i think.
I mean it will be difficult to say, you could use type parameters but
you should use interface, or something like that...
I'm speaking about ease of learn Go2.

--
wilk

Henrik Johansson

unread,
Dec 23, 2020, 3:41:16 PM12/23/20
to wilk, golang-nuts
Why will interfaces be more idiomatic once generics lands? It remains to be seen I guess but I could very well see the other way become the idiom.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.

Elliot

unread,
Dec 26, 2020, 10:59:01 PM12/26/20
to golang-nuts
If we remove slice from OP's example:


func Print[T Stringer](s T) {
    fmt.Print(s.String())
}

func Printi(s Stringer) {
    fmt.Print(s.String())
}

Are these two equivalent? When should one be chosen over the other?

To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts+unsubscribe@googlegroups.com.

K. Alex Mills

unread,
Dec 27, 2020, 12:25:55 AM12/27/20
to Elliot, golang-nuts
While it depends on the final generics implementation, my understanding of how things stand now is that Print would compile down to a separate chunk of binary for each type T that is used. For instance, if you used Print[A] and Print[B] in your code, they would each refer to separate binary implementations in which T is replaced by A and B, respectively.

Printi does not do this, so you should see a smaller binary. 

IIRC, Printi also has to do a bit of work to lookup the Stringer method on the type inhabiting the interface. I don't think that creates a significant performance hit, but I might be understating the overhead of interface dispatch. A benchmark would help here (alas, I am on my phone).

With respect for the concerns mentioned above, I don't see an argument for preferring Print over Printi. However, there may other concerns which I am unaware of.

To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/d044ae30-7254-4a86-9cba-1bc18eeb7fefn%40googlegroups.com

Arnaud Delobelle

unread,
Dec 27, 2020, 5:24:22 AM12/27/20
to K. Alex Mills, Elliot, golang-nuts
In my opinion, the issue is that there are two ways to express (almost) the same thing and that in itself creates friction in the language.

There may be a valid reason to choose one version over the other, but every time it will be necessary to review the pros and cons of each alternative.

If we could have "generics" without having to make this choice it would unblock the whole thing as far as I am concerned.

Cheers

K. Alex Mills

unread,
Dec 27, 2020, 12:32:48 PM12/27/20
to Arnaud Delobelle, Elliot, golang-nuts
Since in this case the use of generics doesn't let you do anything new, I would argue that the KISS principle applies and the non-generic version should be preferred.

I think a linter can be added to go vet to warn about instances like this one (where the type parameter can be replaced by the type bound) to encourage simplicity.

But as Ian pointed out, in the version that takes a slice argument, using generics does allow you to do something you couldn't do otherwise (without reallocating the contents of the slice to effect a "cast" to []Stringer). In this case using generics actually makes the caller's job simpler and improves performance by avoiding the need for reallocation.

Arnaud Delobelle

unread,
Dec 27, 2020, 12:35:26 PM12/27/20
to golang-nuts
I understand the difference, but that doesn't prevent me from having to choose between the two implementations.  To simplify greatly, and as you pointed out in your reply, there is a tension between "simplicity" (non-generic) and "performance" (generic), and that is where I fear the friction will come from. 

Looking beyond the syntax, the code for both implementations expresses exactly the same thing.  So in a way it is the syntax that forces the choice to be made at the time the function is written.  What I would like is not to have to make the choice at the time that I write the function, but perhaps this wish is impossible given the current Go syntax.

Arnaud

robert engels

unread,
Dec 27, 2020, 12:36:41 PM12/27/20
to K. Alex Mills, Arnaud Delobelle, Elliot, golang-nuts
That is not true. The generic version can have significant performance implications for low-level/tight-loop functions as it will avoid the indirection required for interface method dispatching (at the expensive of code explosion for common calls with lots of types).

K. Alex Mills

unread,
Dec 27, 2020, 12:54:12 PM12/27/20
to robert engels, Arnaud Delobelle, Elliot, golang-nuts
Makes sense. I just attempted to quantify the overhead and it looks like it's at least 2x on my machine, which is not as trivial a performance bump as I had imagined, so thanks for the push back.

There are clearly some uses for which this pattern is going to be desirable for the performance benefits of monomorphization.

Benchmark I used is included below, for completeness. It only quantifies the overhead of interface dispatch, which I think is the salient point. It may be worth noting that I don't have a go2 compiler locally and the playground doesn't support benchmarks. OTOH, this performance may be closer to what we might expect from go2 in case the current prototype isn't (yet) as optimized as it could be.

package main

import (
  "strconv"
  "testing"
)

func BenchmarkNonGenericScalar(b *testing.B) {
  x := myInt(5341)
  for i := 0; i < b.N; i++ {
    nonGeneric(x)
  }
}

func BenchmarkGenericScalar(b *testing.B) {
  x := myInt(5341)
  for i := 0; i < b.N; i++ {
    generic(x)
  }
}

type Stringer interface {
  String() string
}

func nonGeneric(x Stringer) string {
  return x.String()
}

// my assumption here is this will output monomorphized binary code roughly equivalent to
// what the generics implementation would output; that assumption may be wrong.
func generic(x myInt) string {
  return x.String()
}

type myInt int

func (i myInt) String() string {
  return strconv.Itoa(int(i))
}

BenchmarkNonGenericScalar
BenchmarkNonGenericScalar-24
21053407          55.3 ns/op        16 B/op        2 allocs/op
BenchmarkGenericScalar
BenchmarkGenericScalar-24
52173685          23.7 ns/op         4 B/op        1 allocs/op
PASS  

Axel Wagner

unread,
Dec 27, 2020, 1:08:46 PM12/27/20
to golang-nuts
@Arnaud: I tend to agree that it's a downside of having two ways to do things. However, I don't know how we would get the actually additive expressive power of generics without that downside. If you have any ideas, that would be great.

@Robert: I think sweeping statements about the performance of generic code vs. non-generic code are premature. In particular, in a situation where *either* generics *or* interfaces can be used for the same effect, I think the goal should be for both to perform equivalently. In those cases, there is nothing preventing the compiler from doing exactly the same optimizations - that is, nothing in the language prescribing that an interface needs to be passed and used as a boxed value, instead of devirtualizing the function to operate on each concrete type it is called with. In fact, AIUI, go 1.16 will already be significantly more clever about doing that optimization.
Of course there *are* cases where it's not possible to devirtualize a function. But in those cases, generics will run into similar problems.

I think it remains to be seen, if and when there is a performance benefit and how large it is. Personally, *because* I would prefer us to continue to favor interface-code where it can be used and only reach for generics if they are necessary, I would very much like them to perform similarly, so people don't use generics just "because they're faster".

Reply all
Reply to author
Forward
0 new messages