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 with C++,
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 example, for the end of the scatter to onboard 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 onboard new people.
Also, C++ is notorious for its slow compilation times.
So, they wanted a new language,
not 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 complaining
about the lack of genetics.
Yeah, this is a Google group,
a discussion about the language,
and you see, this is November 11th.
So, one day after the language was released,
someone complaining 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 in 2013 called type parameters
and generalized types V2.
As you know, all the more 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 and 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 got 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.
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 had 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 have 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 to 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,
go already did have limited support for genetics
for the building types, like maps, slices, channels.
I guess they just didn't want to do 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 implemented and go.
Well, it didn't tell us anything.
The final proposal and also 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 guys 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 stenciling,
and this is basically the simplest way of doing things.
And stenciling 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 cosite, 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 go compiler doesn't produce any go code.
It produces machine code,
but it's just on illustration of how this is going
to look like according to the proposal.
So yeah, this is done.
This is the implementation and the floating presentation.
The big benefits here are that you basically don't have any performance
during any performance penalty during current time,
because in current time you don't have genetic functions.
We have regular 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 other 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 scenarios.
This says we're going to compile a single function
for the generic 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 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 the introduction dictionary as well.
In this case, the data function
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 there actually with your 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,
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,
but we can say it's negligible.
However, the big penalty here comes during runtime.
Because all this 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, what's the information is not present
in compile time is present on it 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 here in runtime.
And this is why this proposal will reject it.
So we can enter a third 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,
dictionaries are bad, let's do both.
Yeah, so as I said, this is the middle ground.
So we do this a little bit of stenciling,
and a little bit of dictionaries.
And actually what it does is,
it this produced multiple versions of our functions,
but only for types that have different representation in memory.
This is what gc shape stands for,
garbage collector shape.
So functions, types that have the same memory representation,
we get the same representation 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,
although different in the previous one.
I have a generic function called pint and return.
It accepts type t,
which needs to satisfy the stringer interface.
It gets the value of the string.
It brings it and it just returns the value t.
Please keep it 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 pseudocard
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 optimizations and inlining.
And then I used the go-to object dump command
to do the sample executed by the go-view.
And we can actually look at some of the sample code.
This is how it looks like.
On the right is this a demo code
and we see the shapes of the compiler produced.
So we see 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 have the same memory representation.
They are 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.
R0 and R1, these are CPU registers.
And in this case they hold the arguments of the function of the functions.
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 say 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 runtime.com to 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 the R1 which is this thing here.
And then we just return the value but I have omitted that.
The power instructions of how I got here.
You can find this githical repository.
You can also find the slides there.
So you can reproduce this example and see if we are self 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 the drawbacks.
We have little performance penalty in compile time because we are compiling more shapes.
But they are limited by the number of types that we can produce with different memory representations.
And we still have some performance penalty around time because we are still using these dictionaries.
But we can say they are also limited by the number of types.
I gave this asterisk here that says little to high with an asterisk.
So I want to elaborate on what this means and what this.
What can high mean.
So there is this block post by the folks from one scale.
One scale is a company that does manage the SPL offerings.
So there are a lot of code which is very run some very low levels.
The 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 bit.
It's a very interesting one.
They do a lot of this assembly similar to what I did.
With much more examples, benchmarks and so on.
So you can see for yourself.
But the TODR is that genetics can introduce a performance hit in the baseline of like two to three microseconds for a function that usually runs in five microseconds.
So this is like a 50% performance hit if you are using genetics.
What they also say in this article is that interfaces can produce a similar performance hit.
Because 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 and you pass interface to genetic function.
Because in this case the performance hits accumulate.
So you can get like.
This thing is accumulates or you can get three to five microseconds per performance penalty for five microseconds function.
So yeah don't if you have if you have a genetic function don't give it interface.
It just give it a specific types.
And yeah if this really matters to you just don't use genetics because the other parts of the goal hasn't gotten any slower because of genetics.
It's 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 sure 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 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 the goal stands here.
Do we have the do we have fast programmers now to write genetic code?
Yes.
Do we have fast compilers still?
Yes mostly.
And do we have fast run time?
Yes with 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 will be all.
You can see the slide.
Here.
This is the awesome question.
Do we have any questions which are very important.
If you are leaving do so in your most other most silence.
No questions.
I have a question over there.
I'm going quickly to give you the microphone.
Please keep your hand up.
I was wondering.
I was wondering whether it was possible to use a compiler flag to decide implementation.
For release, you may want to run time.
So in that case, you may want to stand still.
I don't hear anything.
I was wondering whether it was possible to use a compilation flag to determine whether you want
to do it like fast run time.
I did hear something about compilation.
Sorry.
Let me come close to you.
He was wondering if it was possible to add a compilation flag to reduce.
If you could add a compilation flag to disable generics.
I don't think so.
If you want to disable generics, just don't write a code.
It's very hard to hear this code everybody leaving.
If you have more questions, you will be available in the hallway.
You will be available later.
Just say hi.
Give him a free drink or something.
I will have a message for your answers and questions.
Thank you very much.
Run over class.