It's always very nice to see what you have been working on, and this year it's parallelism
which you understand is very difficult in the microcontroller space.
Do your earrings also run tiny go.
And you can turn on the mic for me.
The microphone is still omitted.
Let me...
It was a hardware issue.
That's all you want to give you a ride of applause with a person with a tiny go earrings.
Well, hello, everyone.
So today I'm going to talk about the implementing parallelism.
As you all know, concurrency is not the same as parallelism.
concurrency is like running multiple quarantines and having them run independently of each other
and communicating a search.
And this is all built into the go language itself, and most popular languages.
But parallelism is actually something different.
That's actually running multiple things at the same time, like from different CPU course.
And well, go is well known for doing parallelism very well.
In the beginning, it was only a single threaded system.
Like starting with go 1.5 parallelism was enabled by default.
In tiny go so far we haven't had any parallelism because basically most market controllers are single core, like almost all of them.
But this is slowly changing.
So why are we adding parallelism now?
First of all, because tiny go is not only used for market controllers, but also for Linux.
For example, the euro project, which is something like busy box, but threatening go.
Well, they would like to string the boundaries, and so they try to use tiny go.
But I'm sure there are many other projects who we use tiny go for a similar purpose on the next.
For what, for example,
So basically we do have concurrency on what, for example,
but the way it works is it unwinds the stack and rewinds the stack on every
Garting switch, which is very, it's not efficient at all.
It's like pretty slow and also some issues.
And like, I would like to try when I're using web work for instance,
that works better.
Also, it's probably going to improve performance, but that's on a side benefit.
And of course, for a bare metal sport.
So, there are very few market controllers that have more than one coral,
basically all of them are single-core.
But there are a few exceptions.
One is the R2040 that's actually running on this go for batch.
But this chip didn't exist back when I saw the tiny go.
But that's why we are adding training,
multicore support right now because it's time to get useful.
The rest of the talk will focus on Linux mostly,
but most of the things I'll be talking about are applicable to all the other things as well.
On Linux, I decided to use one-to-one training.
That means, or one-to-one scheduling.
That means that every Garting runs in a separate stress.
Actually, that's how KCCC also works if I remember correctly.
This has a few downsides.
Like, for example, it will use a little bit more RAM,
but not that much more, most of it is virtual.
On it, like, eight kilobytes is physical RAM at the start of Garting.
And it will be a little bit slower.
Switches between Garting will be slower.
So there are a few downsides, but benefits are that it's way easier to implement.
I just use P-trats, basically.
C-gore calls are very easy, just call them.
Instead of, like to make standard go,
you need to switch your different seats stack to run C-gore,
and then switch back.
That's one of the major reasons why it's slow.
System calls can just be made directly.
Like, there's no reason to use E-pull on things like that.
And also, normally, when a Garting does some numeric computation, for example,
that doesn't block, it can just lock up the OS threats,
without the OS knowing, like,
the schedule has to preempt it at some point.
But I think I put in, like, check set at some points.
So one Garting doesn't hold the system.
But when you're using Tering,
the operating system is already doing this by default.
So it's pretty, basically.
So there are a few, and here on the slide,
you can see, for comparison, the different performance overheads.
And, like, yeah, treadlons takes, like, five microseconds in it.
Oh, well, but it's not that much time.
To implement all this, a lot of things were needed.
First of all, of course, the schedule are, like,
in self-using a single tread.
It uses multiple tread, so, obviously, that needs a change.
The garbage collected needed to change.
Before we could just allocate memory,
and as soon as the heap ran out,
we ran a garbage collection cycle.
And by definition, there was nothing else running,
because that was the only thing running at the time.
But now, we need to stop the whole world,
every tread that's running.
And we do this, basically, by sending each guarantee
or each tread actually, a post-exignal.
Basically, to pause, scan all the stacks, scan the globals,
and then everything can start running again,
and the heap can be all the garbage can free, basically.
I'll come back to a chat and select later,
if there's enough time.
The sync package, I'll also come back to that,
but I can already say that it's using few taxes.
I think atomic actually didn't need any changes at all.
We were already using LVM in forensics for atomic instructions,
and so that just worked, no change needed.
And if you're other things like print a land,
you don't want multiple guaranteeing,
printing at the same time,
and like interleaving on the same line,
it's just going to be a mess,
so that looks the outputs,
then prints the whole line, then unlocks the output.
To explain everything,
I need to explain few taxes first.
So what's a few tax?
It stands for fast user space mutics,
but it's a system call, so it's not that fast,
and not a user space,
and also it's not really a new tax.
So I'm not sure where the name came from,
but you can build new taxes out of them.
Like there are basically the building block of
all the concurrency primitives,
like new taxes,
some of us, the way group,
and in go or channels,
basically everything you want to synchronize,
you can use a few taxes for.
So the way I would,
so the way it works,
basically,
if a lock for example is incontended,
like there's no other traps using it at the same time,
it's just an atomic instruction,
so it's really, really fast.
And only when it's content,
when they're multiple traps,
trying to use the same resource,
only then there's a system call being made.
So this is what a big reason why locks nowadays
are like super fast as long as you're not contented.
The API looks something like this.
It varies depending on the operating system,
I'll get to this later,
but this is like basically the lowest common denominates
across all operating systems.
The way you can imagine this is that the operating system
has an internal hash map of addresses or pointers
to a list of traps that are waiting.
And there are two important goals
or three depending on how you count.
Wait, in the kernel,
this is the pseudo code you can see here.
In the kernel, this something like this happens.
It checks whether the address that is passed
should point to a full atomic variable.
It checks whether it is the value that it expects.
And if so, it adds the calling threats
to the waiting threats on this specific address.
And the hash map I mentioned,
and if not, it just returns.
But if it matches, it also just pauses the threat
because that's all point of it.
And to unpause it,
another threat can call wake or wake one in this case
and say to the kernel,
please wake one threats that's waiting on this address.
Note that the operating system doesn't really,
like if you just change the value at the address,
nothing changes.
Remember, it's basically the address,
but the points are basically just a key
into the hash map inside the kernel.
So yeah, when you call wake,
it'll just wake one threats,
or when you call wake all,
it will wake all waiting threats.
And that's basically all the parameters you need
to implement basically all comprehensive parameters.
And this is how it looks on different operating systems.
On Linux, it's a single system call overloaded system call.
It does a lot of different things.
I'm not going to go into this,
check them in page.
On macOS there are two internal functions
that you can call.
They're not really documented anywhere.
However, LVM standard C++ library uses it internally,
so I guess it's not going to change ever.
Hopefully.
Let's apple this something stupid.
On Windows, there are three functions that are very diverse,
this part is.
And the WebAssembly there are also in the WebAssembly
and the WebAssembly is basically a subset of all the other operating systems,
which makes sense because WebAssembly does it.
In time ago, I've wrapped all these different APIs in this simple API.
If your text is basically just a superset of a atomic integer,
it's basically the same API as before.
Just wait, wake, and wake all just in a nice API.
And I found that like on bare metal,
I'm using almost the same API.
Well, the external API is all internal,
but like the exported functions are exactly the same.
So it still has an atomic value,
but because the timing goes basically the operating system.
It's running bar metal, so that's the only thing that's running there.
It can implement things however one.
So the few text type itself just has a link list of way to encourage things.
You can see here that for example, the weight function is basically the same code as before,
but now it uses pin lock for locking.
For example, the RP-2040 has a hardware spin lock for some reason.
It does have, yeah.
And this is how you actually use a few text.
To be clear, this is not the same implementation as standard code.
It's similar, but it's not the same.
And also, I skipped all the overflow checking here.
So this is not super safe, but it should work.
I didn't test for the cheaper.
This is the same methods as you would normally see on a few text.
Sorry, on a white group.
Add, wait, and done.
Done, and basically just adding one negative number.
So pretty simple.
Adding is actually because a few text is also a atomic variable.
It can just automatically add the delta,
and check whether the realt of the addition is zero.
For example, if the delta is negative.
And if it's zero, that means it's done,
and it can wake up all the weight in gravity.
So it signals every order of gravity into finish.
The weight function loads the current value in the atomic value.
Check what it's zero.
If it's zero, it's done.
It's ready.
It can return.
If it's not, it will wait.
But as you can see, it will pass the current counter value.
If it didn't, it would immediately return.
However, one note is that you can see that it's a for loop.
This is because the weight function can spiritually return.
This is an operating system limitation.
I think it can really spiritually return on like,
interrupt and stuff like that.
It's going to stop.
So it can sometimes happen.
So it needs to be in a loop until it finishes, but it's very low overhead.
Now channels.
Channels may seem a lot more difficult,
but actually channels and go are mostly implemented by just using a lot basically.
For example, to send the value on my channel,
happens is the sender first locks the channel.
It checks whether there's a receiver ready.
If so, it can precedes the channel operation.
If there's, if not there's, it checks whether there's a space in the buffer.
If it's a buffer channel and if so, it can serve the value and return.
And if not, it will need to wait.
So it adds itself to the linked list of weighting receivers.
It will unlock the channel and pause itself and wait for.
Yeah, the next sender.
Next receiver to use this channel.
So that's relatively straightforward.
It's still low-level synchronization, so not super straightforward,
but relatively straightforward.
Selectable forever is a lot more complicated.
So what you can see here is just an example of what can go wrong.
So say you have two guardians, full and bar,
and they both have a select statement.
So if you know the way select works is basically by locking every channel that is in the select statements.
But it has to do this, and when everything is locked,
it can check whether any of them can precede or add itself to the sender.
Severs list, et cetera, et cetera.
But if you do this to easily, you can see here the problem.
For example, guarding food, locks channel 1,
and then guarding bar, locks channel 2 at the same time.
And then guarding food, tries to lock channel 2, but it can,
because it's already locked, and guarding bar tries to lock channel 1,
but it's also blocked, so that's not going to work.
Main core implementation solves this by basically sorting all the channels.
Those channels are a reference type, I guess.
It's internally, it's a pointer.
So it can just sort by address.
And that's one solution, and if you have like many course and many select statements,
running at the same time, that's probably the fastest.
But for time you go, we have different trade-offs,
and I basically use a global new text for this.
Whenever a select is going on, it'll just lock it for a short while processing.
So that's mostly all the, I can go into new text,
but new text are surprising, complicated.
It's more complicated.
They are way more complicated than waiting for some reason.
It's like you want to optimize in case there's no content,
it's separate, not going to go into that.
So that's so far all the Linux stuff, and then.
But I also ported this to this go-for-bex.
This is actually running very bad code right now.
You can see here what's happening.
So I optimize this is a Mandelbrot fractal,
and I managed to run two different cover teams doing.
So the work is spread across two different course.
And so that the calculations coming down on Pearl,
because Mandelbrot's calculations are really complex to get,
get right.
And yeah, this tip is not that fast.
And by running them in parallel, you can see the difference.
I measured it to be about 30% faster.
There's probably some overhead, I don't know.
So that's right.
It's still all for quality, but I should be making a pull-up press somewhere
in the next week or so.
The way I implemented this is basically by running the scheduler,
the old scheduler that's used for single operations on both course
at the same time.
So both course are a little bit waiting for the next cover team
to arrive to be ready.
There's some stuff around sleeping, but that's fixed.
So that's where we are.
That's so far.
Next up, we need to clean up everything and merge.
Linux is almost ready, like some comments,
but it's basically ready to be merged.
Mac OS, I added support for, but there's some CIO issues.
It's also used as speedtrad, so it's very similar to Linux.
Bare metal needs a little bit more clean up,
but it goes quite terrible.
And then maybe I'll look into weapon assembly supports,
not sure where how many people are interested in that.
Maybe use speedtrad to supports.
The ease speedtrad to is not a chip that has two different course.
And of course, performance improvements.
So, but yeah, that can happen after the initial implementation is done.
So that's it.
applause.
Sadly, I have no time for questions, I'm really sorry,
but I need to catch up because of some things with something
of lightning talks, and one is usually famous for running out.
So, if you have questions for her, there is an amazing hallway track,
just give her the free drink of lunch choice.
Thank you for this amazing work in tiny go.
Thank you.