In Go 1.18, generics are implemented through code specialization
The news of the time interval is that Go 1.18 Beta 1 is available, with generics. Barring something terrible being discovered about either the design or the current implementation of Go generics, this means that they will be part of Go 1.18 when it's released in a few months. Further, because we're already in the beta phase, it's highly unlikely that how generics are implemented will change substantial before Go 1.18.
(The Go developers generally won't want to do a major code change between beta and release, so if there turn out to be significant enough problems with the current implementation, it's more likely that generics will be pulled from Go 1.18 entirely, even at this late stage.)
One of the practical questions around generics in Go has been how they would be implemented. The generics design proposal was careful to not close out any options. To quote from its short section on implementation:
We believe that this design permits different implementation choices. Code may be compiled separately for each set of type arguments, or it may be compiled as though each type argument is handled similarly to an interface type with method calls, or there may be some combination of the two.
The first option listed, generating and compiling code separately for each set of type arguments, is generally called specialization or monomorphization. The second option would likely be a combination of how interfaces are implemented and how the Go runtime implements maps efficiently without generics, depending on what operations you were doing in the generic code.
With the release of Go 1.18 beta, we have a pretty firm answer. At least in the initial implementation in Go 1.18, generics in Go will be implemented with specialization. When you use generics in Go, the Go compiler effectively creates a non-generic version of the code that has the specific types you're using, and compiles that. If you use generic code with two or three or four difference types (or sets of types), you get two or three or four different versions of the code.
Update: This is partially wrong. Generics aren't necessarily specialized in practice, and the implementation design was not based around specialization. The current implementation in Go 1.18 is described in the design proposal Generics implementation - GC Shape Stenciling; see the comments for more detail.
One way to see this for yourself is with the use of the godbolt.org compiler explorer, which has the latest Go development version as one of its compiler options. That lets you write generics examples like this silly example and see what they compile to. This example has a very simple, silly generic function and some uses of it:
func Compit[T comparable](a, b T) bool { return a == b } func CompInt(a, b int) bool { return Compit(a, b) } func CompFloat(a, b float64) bool { return Compit(a, b) } func CompString(a, b string) bool { return Compit(a, b) }
These compile to three different sets of amd64 assembly code, each
using the relevant CPU instructions (and for strings, runtime
functions) to compare the different sorts of values. In addition,
CompInt
and CompFloat
actually receive their arguments in
different registers, because Go's amd64 register based calling
convention puts integer arguments in different registers than
floating point ones. This is a common decision in calling conventions;
many C calling conventions also behave this way, which can lead
to some interesting effects.
In theory, if you use the same generic code with different declared types that have the same underlying type, the Go compiler may not need to generate a new function for each declared type. In other words, suppose that you had:
type Tank int func CompTank(a, b Tank) bool { return Compit(a, b) }
In theory CompInt
and CompTank
could be the same function, not
merely two functions with the same machine code. In practice I don't
know if the Go compiler will ever do this, and there are a number
of situations where it can't do this because the declared type is in
some way used by the generic code. For example:
func Report[T any](s ...T) { for _, v := range s { fmt.Printf("%T %#v\n", v, v) } }
(Taken from the Go tip playground current example.)
If you invoke Report()
on arguments of type Tank
instead of
just int
, it had better be able to report their type correctly
(and it does).
Update: In practice, I now believe these sorts of functions can be compiled in Go 1.18 to use common code, depending on what types are involved. See the implementation proposal and the comments for how this works.
PS: An interesting thing happens with inferred typing of generics and constants. Suppose that you have the following two lines:
Report(Tank(10), 20) Report(30, Tank(40))
You cannot mix two different types in the same invocation of
Report()
, because it has only a single type for all of its
arguments. So these two cases sort of look as invalid as 'Report(
int(1), Tank(2) )
'. But untyped constants are special in inferred
typing; if it makes type inference work, they take on the type of
the typed argument instead of their default of, say, int
. So in
both lines, both arguments are of type Tank
.
PPS: You can appear to mix types in arguments to Report()
with the
trick:
Report[any]("abc", 10)
As we know from the draft release notes, the new predeclared identifier
'any
' is an alias for 'interface{}
'. Our syntax here is forcing
a type T
of interface{}
, which of course anything can be converted
to automatically, and then the real types show through in the end.
|
|