Passing function params via registers

Other misc things
Post Reply
User avatar
jorgegv
Well known member
Posts: 287
Joined: Wed Nov 18, 2020 5:08 pm

Passing function params via registers

Post by jorgegv »

Hi,

we all know the __z88dk_fastcall modifier for passing a single argument in HL, but I stomped on the following SDCC issue, it was an interesting read: https://sourceforge.net/p/sdcc/feature- ... 53/?page=1

In short: there is a (not so) new SDCC calling convention for passing several parameters in registers, following some rules. The calling convention is described in the SDCC manual, section 4.3.3.1.

That lead me to check if we were using an SDCC version which has this feature... and indeed we have! The main restriction is, of course, that we must use SDCC for compilation (and follow the rules).

I wrote the following stupid code to see what the generated asm looked like, and it really seems that it can be used in z88dk code, using __sdcccall(1) decorator:

Code: Select all

// compile: zcc +zx -c -compiler=sdcc --list --c-code-in-asm test.c

#include <stdint.h>

uint8_t rubbish8, rubbish8_2, rubbish8_3;
uint16_t rubbish16, rubbish16_2;
uint32_t rubbish32;

void fun_8( uint8_t p ) {
    rubbish8 = p;
}

void fun_8_fastcall( uint8_t p ) __z88dk_fastcall {
    rubbish8 = p;
}

void fun_16_fastcall( uint16_t p ) __z88dk_fastcall {
    rubbish16 = p;
}

void fun_32_fastcall( uint32_t p ) __z88dk_fastcall {
    rubbish32 = p;
}

// does not work, "error 222: invalid number of parameters for __z88dk_fastcall"
// void fun_8_8_fastcall( uint8_t p1, uint8_t p2 ) __z88dk_fastcall {
//     rubbish8 = p1;
//     rubbish8_2 = p2;
// }

void fun_8_8_sdcccall( uint8_t p1, uint8_t p2 ) __sdcccall(1) {
    rubbish8 = p1;
    rubbish8_2 = p2;
}

void fun_8_16_sdcccall( uint8_t p1, uint16_t p2 ) __sdcccall(1) {
    rubbish8 = p1;
    rubbish16 = p2;
}

void fun_16_8_sdcccall( uint16_t p1, uint8_t p2 ) __sdcccall(1) {
    rubbish16 = p1;
    rubbish8 = p2;
}

void fun_16_8_8_sdcccall( uint16_t p1, uint8_t p2, uint8_t p3 ) __sdcccall(1) {
    rubbish16 = p1;
    rubbish8 = p2;
    rubbish8_2 = p3;
}

void fun_8_16_8_8_sdcccall( uint8_t p1, uint16_t p2, uint8_t p3, uint8_t p4 ) __sdcccall(1) {
    rubbish8 = p1;
    rubbish16 = p2;
    rubbish8_2 = p3;
    rubbish8_3 = p4;
}

void fun_16_8_16_sdcccall( uint16_t p1, uint8_t p2, uint16_t p3 ) __sdcccall(1) {
    rubbish16 = p1;
    rubbish8 = p2;
    rubbish16_2 = p3;
}

void main( void ) {
    fun_8( 0x17 );
    fun_8_fastcall( 0x18 );
    fun_16_fastcall( 0x0019 );
    fun_32_fastcall( 0x00000020 );
    fun_8_8_sdcccall( 0x21, 0x22 );
    fun_8_16_sdcccall( 0x23, 0x0024 );
    fun_16_8_sdcccall( 0x0025, 0x26 );
    fun_16_8_8_sdcccall( 0x0027, 0x28, 0x29 );
    fun_8_16_8_8_sdcccall( 0x30, 0x0031, 0x32, 0x33 );
    fun_16_8_16_sdcccall( 0x0034, 0x35, 0x0036 );
}
So.... that's another tool for my bag of tricks! =D
DarkSchneider
Member
Posts: 71
Joined: Sun Apr 01, 2018 4:02 pm

Re: Passing function params via registers

Post by DarkSchneider »

I think z88dk should integrate some kind of method to load data into DEHL to take full advantage of the __z88dk_fastcall, much powerful than what could seem.
With it, we were be able to pass up to 32-bit of parameters in registers. But in C code we have no way to put them individually. Think how good could be to have a function like:

Code: Select all

void function(uint8_t l, uint8_t h, uint8_t e, uint8_t d) __z88dk_fastcall;
Or:

Code: Select all

void function(void *hl, void *de) __z88dk_fastcall;
So it could operate directly from registers.

Let's take the last one as example. A "trick" we can use is using structures:

Code: Select all

union hlde {
struct {uint16_t hl, de; };
uint32_t hlde;
}
So we can:

Code: Select all

union hlde var;
var.hl = data1; <- pointers
var.de = data2;

function(var.hlde); <- we send all data unified
But, as noticed, we need to create a variable and fill it, maybe losing sometimes the purpose of the fastcall itself. Would be nice if we could do something like:

Code: Select all

function(__hlde(__hl(data1), __de(data2)));
Then for readability we could create macros for using it.

Notice that we also need the corresponding __l(), __h(), __e(), __d() for loading 8-bit data for when required.
Timmy
Well known member
Posts: 392
Joined: Sat Mar 10, 2012 4:18 pm

Re: Passing function params via registers

Post by Timmy »

DarkSchneider wrote: Fri Mar 24, 2023 12:23 pm I think z88dk should integrate some kind of method to load data into DEHL to take full advantage of the __z88dk_fastcall, much powerful than what could seem.
With it, we were be able to pass up to 32-bit of parameters in registers. But in C code we have no way to put them individually. Think how good could be to have a function like:

