Hello, Fozdom. I am Nandini Jomsanas. I am a software toolchain engineer at Ember Cozum.
I lead the Core 5 GNU toolchain project. I am also a UK electronic scholar from UK ESF.
UK ESF encourages young electronic scholars, students to study electronics and pursue a career in the sector.
UK ESF also connects top UK universities with leading employees.
In this talk, I will be giving you a tutorial on how to add a GCC built-in to the RISC-5 compiler.
Okay, so what is a built-in? Well, in C++ and C, there are two types of functions.
You've got your user defined functions and your built-in functions. User defined functions are functions that the programmer has defined within their code so they can use it.
But a built-in function are functions that are already implemented in the compiler.
So the programmer doesn't need to write specific code for it and can directly use these built-ins.
Many low-level architectures in GCC use built-ins.
Built-ins look superficially like any C function, but there are intrinsics to the compiler which are directly implemented within.
These built-ins have specific patterns to be matched in the machine description file and have access to unique individual machine functionalities.
Because they are integrated within GCC, they are more efficient than using just simple inline assembly.
For RISC-5, this presents an excellent opportunity to expose the ISA extension to C and C++ programmers.
This is an example of a simple built-in in GCC which takes the square root of a float.
There are tons and tons of GCC built-ins, but I don't know if you know, but there's probably like two in RISC-5.
And this is why I'm giving you a tutorial about it so we can add more.
It is important to say that yes, we call it a built-in function, but it's not really a function.
There are any corresponding entry or exit points and a just cannot be obtained.
Here is the square root float built-in that is implemented in GCC.
If you want to find it in GCC built-ins.dev, all of the source code will be linked at the end, so don't worry, I will give that to you.
And if you want to make a specific RISC-5 built-in, then you would go into the link below, or the path at the below, which will be in RISC-5 built-ins.cc.
Yes, I'm talking a lot about built-ins, we could simply just use inline assembly.
But this is why we shouldn't be using inline assembly.
If you want to use inline assembly, you have to annoyingly specify the pattern every single time you use inline assembly.
Sometimes you can get it wrong.
GCC does not know about this built-in, so there's a huge risk of data flow information being lost.
Again, GCC does not know about this instruction that you're using with inline assembly, so optimization cannot be used.
The reason we use built-in functions, well, all of your data flow information will be retained.
Patterns can be recognized and used elsewhere by GCC.
You only need to specify the pattern once, and that will be in the machine description file.
And then, voila, you just need to use your built-ins, put in the arguments, and the programmer will be fine.
And again, with built-in functions, they're implemented directly in the compiler.
So GCC will know about it and can use their optimization flags.
What do I talk about when I say optimization?
Well, GCC has a bunch of optimization flags.
Here are two that I'm currently using as an example.
The first one is with the flag minus 0.
I don't think that is. That's the basic level of optimization.
In fact, I don't think that's any optimization at all.
This is just hardcore assembly, which you will use for cv.er, which I'll explain later.
And when you use an optimization flag, minus 02, that will increase performance, reduce compilation time.
GCC optimizes those assembly instructions because it knows that it doesn't need to be used.
You might have noticed that I'm using cv.erw, probably wondering what the hell that is.
Well, cv.erw is part of cv3 to e4ep iso extensions, also core 5 iso extensions.
The cv.erw is part of event load extension.
We are currently implementing version 2 of this in Open Hardwares core 5 GCC and binutils.
The first set of extensions, the first set of versioning has the first five extensions,
and then version 2 has event load, SIMD and bit manipulation.
I would like to emphasize that all of these extensions and instructions are in binutils, the assembly and the linker.
But it's time to add built-ins in GCC.
I am going to be using event load for this tutorial.
This is because event load only has one instruction, so it's a very beginner-friendly task.
That instruction is cv.erw, which will load a word and cause the cv3 to e4ep process cycle to go into sleep state.
This is an instruction that GCC will not know about because it's very machine-specific.
Thus, we need a built-in.
Before we get into all of this, it is very important to call out the naming conventions of these built-ins.
A general convention name for a built-in in GCC will just be built-in and then the instruction name.
But if you want to make it a RISC-5 specific built-in, it will be built-in RISC-5, the vendor and the name.
For a core-5 specific one, it will be built-in RISC-5, cv4 core-5, the extension name and then the instruction name.
Yes, I understand it's a bit long-winded, but it is very important to emphasise which vendor, which architecture you want to use,
what extension, what instruction.
It just makes it a lot easier for the programmer to know which instructions they want to use.
So for my built-in, and if you want to use it, it will be called underscore underscore built-in underscore RISC-5,
underscore cv underscore aw underscore aw.
Because there's only one instruction, I just call it the same thing.
So this is an example of how to use this built-in.
This built-in will take a void pointer.
It will be loading it from a specific memory address and then loading it into a general-purpose register,
which is an unsigned day-to-bit integer.
From this example, yes, the only thing you'll have to do is just put in the pointer and it will return your unsigned day-to-bit integer value.
Can you speak a little louder, please?
Oh, okay, sorry.
Now that I've spoken about what event load is, it's time to add an extension to GCC.
So most of these implementation for adding an extension will be in RISC-5.common.cc.
So we've called our extension xcv, which will be the main extension,
and then you'll have a sub-extensions, which will be xcvew.
There isn't any ISO-specific class, so I'll just use a macro none,
and this will be the first version of it.
Because I am implementing a sub-extension, we'll have to imply it here by putting the sub-extension first
and then the main or parent extension.
Next, we add the corresponding masks and targets.
Before we do all of this, we need to go into RISC-5.opt to emphasize or add the target variable and the corresponding core five flags.
This file is very sensitive, and so you'll have to, even though it's two lines, if you mess it up, then you've got GCC crashing everywhere.
So you have to be very careful in this file, and then you use that flag for your corresponding target,
but you also use it when you have to specify your GCC options.
So I've done that in RISC-5.common.cc, which is here.
Now it gets into the interesting stuff to actually define the built-in.
RISC-5 has a function already made for us, so we can make these built-ins.
That is in RISC-5's built-ins.cc.
It takes in five arguments, and I'll be going through all of these in the following slides.
That'll be the instruction name, the built-in name, built-in type, function type, and availability predicate.
So using this function, I have created my own file, which is called corev.dev,
and this is where all the corev-related built-ins will be in.
My first built-in will be in corev.dev, and the name of the instruction name will be C-V-E-L-W-S-I for single integer.
The name of the built-in that the programmers will be using will be C-V-E-L-W-E-L-W,
but that will be expanded to built-in RISC-5.
Then you've got the corresponding built-in types, function types, availability predicate, and I'll go into that more.
So the instruction patterns, this is probably the most difficult part of the whole built-in implementation.
So the insert name is the name of the associated instruction pattern in the machine description file.
It uses, it takes in five operands, but the last operand is optional,
but I recommend you putting in if you can.
You've got the name, you've got the RTL template, conditions, output template, and instant attributes,
and that will be all in RISC-5.md, but I will be creating my own md for corev-specific,
so we don't merge it into RISC-5.md.
So this is an example of RTL templates or register transfer language.
It's a template that is very, very similar to intermediate representation that GCC uses.
It's a template that GCC will take and then put in the corresponding registers or operands that it needs to do.
So this is my instruction pattern that I will be using for this built-in.
The name will be RISC-5 underscore CV as we've previously defined it.
I am using the set pattern and this will take a destination register and a source register.
The destination, I think, this will be the destination register, the first operand,
and I've used the match operand pattern which will take m as machine mode and the index of this operand,
the predicate and the constraint.
The machine mode for this will be SI which is a single integer, it's 32 bits.
It's zero for the index of this operand.
We usually start with zero as the indexing.
The predicate for this will be a register operand as we'll be loading it into a general purpose register
and the constraint will be R emphasizing as register equals to meaning it's going to be written to.
Next part of this is the source register which will be the memory specific address.
So we're using mem to specify the size of the object being referenced.
SI being single integer, 32 bits.
Again, we're using match operand to match the register or the pointer to the specific address.
The index number will be one because that's the next number.
I am using an address operand and then p specifying as pointer.
I am using an unspect volatile for this instruction because it's a volatile operation.
It's very machine specific.
It can get difficult and there are times where it could be trapped.
We are referencing in this state that is fragile and vulnerable so that is why I've been using an unspect volatile.
Now that I've talked about the RTL pattern, we talk about the condition.
The condition is important to add so that the instruction can only be generated within these conditions.
You can only generate this pattern if the target is to X call VELW and that it's not a 64 bit target.
Next we talk about the orange bit which is the output template.
The output template will be what you will see in the assembly.
You define it with the instruction name so cv.el and then slash t for tad.
This is where you use those index numbers to reference which operands you want to use.
I will be referencing %0 and then %a1.
%0 will be the destination register and %1 will be the source register.
I am using %a to substitute as a memory reference.
Lastly we talk about the optional operand but again this is something we should try to put in if you are going to add a built-in.
We want to tell GCC that this is a load type of instruction and the mode is SI throughout the whole built-in.
The reason I have added this optional operand is that the instruction can still be generated but GCC can now optimise it knowing that it is a load, knowing that it is in machine mode SI.
That is now the big part of the built-in.
We have discussed the instant name and the template name.
Here it comes to the built-in types.
In RISC-5 there are currently only two types of built-in types.
Those built-in types can be found in RISC-5 built-ins.cc.
This is RISC-5 built-in direct and RISC-5 built-in direct no target.
RISC-5 built-in direct corresponds directly to a machine pattern we have just created whereas RISC-5 built-in direct no target does the same thing but the return type will be void.
But we are returning a general register operand or theta bit unsigned integer.
So we will be using RISC-5 built-in direct.
Next comes the function types.
And again, everything is in RISC-5 built-ins.cc.
And currently there are only two types of prototypes for RISC-5.
You can only return.
You can only have a returning type.
You can only have a return type and one argument.
In coming presentations I will be talking about it a bit more because I only have 45 minutes to talk about this presentation.
When it comes to defining which return types and argument types we are using that will be in RISC-5-f types.dev.
So the comment says that it will expand to RISC-5 underscore unsigned integer and then avoid pointer because that's what I will be using for my built-in type.
Lastly we have the availability predicate.
This is very similar to the conditions we had in the RTL template.
So we use this avail function that has been declared in RISC-5 built-ins.cc.
It takes the name of your availability predicate and then the corresponding conditions.
As you can see it's very similar to the condition we had in the RTL template which is a target reference and then it's not a 64-bit target.
Now that we've added the extension and the instruction and the built-in it's time to test it.
And this is a very simple test just to make sure that it works.
It's a compilation test.
It takes in a void pointer with an offset.
It returns an unsigned 32-bit value.
You can see there are comments on the side.
These are deja vu comments.
We are using deja vu because we want to use a simulator or it can be used on microcontrollers.
It's a framework testing model that we use for our test scripts.
The first comment we'll talk about telling it it can be an execution or a compilation test.
So this will be a compilation test because we haven't got an executable target yet.
The second line is to tell you the options for this built-in.
If you don't specify the options then this test won't run because this instruction only works within X core VELW.
And then the last line or the last comment will be for checking if our instruction has been generated in the assembly.
And it should be generated once.
There are dashes to escape.
It's very sensitive because it's a regular expression type of framework.
We've got a run script for this.
It's very important to build GCC because I've been running tests without building GCC and wondering why it doesn't work.
And it wasn't until our GCC experts told us, no, you've got a run build.
You have to run GCC and then run it.
So this shows the results from our run test scripts.
Although it's just one test, there are 18 passes.
That is because it goes through nine optimization levels.
The optimization level goes through a scan assembly test and then a compilation test.
Like I promised, I put up the slides for where all of this will be found.
This will be found in GitHub's Open Hardware Core 5 Vinutils and Core 5 GCC.
This is also part of the Open Hardware group.
We are still looking for volunteers and people to contribute to this project.
And it's very important to also mention the GCC internals manual.
It's probably the guru of GCC.
That's what I rely on the most now.
Thank you for listening to my presentation. Do you have any questions?
Yes?
I have a question.
So I know that these built-in functions are used by the code people, which I think is what came before the Core 5 project, right?
I think they use it for various mathematical functions to speed them up.
I was just wondering, what I'm interested in, what I'm working on, is using higher level compilers to compile into automatically generated kernels.
What's not clear to me right now is that if I use a built-in, then I would need to compile to a C code, right?
Is there any way that you can still reuse part of this work without having to use C code, or would you always need to go to C code?
For now, I've just been using C code, so I'm not really sure.
I don't know.
If you've got a fault, I'm fine.
There's the C API, so you can sort of wire it into it.
This is in the compiler, so you just need to find your own code to reach to the client.
So in this case, you would also use these things in Fortran code.
You could, yeah.
I have an amazing that myself, I've been working with the staff, so there's no reason for this not to work.
It's expressed in terms of a C code, so it has to be expressed somehow.
I was a bit confused more about the built-in concept in general,
because I mean, usually people use C code to not be machine specific,
but if you use it like a built-in, then you become machine specific, right?
Yeah.
Oh, yeah.
It depends on the built-in.
GCC has built-ins that are sort of general.
I mean, like all the maths functions, for example, like a body of maths, it's not machine specific.
And it says, obviously, compiler specific.
It's not that specific in this case, yeah, but because you can have other kind of other mathematics.
Yeah.
Okay, at least architecture specific, right?
Well, actually it is not architecture specific.
It's a general.
Yeah, but even for mathematics built-in functions, you always have,
not always, but mostly, yeah, kind of architecture specific.
Oh, yeah, there can be stuff like encoding of numbers or such like.
Yeah.
It's a sort of, you know, just because.
So it should work, yeah.
Actually, that's one way to avoid these architecture specific.
Like, rather than encoding a non-pattern into your code, just by using a constant or bit pattern
and then sort of casting to proper floating point type, you can use built-in non.
It's a built-in function that produces the correct encoding of a non for your target.
Okay, thank you for listening to my presentation.
Thank you.
For me.
Thank you.