Menu

#79 Register parameter passing

open
nobody
None
5
2024-07-09
2004-06-25
Stephen
No

First, well done on a great product, to all those who
have worked on the compiler.

I have used Keil and HiTech compilers for an 8052, and
both use the registers to pass parameters to functions.
My project which compiles using the Keil compiler is 96
bytes short of data when compiled using SDCC because
most of the parameters are passed in data space.

Have you considered using the registers to pass
parameters as part of the optimisation of data space?
or allowing this as an option?

Related

Feature Requests: #253

Discussion

  • Bernhard Held

    Bernhard Held - 2004-06-25

    Logged In: YES
    user_id=203539

    What about --parms-in-bank1?

     
  • Stephen

    Stephen - 2004-06-26

    Logged In: YES
    user_id=1071031

    I didn't know about that option.
    Helps with data optimisation, but I see the libraries need to
    be re-compiled.
    Would it not make sense to use the active register bank to
    pass some of the parameters because often the parameters
    are being used in the registers. They are copied into bank 1
    to be passed as parameters, and then the called function
    moves them back into registers to use.

     
  • Bernhard Held

    Bernhard Held - 2004-06-28

    Logged In: YES
    user_id=203539

    At the moment the register allocator is not clever enough to
    efficiently deal with parameters in registers.
    In general the benefit from register parameters is often
    overestimated. It certainly makes sense on CPUs with plenty
    of large registers. On a 8051 tiny functions will look much
    better. But in larger or complex functions you'll often find that
    the registers must be saved immediately after the entrance
    code. In many cases it's better to let the register allocator
    decide which variables should be kept in registers. It would be
    a big performance drawback, if e.g. an automatic variable
    used as a loop counter is located on the stack, because the
    parameters eat up all the register space.

     
  • Frieder Ferlemann

    sample code using different parameter passing methods

     
  • Frieder Ferlemann

    Logged In: YES
    user_id=589052
    Originator: NO

    Hi,

    I also see SDCC at a disadvantage versus other compilers
    because of SDCC's parameter passing via DPTR.
    Access to DPL needs one byte more than access to
    register R0..R7 or A.

    This adds up... In the functions within the
    appended file ("parameters.c") the caller prepares,
    the callee accesses, the callee sets up return value,
    and caller stores the returned value...

    For small function bodies the disadvantage of
    using DPTR can be substantial (more than
    40% longer code in the constructed sample code.)

    This parameter passing (and returning) scheme
    is probably noticeably better adapted to the 8051?

    a) 1 byte: A
    b) 2 byte: R6(LSB), R7(MSB)
    c) 2 byte: DPTR (xdata/code pointer)
    d) 3 byte: B,DPTR (generic pointer)
    e) 4 byte: R4(LSB)..R7(MSB)

    The advantage of a) is evident.
    (13 versus 20 bytes in the constructed example)

    Point b) probably does not (yet) fall into the
    category which Bernhard mentioned (parameters
    eating up register space).
    (24 versus 35 bytes in the constructed example)

    Point c) and d) is currently in use and well
    adapted to pointers.

    Point e) is (for more complex functions) in the
    category which Bernhard mentioned. Yet it would
    be (3 byte) shorter to put R4..R7 out of the way
    than it is to move DPTR,B,A out of the way.
    (And one doesn't have to (whereas DPTR,B,A
    would usually have to be saved if the parameter
    is to be used later)).
    (37 versus 54 bytes in the constructed example)

    File Added: parameters.c

     
  • Philipp Klaus Krause

    Logged In: YES
    user_id=564030
    Originator: NO

    How about letting the user decide? The c standard allows the use of the register storage class specififer for function arguments.

     
  • Philipp Klaus Krause

    While the standard allows to use the storage class specifier it doesn't allow passing the argument in a different way depending on it's presence.

    Philipp

     
  • Konstantin Kim

    Konstantin Kim - 2020-07-30

    The topic is still relevant

     
  • Oleg Endo

    Oleg Endo - 2023-10-21

    When looking at some of my larger firmware running on mcs51, yes, this is a real bummer. mcs51 is not good at accessing variables on the stack. And that tends to accumulate as code bloat. In some cases I even resort to passing gptr as void* or uint32_t just to avoid ferrying the args through the stack. The stack calling also prevents tail call optimizations, or just simple functions that only forward arguments to another function (common for wrappers/ adapter functions).

    Although DPTR is convenient for passing 16-bit values, especially constants, its use in arguments prohibits using 'jmp @a+dptr' for pointer-to-function calls/jumps.

    I'd propose something like this (inspired by other RISC type ABIs):

    • call clobbered registers: r0-r3, a, b, dptr, carry flag
    • call preserved registers: r4-r7, psw bank select bits
    • function args and return value in r0-r3
      • stuff as many args as possible into registers. e.g. 4x uint8_t or 2x uint16_t or 1x uint32_t etc
      • small structs <= 4 bytes passed by value in registers
      • whatever does not fit into registers, goes onto the stack. e.g. uint64_t would have the first 4 bytes in registers and the last 4 bytes on the stack
    • bool return type should be automatically converted to bit and returned in the carry flag
    • banked calls should pass the call pointer in [a:dph:dpl] or [b:dph:dpl]instead of [r2:r1:r0]
      • loading dptr with lower 16-bit constant address is most efficient
      • a register is usually needed to implement bank switching
    • a register usually holds the computation result, so it might be better to use it as the first register for argument/return value.
    • va_args should be passed entirely on the stack
      • but e.g. (uint8_t x, uint8_t y, ...) would be passed in (r0, r1, stack)
     
    👍
    2
  • Philipp Klaus Krause

    IMO, the best way to approach this would be:

    1) Do the big overhaul of the mcs51 port that includes switching to the new register allocator.
    2) Make codegen flexible wrt. calling conventions (e.g. introduce the aopArg / aopRet / isFuncCalleeStackCleanup mechanism we have for some other ports to mcs51).
    3) Run experiments to get a lot of data that will help find a good calling convention (i.e. something like https://arxiv.org/abs/2112.01397, but for mcs51 and the then-new mcs51-derived ports).
    4) Decide on the new calling convention.

    Breaking the ABI inconveniences users, so we shouldn't do it often, so we better do it right the first time.

     

    Last edit: Philipp Klaus Krause 2023-10-21
  • Oleg Endo

    Oleg Endo - 2023-10-21

    I do have a situation where newer ABI code would need to call the older ABI code (for some compatibility reason). It'd be good if the backend was a bit more flexible regarding handling of different calling conventions and could eventually support both (new + old). It kinda does already ... with e.g. callee_saves and also special libcalls.

    The fact that register based calling conventions are generally are a good thing (given enough registers) is actually nothing new, I think.

    For Z80, we found that having 8-bit return values in a, 16-bit return values in de, 32-bit return values in hlde worked best, a calling convention very close to the one SDCC used for SM83. For the arguments, the best choice was to have the first argument in a if it has 8 bits, in

    This is pretty much what I have also observed on mcs51, just looking through the generated code of larger firmware projects. Since the output of computations is usually in 'a', using that as a return value / first 8-bit argument seems natural. However, 'a' is also often used for calculating pointers (on the stack frame etc.) so having it free might be actually better. Would be worth an experiment to compare both approaches, but sounds like a lot of work.

     
    • Philipp Klaus Krause

      For the ports (stm8, z80 and related) where the ABI was changed to a more efficient calling convention, this is possible: assuming the default is the new ABI, all declarations of the old-ABI function will need to have __sdcccall(0) added (also all function pointers to old-ABI functions), then the old ABI will be used when calling them.

       
  • Oleg Endo

    Oleg Endo - 2023-10-21

    assuming the default is the new ABI, all declarations of the old-ABI function will need to have __sdcccall(0) added (also all function pointers to old-ABI functions)

    Sounds workable!

     

Log in to post a comment.