Code: Select all

void function(uint8_t l, uint8_t h, uint8_t e, uint8_t d) __z88dk_fastcall;
Or:

Code: Select all

void function(void *hl, void *de) __z88dk_fastcall;
So it could operate directly from registers.
You must have seen our ealier thread on this, right? viewtopic.php?p=21320#p21320

The one difference here is that it's a CALLEE function instead of a FASTCALL.
DarkSchneider
Member
Posts: 71
Joined: Sun Apr 01, 2018 4:02 pm

Re: Passing function params via registers

Post by DarkSchneider »

But that difference is the main point, as a fastcall uses registers instead stack. Specially when calling ASM functions being able to pass up to 32-bit registers would be a great feature.
User avatar
jorgegv
Well known member
Posts: 287
Joined: Wed Nov 18, 2020 5:08 pm

Re: Passing function params via registers

Post by jorgegv »

The __sdcccall(1) convention allows up to 32 bits, with some restrictions on the types.
Timmy
Well known member
Posts: 392
Joined: Sat Mar 10, 2012 4:18 pm

Re: Passing function params via registers

Post by Timmy »

DarkSchneider wrote: Fri Mar 24, 2023 6:49 pm But that difference is the main point, as a fastcall uses registers instead stack. Specially when calling ASM functions being able to pass up to 32-bit registers would be a great feature.
Months ago, I had thought about this problem, when I wondered how this could be done.

And I came to the conclusion that this idea is not easily solved in C, at least without using a stack or some other intermediate storage in the background.

Random example: say there is a function c() with an invocation: "y = c (x, y, f(x,y,z), c(g(x),g(y),a,b));". And now it can be seen that it's not possible to just put the parameters in predefined registers, because the registers are still needed to produce the other parameters.

With just one parameter it's easy. With extra parameters, the parameter passings become a black box, and now the overhead is unpredictable.

There is a silver lining, you can still do these calls inside your own assembly code. In assembly, you can define your exact order perfectly.
DarkSchneider
Member
Posts: 71
Joined: Sun Apr 01, 2018 4:02 pm

Re: Passing function params via registers

Post by DarkSchneider »

Yes, that's why I think it should be focused in the passing parameters into registers directive (like __hl() and others). I doubt we could have something like that as calling convention at least in short term (if any), it requires to implement all that logic.
Then delegate in the programmer to use it and fit into the program.

I.e. we could have a function something like:

Code: Select all

uint16_t add(uint8_t a, uint16_t b) __z88dk_fastcall;
It is one of those confusing to pass into registers, but if this delegates in programmer then can do:
This function should be really something like:

Code: Select all

uint16_t _f_add(uint32_t ab) __z88dk_fastcall;
Then define the calling macro.

Code: Select all

#define add(a, b) _f_add(__hlde(__l(a), __h(0), __de(b))
So the programmer use the macro.
Then in the function can get data:

Code: Select all

struct params { uint8_t a, zero; uint16_t b; };
uint16_t _f_add(uint32_t ab) __z88dk_fastcall {
return (uint8_t)ab + ((struct params)ab).b;
}
Note: notice that for getting the low part we can simply cast to smaller data size.
Or, if allowed to use the registers directives mentioned:

Code: Select all

uint16_t _f_add(uint32_t ab) __z88dk_fastcall {
return __l() + __de(); // these ones for read
}
But for C compiler I see this more difficult to have.


Also, in any case, as you mention it would be useful to call ASM functions from C, as the ASM function can handle the data at DEHL much better at low level, but the problem is doing the call itself.
User avatar
dom
Well known member
Posts: 2072
Joined: Sun Jul 15, 2007 10:01 pm

Re: Passing function params via registers

Post by dom »

It's been a while since I looked at that feature, but I seem to remember that the initial benchmarks gained about 2-3% on pure C compiled code with sdcc.

If you're calling some assembler code, odds are that you've already gained more than that by writing it assembler rather than compiling it in the first place.

Writing the majority of the libraries in assembler is the route we went down in z88dk which does make it difficult to adapt to a new register based calling convention - the registers that parameters are passed in by the compiler may not actually be the best registers to use in the routine so as well as possibly ending up with shuffling at the calling site, you could end up with shuffling at the callee site.

If you only want this feature for infrequent calls (eg for BDOS), then ASMLib in <arch/z80.h> allows setting of the register for invocation of a particular address.
User avatar
feilipu
Member
Posts: 45
Joined: Tue Nov 15, 2016 5:02 am

Re: Passing function params via registers

Post by feilipu »

The discussion from about two years ago showed about 2% to best case 3% improvement for the new parameter passing method, as Dom remembered.

Discussion source for reference.
https://github.com/z88dk/z88dk/issues/1827
DarkSchneider
Member
Posts: 71
Joined: Sun Apr 01, 2018 4:02 pm

Re: Passing function params via registers

Post by DarkSchneider »

I recently found the intrinsic, that allow to load and store 16 bit values from/to address. Also the ones to return values using other registers.

Is not worth to change all the library, then maybe it could be achieved adding intrinsic, these are the ones to load values (instead passing address) into any (8 or) 16 bit register, , then the programmer could use them by its own convenience. Type:
void intrinsic_loadhl(address/value)
void intrinsic_loadde(address/value)
Etc.
And also for Classic as seems to be only for newlib.

Not sure if can be done or if interferes with the compiler like using inline asm.
Post Reply