So I'd like to start with three numbers. 26.9 million, as reported by Wikipedia, is the number
of software engineers that were in the world in 2022.
10174 is the number of commits to the GCC code base between GCC 13.1 and GCC 14.1.
And 49 is the number of contributors to that work who contributed more than once a week on average.
So each of them's response would for about half a million software engineers.
And the purpose I'm Jeremy Bennett, I run in because I'm in this talk.
I'm going to show you about one aspect of GCC development.
In the hope that in time you'll be able to add to that number and the load on those 49 can be shared a bit wider.
I hope when you get to the end of this talk, you'll have an idea about building functions and how you could add them to GCC to improve GCC's performance.
So just to put the context, here's all the bits that make up a good new tool chain, but we're going to worry about the compiler today.
And we're going to use as reference support for a derivative of risk 5, the core 5, which comes from the open hardware foundation.
And that adds eight custom instruction extensions adding a few hundred instructions to the architecture.
And we're going to look at how we initially support some of those using built-in functions.
So first of all, what's a built-in function?
And the answer is it looks like a regular C function, but it's important to understand it isn't an actual function.
It's actually built into the compiler, the compiler understands it.
And it's represented not by writing it in C code, but as patterns within the pattern match of GCC.
And they're used for a number of reasons to access unique functionality, which doesn't map to a programming language.
And we'll look at initially an example like that.
But in the short term, when particularly with risk 5, you add something to the hardware, it's a quick way of exposing that functionality before you get to the full blown generating the code automatically from the C programming language.
And as I say, they're called built-in functions.
They are not functions. You can't take their address or pass them around as pointers as over.
Here's an example, the built-in square root function.
Okay, that's a standard pass. See, every architecture provides it.
And it's designed to do square root, not using the standard library way of doing it, which is well established works on a broad range of architectures.
But it says, please do square root as efficiently as you can on my architecture.
And if your architecture is something like an old vax that has a square root instruction, then you can do it in a single instruction.
Okay.
So there's an example of a built-in function.
And it's built-in, so you don't need a header.
The standard built-in, you can find inside the GCC source in the built-ins.dev function.
And here we are, there's the spec of built-in square root function.
It's got a bit saying what the arguments are and what it's returned is.
It's a built-in function, float float means it takes a float argument.
It returns a float and it's got some attributes something to do with match rounding there.
Custom built-ins, going your architecture specific configuration.
So with that we'll be in config slash risk five for risk five.
So we've got config risk five, risk five.
Hive and built-ins, actually dot cc.
That's the c code, but there's deaths as well.
Okay.
Why don't you just use in line assembly code?
And here's an example.
I've picked one instruction.
This is from Core 5.
It's the external load word.
It looks like a load, but it's used to load between cores.
You're trying to synchronization.
It's actually a synchronization instruction.
Okay.
And the problem with the assembly function is it's a bit of a black box.
The compiler knows to put it in here.
You tell it a bit about what might be where the arguments are expected to go.
And anything that might get clubbed.
But it really does spoil your data flow through the program because you don't know much else.
And the compiler must take a cautious view.
Okay.
When you do built-in functions and it's a black box, that instruction will appear.
Come what may.
Okay.
It's nothing to be done about it.
Built-in functions offer some advantages because they do understand the data flow properly.
And the patterns they have can be used and recognized and used elsewhere in gcc.
So unlike to be for this instruction.
But for other instructions you may see, oh, that's a pattern I can fit it in somewhere else.
So you get a bit of code generation coming out.
And they're generally available to be optimized.
And we'll look at an example of that in a moment.
So let's look at implementing a simple built-in.
So the event load is like event load where I'm not external load.
Event load word.
So it's a synchronization load that you use to synchronize multiple cores in a multi-fix.
So you'd never generate it from C.
So you need to have a built-in function to generate it.
And it looks like a typical load function.
You've got a destination register and a constant index on a base register.
And here's how you'd write that built-in in the source code.
You'd say I'd call built-in, Rys5C, the ELW and then the address, which is presumably a semaphore.
Okay.
And with those zero, it would generate that code.
You may as well have used in line assembly.
However, if I turn on O2, the compiler understands what's going on in the patterns.
It's able to up to Rys round it and give me much more efficient code.
I was doing this within line assembly, it would struggle to do that.
Naming built-ins, there's a convention for naming them.
If you're doing for your architecture something that's already used as a standard built-in,
across all our architectures use that.
But typically, you name it in an architecture specific way to avoid confusing things.
So for Rys5, the credentials and score built-in, underscore Rys5, underscore vendor name.
So in that case, CV for score 5.
And then, in our case, ISR extension, because we've got multiple ISR extensions and then the name.
So hence, ELW, ELW, because the ISR extension is ELW and the instruction is a ELW.
And there we are, and specification is that, and there's the example we saw earlier.
So, let's go and look inside Rys5 built-in.cc.
And we've got a macro called Define Rys5 built-in, which is sitting on top of some standard stuff here.
And we're going to tell it, the pattern in the machine description file,
and we'll come to that in a moment.
The name of the built-in, where we've already talked about, in the built-in type,
is it a built-in direct, or there's a whole load of them there.
You can have there.
And the return type and argument types.
So in this case, it's a void return.
No, it isn't a void.
It's got a returns of value, but it takes a void argument.
Okay, and the name of the availability predicate.
That's because this instruction is only available with 34 bit, Rys5, but not 32 bit, but not 64 bit.
So you have a general availability, is it available?
So, that's what we've got there.
This is a direct built-in.
Okay?
Our instance is the import one, and this is what I'm going to focus on.
This is the instruction pattern.
This is how the whole of GCC is driven through these patterns,
written in a list by a list-like language.
And you basically have where you define an instruction,
say what the name of that instruction.
That's the name we put in the macro earlier.
An RTL template that tells it what it's looking for and try to fit to any conditions.
An output template to tell it how to generate the assembler and any attributes.
And they are in, for Core 5, which is at their own side, the Rys5 convict, but they're Core 5.MD for machine description.
So here we've got here's the event load word built-in.
Let's take you through the various bits there.
Okay?
What happened there?
There we go there.
There we go.
But the name.
So there's the name.
There's the RTL template, we'll come back to that in a minute.
There's a condition.
So that extension, a better being enabled, and it's 64 bit.
There's the output template.
That does look a bit like an assembly instruction.
And there there's the attributes at the bottom.
So let's go into a bit more detail.
The RTL template.
Okay?
So this is saying, this is logically what's going on.
It's a set instruction.
I'm setting something from memory.
Okay?
It's, and I'm trying to match the different operands.
So the first one I've got is set something where the address is.
Something to store about, and then what you want to store at that address.
Feel it.
Go through and look a bit.
Okay?
So first of all, this is where we're saying where we want to store.
Okay?
We're saying set, and the thing I'm going to set, it's SI single integer.
It's 32 bits wide.
SI is what GCC is in.
It's operands zero.
They count from zero.
It's a register or a plan, because it has to be a register.
And it's equals our, what to mean?
R is it's a register operand equal before it says being written.
Okay?
And then we'll come back to the aspect of volatile in a minute.
But then we're talking about where are we going to get the second operand.
So it's operand.
It's 32 bits.
It's integer, but the address.
Remember we showed it was index on a base register.
And I've type P, and that's for a pointer.
P for a pointer.
Okay?
And the use of that mem in front of SI says dereffancy.
It's a right value, not a left value.
And lastly, we've got, let's come back to this unspec volatile.
Unspec volatile indicates a side effect.
Now, of course, in this side effect is to do with synchronizing architectures.
Okay?
And we put at the end of it a bit more detail.
And that's just allow us to divide these up particular architectures.
Okay?
There's no real attempt, because it has side effects.
We see won't try and say, oh, I've spotted this sort of pattern in general code and put it in there.
Okay?
So now let's have a look at the condition.
That's very simple.
The condition just says, I've got to have this extension enabled.
And I'm not interested in 64 bit architectures.
That's just to see, there's just to see expression.
Okay?
Those are hash defined in the spec of this five.
Okay?
And then we come to, oh.
The output template.
Okay?
And that's just a string or fragment of code returning string.
And the operands, remember, we numbered those operands north for the thing I was setting.
One for the actual address.
I was getting it from.
Those are referred to with percent.
Okay?
So percent zero.
That's the thing I'm trying to send to the right.
And percent one.
That's the op-amp.
But I've qualified it with an eight sets and a dress operand.
So build something that looks like an address operand.
Okay?
And that and the attributes, they're mostly other effects.
You only really need to worry about the attributes.
I've put some places.
The mode is, sites of 32 bit behavior, and it's a load type.
Honestly, for initial plugin you don't have to worry about that.
So there are more for optimization passes, which we're not going to go into today.
So, and then we come back and you see we put everything together and that's where we started
off.
That's the macro we call and it's referring to this instance and the associated instruction.
So, I say the two big ones are built in direct and built in direct no target.
If I correct, no target is effectively void functions.
So, the return and argument types, the built in.
So, these are built up by a complex set of macros where what you're trying to do is just
get a set of macros that generate to say what the type of each argument is and they construct.
So, if you look here, if we can go back, yeah, we're trying to get these function type
is the return type and arguments, okay.
So, if we go on fruit, there we are, you'll see we've defined a type here which is, we've
got types here for having unsigned single integer, then a void return and unsigned integer,
unsigned integer void points are in so forth.
So, lastly, we talked about the availability predicate and we've got some examples here,
see the early W, which is the target and it's not 64 bit.
So, we end up there.
So, here is the constructed type, the constructed type is it's USI, unsigned 32 bit and the
function type is it takes, so that's the return and it takes a void pointer as argument.
It's rather the cunkey, but it's sort of living within the framework we've got the NGCC.
All you're trying to say is it takes in returns 32 bit integer and it takes a void pointer.
And the predicate, we've put their CVLW, that's the check it's got the isre extension enabled
and that it's not 64 bit.
So, that's actually all you need to do and all this stuff is freely available in the call 5 upstream.
And the biggest problem there is going to be getting the hang of the patterns that you're putting together.
Let's show you a bit more about those patterns.
What happens here, if I've got two assembly functions in the assembly, okay, what to do
is scale or add across a SIMD register, okay.
One where it's add, you've got RS1, it's a SIMD, so potentially four bytes and you can
either add a constant from RS2 or you can add an immediate constant, so two instructions.
But we don't have two plugins, we've got a single built in, until the single built in,
which says here's the thing I want to add using this built in and here's this argument.
Now, is that something I need to put in a register or is it something that I can do as an
immediate?
And the answer is if you call this built in, remember it's not really a function, I can look
at exactly what's being put there.
If you give me a constant argument, that will fit into a six bit signed integer, then I'll
generate the second instruction, but if it's not, I'll put it in a register and use the
first instruction, okay.
So how do we do that?
Well, the answer is we've got the same defining the built in, let's skip over that.
But the answer is in the pattern here, because you'll see the register operand, now I've
got not, it's a register, but it's a register comma register, okay.
So actually, I'm going to specify a pair here and then on the operand, I've said the
first operand, operand one, that's a register register, so that's my input.
And then my constant is either a six-bit constant that I can use and see V6 is a constant
spec, I've done it because there isn't a standard one for it, or it's a register, okay.
And this pattern match will look at the code as it comes through as it's going through
the compiler and say, oh, does it match a six-bit constant for that second operand, if so,
I will treat this as though it's R, R, CV6, and if it doesn't, I'm going to have to put
in, I'll treat it as R, R, R, okay.
And then going down when we look at the pattern, you'll see that we've got two patterns
there.
If I match the first of the pairs, I'll take the first pattern, and if I match the second
of the pairs, I'll take the second patterns of the first pair, will give me the constant
version, the second pair will give me the load into a red straw brand or a red straw
version.
Okay.
So, as I say, there we are, let's go through.
No, sorry.
So, there we are.
Now, you'll notice here, I don't like, I need to say something, well, this could be a six-bit
integer register.
So, I need a special thing to say, is it a six-bit, it can be either, that's in the
success operand, and as I've got the CV6.
So, what we find is in the success, so it's either a constant or it's a register, and
the constant we define it as, actually, it's got to be a constant integer, and it's got
to be in that range.
So that's how you write a custom constant there.
And, as I say, the output template, there's two, so the first one for the first set
of things, and the second for the second set of constraints.
And so, here's an example here, the first one here, I've got a variable x.
It's got to generate the register version of the operand, okay.
The second version, I've just given it a constant argument, and it goes, oh, that's 28,
that fits in a CV6, a six-bit assigned constant, so I can generate the six-bit here.
And remember, this is sitting in the infrastructure GCC, I didn't have to do anything about
telling it, put the value in registers.
It knows it's got to do that, the register allocated also, sort that all out.
So putting it in a register all happens for me automatically.
We use in arguments, the multiplier accumulating structure actually uses its first operand,
is also, it's both the destination and one of the operation, because it's doing a multiplier,
it's accumulating.
So you notice here, in this example here, oh, sorry, I've got operand 0, operand 1, the second
operand 2, because it's a three-operand instruction, the third operand, though, you'll notice
its type is not anything, it's just 0, it's the same as operand 0, and that's how you specify
duplication.
And then the pattern is 0, 1, 2, and of the actual first operand, in that instruction,
is used, both to supply the base thing you're adding to and is the result.
Okay, so what next?
So if you want to look at this, because in 25 minutes you have to go through quite fast,
I've given you some basic things.
The source codes there in the open hardware group get repositories, much of that's now
going upstream, so you can find it in upstream.
The open hardware group, which is now the open hardware foundation, but that should still
redirect, because it's become a part of eclipse this month.
And lastly, this is your Bible.
All of this is in the GCC internals manual, and that will tell you how to do it all.
Getting a use, as a user, you can just download the tool chains and use them as a developer.
If you want to work on open hardware group, you can join the tools channel as a monthly
training meeting for engineers, and you can submit your patches against the development
branch, if it's out of tree, some of it's now upstream, you can then just put them in
as ordinary GCC patches.
And lastly, acknowledgements, I didn't do this all on my own, there's a team behind me who's
done this.
There is something very unusual about this slide, can anyone spot what it is?
Well, a Charlie might be upset about that, but you're quite right, yeah, Charlie is actually
not a good whole Charlie, it's a boy Charlie, but yeah, exactly, so much this, the team
worked on this, as it happened, they weren't chosen for this purpose, but they happened
to be almost all not male, so thank you very much, and I will take any questions.
Yes.
If I have a height of a gaming console that has a floating-point dart product approximation,
yes.
What?
It takes him from fixed registers and certain numbers, would that be a problem?
So, if you have a hypothetical gaming console that had a floating-point instruction,
I can't remember what the instruction was, dot product instruction, but actually the
op-ants had to be in specific registers, that shouldn't be a problem, because if you look
back here, when I did my custom constraints, where did I get it here, the CV6, now there
I was constraining a register or a particular constant, I think I will look round because
people here and know this probably better than me, you could write your own constraint
to say what the registers were, the permitted registers were there, and hopefully the
registration would then try and get your op-lands into the right place, so you didn't
act yet and up with the blow-up on the register allocation, but yeah, you could just be able
to constrain.
Yeah.
You could also force it into an actual register directly.
So, you can just say, it only works in this register and just say, it's that register.
No, it's not, yeah, it's nice if you can let the compiler do all the hard work, yeah.
Here, over here.
For the same dot product instruction, how do you be able to say that the resident register
can not be used for the next 15 cycles or something like that, so the question is,
the dot product instruction, the hypothetical we've just heard at, how would you then extend
the pattern say, and by the way, having used it, you cannot use this register again
for another 15 cycles.
You can make a scheduling distribution for that matter.
Right, so it can be done, and the answer is, you need to make a scheduling description, and
then we get into the whole question of how GCC they're scheduling, and that's in, that's
in the next tutorial about about 27, but there are people in this room who can point
you in the right direction.
Well, I'll get the door, right?
Any more questions?
Yes, I did have another one, but the term is whether the building is used, if it's in a proximation,
do you typically have to have fast math enabled for, if you choose the built-in, and you
don't have fast math, would you expect it to use an approximate built-in?
So the built-in is, so the question is, are the constraints that say you must have something
for the built-in to be run, and if you look at the built-in here, when I call the built-in
is a particular name, if I say I want to use that built-in, that built-in will be used.
If you've got an exact version and a proximate version, then either it's a very complex
built-in that may be is able to work out which to use, a bit like I worked out whether to do
the constant or the register, which will be run possibly, or you have two built-ins built-in
exact and built-in approximate. You could do it either way round, I think it would be there.
Any other questions?
How would you go about implementing a lot of instruction level built-in, but something that affects
the more the language for that, so you've got to do a constant feature that's a special point of time?
All right, I think those are probably, so the question is, how would you go about doing something
to do much higher level in a built-in, perhaps looking at the front end and the different
function type? I think you're out of built-in technology there, but you can have built-ins that are
quite high level, that may be quite complex instruction generation. I think if you're looking at
about changing the type system, you're probably looking at other parts of the compiler,
you're probably not doing it with a built-in, that would be my, and very likely attributes are
probably your friend in this case, okay?
Yeah, yeah, it's fair comment, it is front end thing, and this built-ins are a back end technology,
okay?
So you want global pointers that are, have the addresses included because of the relative
of addresses and so, absolutely addresses, which are definitely also needed to touch something
in a little bit. So you want global pointers, but how you generate them depends?
Positioning independent point. Positioning independent point is, I think that sounds like using
type constraints, all the stuff that's in the embedded extensions to see,
sort of helps you in that space, and that's sort of all fun-tending and stuff. I don't think the
built-ins built-ins may then use that when it percolates through to choose what code they generate,
but I don't think you can do it just with built-ins. And I've run out of time. Thank you all very much.
These tutorials get improved regularly, so don't hesitate to send me email and say,
you could make it better by doing something, and then next time I give it, it will get better.