Hello everyone, thank you for coming to my talk.
If it isn't obvious enough, it's going to be about generics.
I'm Anton, I'm based in Bulgaria.
I'm a senior software engineer at Castellai,
where I'm working on making your Kubernetes closer to more secure.
I've been doing go-sings around 2019,
and before that I was doing Java,
and I think my time spent working put JavaScript consciously contributed
to my love for generics,
and it's the reason why I chose this topic for today's talk.
You can follow me in social media,
so let's get started.
So what are generics?
Well, it's just a way of programming that allows you to write code
which works with multiple types, while still preserving types,
and they were introduced in GoFerry recently
in Go1.18, which came out in 2022.
So how many of you have actually worked with generics?
Like, written a generic function, or use some library that has had generics.
So, okay, quite a few hints.
Yeah, I'm still going to do a very quick introduction to generics.
This is a generic function.
It looks like a regular function,
but here in the square brackets,
we have this section what makes a function generic.
We define here the generic type t,
and we give it a type constraint, which limits what this type can actually be.
And then in the body we have a regular code that uses this type.
So, with the time of writing this function,
we don't know what this type t is.
We only know that when we start using it.
And we use it like this.
Here, recalling this function twice,
once with integer arguments and one with floating point numbers,
and you see the arguments, they match the type of,
we define here.
Also, the compiler is pretty smart,
so we don't need to actually give it a type.
It can infer it by itself.
In this case, this is still an integer function,
and our result will still be size of integers.
And if you want to do something stupid like this,
the compiler will scream at us.
It will not let us write this code because
these two things here are different types.
So, we cannot use them with this function,
because we require the type to be the same.
And this is basically the gist of it.
A little bit of history on how we got here.
I guess most of you know,
go was released in 2009 in November,
with this blog post.
It was internally developed in Google,
shortly before that.
And in 2009, they released it to the world.
So, why did they actually create a new language in 2009?
There were a lot of languages already in the wild,
so why create a new one?
Well, at that time, Google was using a lot of C++,
so they had a big C++ code base,
and one thing which is plus plus,
it's a very powerful language,
but it's also very complex one.
So, you can do a lot of stuff and the complexity can get quite high.
So, for those that are harder to unboard new people,
especially in more junior ones to their complex code base,
that's why they wanted to have a new language,
which is more simple.
So, what it is years to unboard new people.
Also, C++ is notorious for its slow compilation times,
So they wanted a new language that mitigates that as well.
But they very much liked how fast C++ runs.
So they wanted to keep this in a new language that they created.
And that's how it came to be.
However, it turns out that genetics kind of complicate each one
of these three conditions.
So back then in 2009, it kind of makes sense
that to not have genetics in the language.
However, it took them less than 24 hours
for people with the internet to start
on planning about the lack of genetics.
Yeah, this is a Google group, a discussion about the language.
And you see, this is November 11.
So one day after the language was released,
someone complained that the language doesn't have genetics.
And this kind of set the tone of how the next few years
are going to the combined people will continue
complaining about the lack of genetics.
It's always going to be one of the most requested features
to be able to the language.
So what did the goal team do about that?
It turns out they did quite a lot.
So one year later, we got the first proposal for genetics.
It was called type functions.
It was rejected.
Obviously, then we had another one, 2011 called generalized types.
Then we had two more into an 13, called type parameters
and generalized types V2.
As you know, all of them were rejected.
All four of those proposals came from within the goal team.
And the majority of work was done by one guy.
He was called Ian Lansteuer.
So yeah, this effort came from the goal team itself.
Then we had some radio silence on the topic.
But in 2018, in 2019, we got two more proposals
called contracts and contracts V2.
Essentially, this, we got one more in 2021 type parameters.
And this is actually the proposal that the goal accepted.
And these three proposals, they were still written
by the same guy from before.
So yeah.
Finally, he got his way in, we got genetics.
We did this final proposal.
So what took them so long?
Obviously, the goal team wants to do that.
All these proposals came from within the goal team.
The community also wanted to hear that.
So what took them so long?
I think, last of this question, we need to get back
to this book post by Roscox.
It's from December 2009.
So this is the first month of the language.
And it's called the genetic dilemma.
What it says in this article, it's basically
he came to the conclusion that the genetic dilemma
is do we want slow programmers, slow compilers,
or slow execution times?
So what Roscox did was he looked at all these other languages
that already existed back then.
And he looked at how they implemented genetics.
And he realized that if you want genetics or don't want
genetics, you'll need to choose one of three trade-offs.
He basically came up with three categories of languages.
The first one is languages like C.
But don't get genetics, so you don't have the productivity boost
that feature like genetics give you.
But you do have fast compilers and fast run times.
Then you have C++, where you do have genetics.
And you do have the fast run time.
But you have you get the slow compilation time
because of the way genetics are implemented.
And then you have Java, where again, you have fast programmers.
You have fast compilers.
But because of the way genetics are implemented,
which is completely different than C++,
you get the performance heating in runtime.
And having in mind that, already,
did have limited support for genetics for the building types,
like maps, slices, channels.
I guess they just didn't want to take any one of these three trade-offs
and just decided to leave the things as they are.
But as we show, we already have an accepted proposal.
And now let's talk about the implementation.
Let's see what this proposal tells us about how genetics
are going to be implementing.
Go.
Well, it didn't tell us anything.
Oh, the final proposal went also
for all the previous one.
In their implementation paragraph, they say,
we are aware of the genetics dilemma.
We don't want to take a decision on this.
The proposal is flexible enough so that you can implement it
however you like.
So basically, it doesn't give us any any guides on how
it's going to be implemented.
So what do we do?
We need to write more proposals for the actual implementation.
In reality, there were three proposals written for the implementation.
The final one was accepted, but for us
to better understand how we came to how we came there,
we need to look at all three other proposals.
The first one is called Stencelink.
And this is basically the simplest way of doing things.
And Stencelink means that when we have a genetic function
and we use it with some types, we're
going to compile one function for all the types
that we use our genetic function with.
Let's see an example.
On the left, we have a genetic code,
pretty simple function that just takes two values
and turns the sum of them.
And we are calling it with first with integers
and then with floating point numbers.
What we have done during compilation is that the compiler
will actually produce two functions.
The first one we work with just integers,
the second one we work with just floats.
And on the coincide, we're going to replace the genetic function
with the specific implementation of this function
for this type.
Of course, this is just pure code.
The GoCompower doesn't produce any GoCode,
it produces machine code.
But it's just an illustration of how this is going
to look like according to the proposal.
So yeah, this is done.
This is the georepter notation and the floating implementation.
The big benefits here are that you basically
don't have any performance during any performance
penalty during current time.
Because in your own time, you don't
have genetic functions, we have regore functions.
However, the performance hits come during compilation.
Now the compiler has to do a lot more work
to compile this function multiple times for each of the types
that we work with.
And this also contributes to the bigger binary size,
because now we have more stuff in our compiled binary.
So because of these reasons, this proposal was rejected.
The second proposal is called dictionaries,
and this is more or less the way Java does generics.
This says we're going to compile a single function
for the genetic function.
And we're going to use dictionaries
to keep all the type information for the specific types
that we use our function with.
Let's see how this looks like.
Again, we have the same simple function.
And after compilation, this looks like this.
Now you see on the left, we have just
a single implementation of our same diff function.
And we see here are two arguments A and B.
But now they are just pointer.
Point pointers.
And the return value is also pointer.
But we also see we have one more argument
that's being connected to our function.
And this is this thing here, the dictionary.
And this here contains all the type information
for the types that we are using this function with.
You see on the top, we have the type.
And it also, like all the functions that are present
on these types, we have them in the dictionary as well.
In this case, the data function
has substitutes the plus operator.
What happens when we call such function,
you see here, we are calling it with two integers.
The compiler now needs to box these values into pointers.
And it needs to come up with this dictionary
that provides implementation.
And you see in the implementation, we actually need
to unbox these values to get there,
to get their actual integer values.
We need to sum them.
And then we need to box this again into a pointer
to enter and return it.
And here, we need to unbox that return value, again,
to get the actual value.
This doesn't have a big performance
penalty in compilation time.
Yes, the compiler does more work, work,
but we can say it's negligible.
However, the big penalty here comes during runtime.
Because all these referencing can be referencing,
it just adds some overhead, and it makes our code run slower.
Also, it kind of prevents compiler inlining,
because now a lot of the information is not present
in compile time is present only through runtime.
So this prevents the compiler from inlining our stuff.
And I'm not an expert in compilers,
but I know that this is generally a good thing inlining.
And when we're using this approach,
we are preventing the compiler from doing this nice
optimization for us.
So we can say we get a big performance
hitting in runtime.
And this is why this proposal will reject it.
So we can't return proposal, which is called gc shape stenciling.
This is the proposal that actually got accepted.
And this is more or less the middle ground
between the first two proposals.
So they said stenciling is bad.
Dictionary is our bad.
Let's do both.
Yeah, so as I said, this is the middle ground.
So it does a little bit of stenciling
and a little bit of dictionaries.
And actually what it does is,
it does produce multiple versions of our functions.
But the only for types that have different representation
in memory, this is what gc shape stands for,
garbage collector shape.
So types that have the same memory representation,
we get the same implementation of the generic function.
And the type information we present in these dictionaries.
This is the proposal that got accepted.
So how this looks like?
I have here still a simple example of a different
in the previous one.
I have a generic function called pint and return.
It accepts type t, which needs to satisfy the string
or interface.
It gets the value of the string.
It brings it and it just returns the value t.
Pretty stupid function, but it works for an example.
And I'm calling this function four times, four times,
with four different types.
First time with an integer, then with a string,
they are wrapped into their own custom type
so that we can provide the string implementation.
And then with two structs, A and B, which
I am passing as pointers.
So let's see how this looks like after compilation.
But now we don't need to look at two to code,
because this is the actual implementation of generics.
So we can do something better.
While I did, I compiled this code.
I gave it some compiler flags to prevent
optimization and inlining.
And then I used a go-to object dump command
to do the sample executed by what they got from the go-butth.
And we can actually look at some of this sample code.
This is how it looks like.
On the right is that this is the sample code.
And we see the shapes of the compiler produced.
So we see how our function was compiled three times.
But why three times, if we call it four different types,
well, because A and B, they are just pointers.
So they hear the same memory representation.
They're just a U in date.
So the compiler compiled only one shape for these two types.
Let's dive into the implementation of this function.
Yeah, this is for the integer.
You see here, go shape int.
This indicates that this is for the integer.
Then you have the string here for the string value.
And then you have the U in date for the pointers.
So let's dive into that function.
So let's see what happens, what happens inside.
We see here in the beginning, we are moving some stuff around.
R01, these are CPU registers.
And in this case, they hold the arguments of the function.
So R1 is the value t that we see here.
And R0 is the type dictionary that we saw in the previous slides.
So it has actually two arguments, not one, because of the type dictionary.
So with these two instructions, we are moving them to the stack.
With the next instructions, we move some more stuff around.
And actually what happens here is now in R0, we have the value t.
But in R1, we have the string method of our type.
So this was inside the type dictionary.
But with these instructions here, we took it out of the type dictionary
and put it into the CPU register R1.
This is the string method of A or B.
We cannot save here, because this is just implementation.
So whichever type we use it with, this will be the string method.
And then we actually call this method, the string, which is this code here.
So we see here the type dictionary in practice.
So here some more stuff continue to happen.
We see here the code R1 time.com to the string function,
which is a function that converges the string value
to something that's represented by R1 time.
And then we see the code to print a one, which is this thing here.
And then we just return the value, but I have omitted that.
The following instructions of how I got here,
you can find this githical repository.
You can also find the slides there.
So yeah, you can reproduce this example
since you see for yourself what's going on.
So yeah, this is the proposal that got accepted.
This is how genetics are implemented in goal.
And you see here the benefits are kind of the same as drawbacks.
We have little, little performance penalty in compile time,
because we are compelling more shapes,
but they are limited by the number of types
that we can produce with different memory representations.
And we still have simple performance penalty
around time, because we're still using these dictionaries.
But we can say they are also limited by the number of types.
I can just ask this here that says little to high,
put an asterix.
So I want to elaborate on what this means,
and what this, what can high mean.
So there is this rock post by the folks from one at scale,
one at scale with a company that does manage the SPR offerings.
So there are a lot of code, which is very,
runs on very low levels, so performance is very important for them.
And shortly after genetics were introduced in 2022,
they wrote this block post that says genetic can make your goal
code slower.
I recommend you read the whole of it.
It's a very interesting one.
They do a lot of, this assembly similar to what I did.
With much more examples, benchmark and so on.
So you can see for yourself.
But the TODR is that genetics can introduce
a performance hit in a baseline of two to three microseconds
for a function that usually runs in five microseconds.
So this is like a 30% performance hit
if you're using genetics.
What they also say in this article is that interfaces
can produce a similar performance hit,
because it actually interfaces work in a similar way
with the type dictionaries where you start a type information.
So using them can have similar performance hit,
which is smaller, so like one to two microseconds
for a five microsecond baseline.
So yeah.
And the worst case scenario actually
is if you use a genetic function
and you give it a new pass interface to genetic function.
Because in this case, the performance hits accumulate.
So you can get, like, these things accumulate,
so you can get 3 to 5 microseconds performance penalty
for a five microsecond function.
So yeah, if you have a genetic function,
don't give it interface.
You just give it a specific type.
And yeah, if this really matters to you,
just don't use genetics, because the other parts of a goal
hasn't gotten any slower because with genetics,
is just genetics because of the way that they are implemented.
Also, I've really never heard someone complain
about interfaces being slow.
So I guess for most of us, this doesn't really matter,
because we don't write so performance critical code.
But if you do, here this in mind.
So to summary, to summarize, we have long-awaited genetics
to be implemented in the language.
The goal team really took their time with it,
because I guess they just want to make it right.
And even though they are serve for some performance
implications, I guess for most of us, they wouldn't pick a huge impact.
And if they do, just, I guess, don't use them.
And nothing in the specs prevents these from being optimized
further.
So in future releases, and if people complain more,
I guess we can get some performance fixes.
And let's finish with this table.
We already saw this, like the three categories of languages
according to the genetic dilemma.
So let's see where both ends here.
Do we have the, do we have fast programmers now
so we can write genetic code?
Yes.
Do we have fast compilers still?
Yes, mostly.
And do we have fast runtime?
Yes, we have some little cave eggs.
So comparing to the other three, I would say the goal team
did pretty well on genetics.
And I think we are in a good state.
That's all we owe.
You can see the slide here.
Here.
This is the awesome question.
Yeah, that's it.
Do we have any questions which are very important?
If you are leaving, do so in your most other most silenced.
Yes.
No questions.
I have a question over there.
I'm going to quickly give you the microphone.
Please keep your hand up.
Hey.
Hey, sorry.
I was wondering.
I was wondering whether it was possible to use a compiler flag
to decide implementation.
Like for release build.
You may want some runtime.
Right?
So in that case, you may want stenciling approach.
No?
I don't hear anything, no?
I was wondering whether it was possible to use a compilation flag
to determine whether you want fast runtime.
So like stenciling approach and otherwise.
I did hear something about compilation.
Sorry.
Let me come close to you.
Yeah.
He was wondering if it was possible to add a compilation flag
to refuse.
For me, this would correct.
I would use the slowness in the code.
If you could add a compilation flag to disable generics.
To disable generics?
No.
I don't think so.
I mean, if you want to disable generics,
just don't write generic code to just write.
I don't hear anything.
It's very hard to hear this code everybody leaving.
If you have more questions, you will be available in the hallway
later, just say hi, giving me a free drink or something.
And you will happily set your answers and questions.
Thank you very much.
Run over glass.
Thank you.