EEVblog Electronics Community Forum

Products => Computers => Programming => Topic started by: HwAoRrDk on November 09, 2021, 09:57:57 pm

Title: [C] Ow, pointers are making my brain hurt...
Post by: HwAoRrDk on November 09, 2021, 09:57:57 pm
In a C program, if I want to pass a pointer to a buffer in to a function, then within that function read from the buffer, but also increment the original pointer, is this the right way?

Code: [Select]
void foo(uint8_t **in) {
    uint8_t n;
    n = *(*in)++; // read a byte from buf, but also increment the original pointer
}

void bar() {
    uint8_t *buf;
    foo(&buf); // After returning, buf pointer will have been incremented
}

My brain is hurting at the moment, so I'm unsure if that's doing it correctly. :-BROKE
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: cfbsoftware on November 09, 2021, 10:29:42 pm
If pointers are making your brain hurt you should ensure that you understand the basic principles. These are explained really well in Chapter 5: Pointers and Arrays of The C Programming Language by Kernighan and Ritchie. It's less than 30 pages. If you haven't got the book already then go no further until you have.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: SiliconWizard on November 09, 2021, 10:36:52 pm
In a C program, if I want to pass a pointer to a buffer in to a function, then within that function read from the buffer, but also increment the original pointer, is this the right way?

Code: [Select]
void foo(uint8_t **in) {
    uint8_t n;
    n = *(*in)++; // read a byte from buf, but also increment the original pointer
}

void bar() {
    uint8_t *buf;
    foo(&buf); // After returning, buf pointer will have been incremented
}

My brain is hurting at the moment, so I'm unsure if that's doing it correctly. :-BROKE

Yes this is correct.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: rstofer on November 09, 2021, 10:53:44 pm
In a C program, if I want to pass a pointer to a buffer in to a function, then within that function read from the buffer, but also increment the original pointer, is this the right way?

Code: [Select]
void foo(uint8_t **in) {
    uint8_t n;
    n = *(*in)++; // read a byte from buf, but also increment the original pointer
}

void bar() {
    uint8_t *buf;
    foo(&buf); // After returning, buf pointer will have been incremented
}

My brain is hurting at the moment, so I'm unsure if that's doing it correctly. :-BROKE

printf() is your friend.  Make up some sample cases and print the results.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: golden_labels on November 10, 2021, 03:22:21 am
No trying to combine as much unrelated operations as possible in a single expression may help:
Code: [Select]
n = **in;
++*in;
Or even:
Code: [Select]
uint8_t* buf = *in;
n = *buf;
*in = buf + 1;
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: Kjelt on November 10, 2021, 07:17:34 am
I hate that code. If someone else reads it or has to modify,  you can only make mistakes.
Instead declare a buffer and pass the start of the buffer as a const pointer to the function, add an index as parameter and let the function increase the index. You never ever want to change the original pointer of your buffer and esp. Not in another function than the one who declared it (and owns it).. Memory leaks and other dangers are lurking.
Oh yes also good practice to pass the size of the buffer or you have another problem that the other function is accessing memory beyond the buffer.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: Nominal Animal on November 10, 2021, 10:34:32 am
When reading pointer types (excluding the variable name), split it into tokens at each asterisk (*), and read the tokens from rightmost to leftmost, replacing each asterisk with "[is] a pointer to".

Thus, const char *volatile p; reads as "p is volatile, a pointer to const char", meaning that the value of p, the pointer, is volatile (the compiler may not assume its value does not change unexpectedly); and it points to a string or char array that we promise not to try and change the contents of.

Because parameters are passed by value, a function that takes say char *s as a parameter, can modify both the pointer s and the value it points to, (*s), but only the latter change is visible to the caller.

In function calls, the name of an array "decays" to a pointer to the first element of an array.  (This viewpoint is useful in that it also explains why sizeof(array)/sizeof(*array) returns the number of elements in array array, but passing the array to a function loses that size information, and instead yields (size of a pointer in bytes/size of the target type in bytes).)

These are not the rules C standard defines, but give you the correct intuition that you can then refine if need be.



If your function wants to modify a pointer, but doesn't return anything, return the modified pointer.  For example:
Code: [Select]
const char *skip_whitespace(const char *src)
{
    if (src) {
        while (isspace((unsigned char)(*src)))
            src++;
    }
    return src;
}
or say
Code: [Select]
const char *trim(char *src, size_t *len)
{
    if (!src)
        return "";  /* NULL turns into an empty string! */

    /* Skip leading whitespace */
    while (isspace((unsigned char)(*src)))
        src++;

    /* Trim out trailing whitespace */
    char *end = src + strlen(src);
    while (end > src && isspace((unsigned char)(end[-1])))
        end--;
    *end = '\0';

    /* Save length, if requested. */
    if (len)
        *len = (size_t)(end - src);

    return src;
}
In some cases you know the pointer won't be modified.  Then, it is a good idea to consider what useful does the function calculate that a caller might be interested in.  For example, you might need a function that trims a string converting all linear whitespace to a single space, returning the final length:
Code: [Select]
size_t trim_to_spaces(char *src)
{
    /* NULL and empty strings have length zero */
    if (!src || !*src)
        return 0;

    /* We keep src unmodified, but: */
    char *s = src;  /* Next source character */
    char *d = src;  /* Next destination character */

    /* Skip any leading whitespace. */
    while (isspace((unsigned char)(*s)))
        s++;

    /* Copy loop. */
    while (*s) {
        if (isspace((unsigned char)(*s))) {
            /* Skip all consecutive/linear whitespace, */
            do {
                s++;
            } while (isspace((unsigned char)(*s)));
            /* and replace it with a single space. */
            *(d++) = ' ';
        } else {
            *(d++) = *(s++);
        }
    }

    /* Remove the possible final space from output. */
    if (d > src && d[-1] == ' ')
        d--;

    /* String ends at d. */
    *d = '\0';

    /* Return the length of the result. */
    return (size_t)(d - src);
}
The idea is that the caller can do either trim_to_spaces(stringvar); or len = trim_to_spaces(stringvar); depending on whether the length of the trimmed and space-compacted string is useful or not.

Before I decide on the function prototype/interface, I like to write a small test case for the key points in the algorithm I want to implement, to see exactly what would be useful there.  Unless I'm implementing something I'm already familiar with, I often discover a completely different way of implementing the algorithm than what I originally envisioned, by just changing the helper functions suitably.



A common way to describe variable-length byte data like strings while modifying them, is to use a structure similar to
Code: [Select]
typedef struct {
    char *data;
    size_t  size;  /* Allocated size, i.e. data[0..size-1] are valid accesses */
    size_t  used;  /* Number of bytes used data, i.e. data[0..used-1] */
} area;
#define  AREA_INIT  { NULL, 0, 0 }
I do believe structures like the above are what Kjelt referred to, above.

Instead of passing three pointers (one to the data pointer, one to the allocated size, and one to the current length of the contents in the buffer), you just pass a pointer to the structure.  Any changes the function does to the pointer are not visible to the caller, but any changes it makes to the structure contents are: perfect.

The AREA_INIT macro is useful in that if variables of type area are initially set to AREA_INIT, we don't need a separate "init" function.  That is, you declare e.g. area result = AREA_INIT;.

For example, to append data to an area would then be
Code: [Select]
int area_append(area *dst, const void *src, const size_t len)
{
    if (!dst) {
        errno = EINVAL;
        return -1;
    }

    if (dst->used + len >= dst->size) {
        const size_t  new_size = dst->used + len + 1;  /* TODO: Growth policy? */
        void *new_data = realloc(dst->data, new_size);
        if (!new_data) {
            /* Old area is intact, but we cannot get more room.  So this fails, but is not fatal. */
            errno = ENOMEM;
            return -1;
        }
        dst->size = new_size;
        dst->data = new_data;
    }

    if (len > 0) {
        memcpy(dst->data + dst->used, src, len);
        dst->used += len;
    }

    /* In case it is string data, we append a nul byte, just to be nice. */
    dst->data[dst->used] = '\0';

    return 0;
}
and to append a C string,
Code: [Select]
int area_append_string(area *dst, const char *src)
{
    const size_t  len = (src) ? strlen(src) : 0;
    return area_append(dst, src, len);
}
When destroying/freeing an area, we return it into the initial state, so it can be reused:
Code: [Select]
void area_free(area *dst)
{
    if (area) {
        free(area->data);  /* Note: free(NULL) is safe, and does nothing. */
        area->data = NULL;
        area->size = 0;
        area->used = 0;
    }
}
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: Kjelt on November 10, 2021, 11:44:03 am
I do believe structures like the above are what Kjelt referred to, above.

The guy is a newbie and I don't want him to get scared of structs with pointers or double pointers etc.  ;)
Just let him pass the three parameters and work it out, that is one level deep and should be comprehensible for beginners.
I have seen 20+ yr experienced programmers go on their behind on double pointer errors, I deal a lot with starter programmers from foreign countries and the main task is to get them enthousiastic about programming,
and not get them scared by being too smart or showing off (not that you are doing that but you get the point)  :)
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: mfro on November 10, 2021, 12:02:48 pm
In a C program, if I want to pass a pointer to a buffer in to a function, then within that function read from the buffer, but also increment the original pointer, is this the right way?

Code: [Select]
void foo(uint8_t **in) {
    uint8_t n;
    n = *(*in)++; // read a byte from buf, but also increment the original pointer
}

void bar() {
    uint8_t *buf;
    foo(&buf); // After returning, buf pointer will have been incremented
}

My brain is hurting at the moment, so I'm unsure if that's doing it correctly. :-BROKE

"if it was hard to write, it has to be hard to read".

Besides what others have mentioned already (most likely not a good idea when the callee modifies a pointer in the caller, etc.), adopt the ultimate wisdom of humanity since stone age:

if you want to eat an elephant: cut it in slices first.

If you can't write something to be easily readable in a single line,  write it in several. That's not to your disgrace, but to you reader's convenience (and yours, if you need to revisit the code).

Compiling code into something reasonably performant is the compiler's job (that's why it's called compiler), not yours (at least not at such early stage).

Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: Nominal Animal on November 10, 2021, 12:26:54 pm
I have seen 20+ yr experienced programmers go on their behind on double pointer errors, I deal a lot with starter programmers from foreign countries and the main task is to get them enthousiastic about programming,
and not get them scared by being too smart or showing off (not that you are doing that but you get the point)  :)
True.

I probably should have left my post with just the initial part, and omit the examples, as internalizing the pointer reading rule, functions passing variables by value and not by reference (so changes to a passed variable are not visible to the caller), and arrays decaying to pointers to their first elements, really covers most if not all "pain" regarding pointers.  It's worth talking about and experimenting with an hour or so, alone.

In my defense, I really like helping others, but I often get excited and too verbose on the net, not having the face-to-face nonverbal feedback that tells me when I need to backtrack and try another approach.

The three string handling examples and the area structures are all cases I've seen others get initially horribly wrong, but spring back when playing with these examples, and discussing why they are written the way they are.  Even the isspace((unsigned char)(*src)) is important, as without the (unsigned char) cast, it will fail on non-ASCII characters on OSes and architectures where char is a signed type, which usefully leads to a short side discussion about casting, and what it means.  (Say, as opposed to type punning, leaving the latter for a later exercise.)

If you can't write something to be easily readable in a single line,  write it in several. That's not to your disgrace, but to you reader's convenience (and yours, if you need to revisit the code).
Very true.  Writing readable, easily maintained code is much more important than writing concise or tricky code.  That's why coding "tricks" are considered "evil" outside obfuscated code contests and code golf!

If you also learn to write comments that describe the programmer intent behind an operation or a function, say /* We sort the data (using Quicksort), so that we can use binary search to find the entries efficiently later on. */ instead of /* Sort the data */, you're already way ahead.  I cannot overstate the value that kind of commenting skill, in real life projects.  I so wish I had learned to write better comments early on, because it is darned hard to learn afterwards.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: DavidAlfa on November 10, 2021, 02:32:49 pm
I've done that recently, yes, can be done, but be very careful when modifying it...
Code: [Select]
Void something (uint8_t **ptr){
  // Increase pointer pointed by our pointer
  *ptr = *ptr+1;
  // *ptr++ doesn't work.
}
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: Nominal Animal on November 10, 2021, 03:20:27 pm
That's because C operator precedence says that *ptr++; is equivalent to *(ptr++);.
You need to write (*ptr)++; or ++(*ptr); to increase the value the pointer points to.

Me, I don't rely on the C operator precedence at all, because the explicit parentheses make it easier to parse for us humans anyway, and the compiler does not mind.
(That is, adding parentheses even when not strictly necessary, does not change the compiled code at all.)
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: DavidAlfa on November 10, 2021, 05:05:47 pm
Nice explanation  :-+
Everytime I get such specific in-depth details I feel like "Unga bunga, me know program thing"  :D
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: HwAoRrDk on November 10, 2021, 05:07:21 pm
Thank you for the 'C Pointers 101', but it really wasn't necessary. :) Unfortunately, today I am one of those "20+ yr experienced programmers" (but not all in C) Kjelt referred to being put on his arse by brain fade induced by late night tiredness. Sometimes you just want to get a quick confirmation that what your brain is concocting from the dredged-up knowledge it can muster is actually on the right track.

I was more concerned with what has been latterly discussed with regard to operator precedence. Like, is *(*in)++ actually doing what I think it's doing? Apparently not. Guess I should do the pointer incrementation separately from the read.

Code: [Select]
void foo(uint8_t **in) {
    uint8_t n;
    n = *(*in);
    *in = *in + 1; // or maybe *in += 1?
}

To address some of the other points raised:

Quote from: Kjelt
Instead declare a buffer and pass the start of the buffer as a const pointer to the function, add an index as parameter and let the function increase the index. You never ever want to change the original pointer of your buffer and esp. Not in another function than the one who declared it (and owns it).. Memory leaks and other dangers are lurking.
Oh yes also good practice to pass the size of the buffer or you have another problem that the other function is accessing memory beyond the buffer.

This sub-function only ever (conditionally) reads one byte from the buffer (which is guaranteed to exist and be within bounds); passing an index argument to be incremented locally and/or a buffer size would be pointless. In fact, this function will only ever get called from one other function, because it is mostly just some syntactic sugar to tidy up and abstract logic a little. Decoupling its pointer manipulation from the parent function would be a waste of time.

Quote from: Nominal Animal
If your function wants to modify a pointer, but doesn't return anything, return the modified pointer.

The function does actually need to return something - I just elided that for simplicity. I might actually change it up so that the value returned is via an output argument pointer, and the incremented pointer is the return value.

Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: SiliconWizard on November 10, 2021, 06:18:03 pm
I've done that recently, yes, can be done, but be very careful when modifying it...
Code: [Select]
Void something (uint8_t **ptr){
  // Increase pointer pointed by our pointer
  *ptr = *ptr+1;
  // *ptr++ doesn't work.
}

'*ptr++' doesn't work here, as you said, but '(*ptr)++' does, as the OP actually did!
All the lecturing after that about semantics and style was interesting and informative, but at least I think we can congratulate the OP on getting it right, which is not that common for people still uncomfortable with pointers!
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: DavidAlfa on November 10, 2021, 08:30:31 pm
I rarely used pointer few years ago, as most C code was for small microcontrollers, with little ram, little flash, no dynamic allocation, and mostly done using state machines.
Honesty, not so long ago the "->" was "The strange arrow I see sometimes"  :-DD

With stm32 and pic32 everything changed.
A lot of power, static allocating everything limited the program grow, so I learned a lot of new things, including pointers.
It's like those things that you never knew about, but when you discover them, you think: Where have you been during my whole life?  :D
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: Nominal Animal on November 12, 2021, 10:56:07 am
Thank you for the 'C Pointers 101', but it really wasn't necessary. :) Unfortunately, today I am one of those "20+ yr experienced programmers" (but not all in C) Kjelt referred to being put on his arse by brain fade induced by late night tiredness.
Don't be offended, though; we didn't know, and in any case, these posts are indexed search engines, and it is common for others to stumble on the thread afterwards.

I myself do not write "answers" or "help" dedicated to the asker, but try to expand a bit, covering not just the asker's particular requirements, but also some of the alternatives in case someone else has the same problem, but slightly different requirements.

The reason is that I just cannot do the focused, narrow answers without any context; and helping only the original asker my way is unlikely to be worth anything to anyone.  Call it a personality flaw of mine.  But at no time should the depth or style of my answers be taken as indicative of my understanding of the asker's knowledge: I always write in the "101 style", trying to keep "everyone along", because I cannot help it.  I've accidentally pissed off many members because they thought I was unaware of their knowledge, but that's not the case; I just have that urge of trying to explain things so that even interested passersby can gain from the discussion details, that I cannot seem to control.

I was more concerned with what has been latterly discussed with regard to operator precedence. Like, is *(*in)++ actually doing what I think it's doing? Apparently not. Guess I should do the pointer incrementation separately from the read.
Or, do like I do, and use "extra" parentheses to ensure the expression does what you want it to do: *((*in)++) to dereference the pointer twice, and post-increment the dereferenced pointer, or (*(*in))++ to dereference the pointer twice, and post-increment the twice-dereferenced value.
(Well, actually, now that I look at that, I too would prefer to split the dereference and increment to separate statements.)

I just cannot memorize details like that; my memory does not work that way.  But I can effectively compensate (by using parentheses, and rely on man pages for standard C APIs, like whether memset() takes (pointer, size, value) or (pointer, value, size) – a surprisingly common bug!), and the resulting code is both explicit, more reliable (because I don't *trust*, I *check*), and just as efficient as if I relied on the inherent operator precedence order.

Yeah, I know that cow-orkers can make snide remarks when they see I always have a terminal window open on a man page, or a browser window open to Linux man-pages online (https://man7.org/linux/man-pages/) (which are useful on non-Linux OSes too, because each page in sections 2 and 3 have a Conforming To section, describing where and when one can expect the function or facility to be available).  After a few months, when they get familiar with my output, they tend to change their opinion and adopt the same approach.  (It's the higher ups and nontechnical people, who don't get that results are more important than appearance.)
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: brucehoult on November 12, 2021, 12:14:47 pm
In a C program, if I want to pass a pointer to a buffer in to a function, then within that function read from the buffer, but also increment the original pointer, is this the right way?

Code: [Select]
void foo(uint8_t **in) {
    uint8_t n;
    n = *(*in)++; // read a byte from buf, but also increment the original pointer
}

It's perfectly correct, but perhaps not the best way to do it.

I'm not sure what you're planning to do with n. As it stands the compiler will just optimise everything away.

Let's say it's a global:

Code: [Select]
#include <stdint.h>

uint8_t n;

void foo(uint8_t **in) {
    n =  *(*in)++; // read a byte from buf, but also increment the original pointer
}

It's pretty useful to compile the code to assembly language and see what it does ... https://godbolt.org/z/r1bdb1Ers ... with added comments:

Code: [Select]
foo:
        lw      a5,(a0)   # fetch the pointer to the buffer from memory
        addi    a4,a5,1  # increment the pointer value
        sw      a4,(a0)   # store the incremented value back to the original pointer
        lbu     a4,(a5)   # load the byte pointed to by the original value of the pointer
        lui     a5,%hi(n) # load the high bits of the address of n
        sb      a4,%lo(n)(a5) # store the byte from the buffer into the global n
        ret
n:
        .zero   1

An annoying an inefficient thing about this is that the compiler is forced to store a pointer to the buffer into memory, even if it is only in a register in the calling function.

A better way can be to just pass the pointer, and pass back the updated pointer: https://godbolt.org/z/foz6af9nq

Code: [Select]
#include <stdint.h>

uint8_t n;

uint8_t* foo(uint8_t *in) {
    n =  *in++;
    return in;
}

This produces much less machine code:

Code: [Select]
foo:
        lbu     a4,(a0)  # get a byte from the buffer
        lui     a5,%hi(n) # load the high bits of the address of n
        sb      a4,%lo(n)(a5)  # store the byte to n
        addi    a0,a0,1 # add 1 to the pointer and return it
        ret
n:
        .zero   1

This version has only the (logically necessary) one memory load instead of two, and doesn't need to store the updated pointer value back to memory.

On a modern CPU using registers is much faster than using memory. And of course fewer instructions is faster than using more instructions -- at least in a RISC instruction set. CISC can look like fewer instructions, but expanding to a lot of hidden expensive micro-ops.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: SiliconWizard on November 13, 2021, 12:32:06 am
You're making a good point, although it goes into the optimization category and may be a bit much here. Another two points I'm thinking of: your remark applies to functions that are *called*, but in all cases where said function can be inlined, I think the compiler will probably implement this using registers (when it can). Another point of course is that returning the updated pointer works if you have only one such parameter. As soon as you have more than one, you can't.

Well, to be fair though: you may 1/ reply that having more than one 'pointer to pointer' as arguments in a function might be bad style (dunno) and 2/ that even so, you can use return values using structs. Yeah, Returning structs is a bit unusual in C, but it's not forbidden. And if you return say two pointers in a struct, they will still be returned in a pair of registers (usually). I actually do not dislike this style - it's kind of a functional style.

Something like:
Code: [Select]
typedef struct { uint8_t *in; uint8_t *out; } InOutPtrs_t;

InOutPtrs_t foo(InOutPtrs_t InOut)
{
    *InOut.out++ = *InOut.in++;
    return InOut;
}

On RISC-V, you get:
Code: [Select]
foo:
        lbu     a5,0(a0)
        addi    sp,sp,-32
        addi    a0,a0,1
        sb      a5,0(a1)
        addi    a1,a1,1
        addi    sp,sp,32
        jr      ra

Not too shabby! The only thing I'm wondering - sorry if this is yet another brain fart due to time - is why GCC manipulates the sp register while the stack is not even used.
(GCC 11.1.0 here, default RISCV target, -O3)
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: brucehoult on November 13, 2021, 01:50:21 am
your remark applies to functions that are *called*, but in all cases where said function can be inlined, I think the compiler will probably implement this using registers (when it can).

Yes, if the function is inlined, and that's the only thing taking the address of the pointer, then the taking of the address and subsequent dereference won't be done and it won't be forced to memory.

Quote
Well, to be fair though: you may 1/ reply that having more than one 'pointer to pointer' as arguments in a function might be bad style (dunno) and 2/ that even so, you can use return values using structs. Yeah, Returning structs is a bit unusual in C, but it's not forbidden. And if you return say two pointers in a struct, they will still be returned in a pair of registers (usually). I actually do not dislike this style - it's kind of a functional style.

Right.

What I don't quite understand is why most ABIs limit this to two return registers. It makes perfect sense to use as many registers for function returns as for function arguments -- it's just tail calling the continuation (where the continuation address is passed in ra).

Quote
The only thing I'm wondering - sorry if this is yet another brain fart due to time - is why GCC manipulates the sp register while the stack is not even used.

Bug. It's presumably the result of not running the stack optimisation pass (again?) after doing the "return struct in two registers" optimisation pass.

Clang doesn't touch sp when compiling the same function.

I hope people don't mind using RISC-V for these kinds of examples. I feel the assembly language is significantly more transparent and less cluttered to read than even ARM or MIPS.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: SiliconWizard on November 13, 2021, 01:57:11 am
Quote
Well, to be fair though: you may 1/ reply that having more than one 'pointer to pointer' as arguments in a function might be bad style (dunno) and 2/ that even so, you can use return values using structs. Yeah, Returning structs is a bit unusual in C, but it's not forbidden. And if you return say two pointers in a struct, they will still be returned in a pair of registers (usually). I actually do not dislike this style - it's kind of a functional style.

Right.

What I don't quite understand is why most ABIs limit this to two return registers. It makes perfect sense to use as many registers for function returns as for function arguments -- it's just tail calling the continuation (where the continuation address is passed in ra).

Yes, this is too bad. And yes I think people should be more "aware" of this functional-like approach (which is not commonly seen in C) which looks more elegant, and can even be more efficient. And less bug-prone.

Quote
The only thing I'm wondering - sorry if this is yet another brain fart due to time - is why GCC manipulates the sp register while the stack is not even used.

Bug. It's presumably the result of not running the stack optimisation pass (again?) after doing the "return struct in two registers" optimisation pass.

Thought so. I'm a bit disappointed currently with GCC for RISC-V. I think it was going better a year or two ago.

I hope people don't mind using RISC-V for these kinds of examples. I feel the assembly language is significantly more transparent and less cluttered to read than even ARM or MIPS.

Well, it might be a bit harder for people not knowing the RISC-V IS, but I agree that, compared to ARM, MIPS or even x86, it looks cleaner and is easier to read.
The same function in x86_64 assembly:
Code: [Select]
foo:
        movdqu  (%rdx), %xmm0
        movq    %xmm0, %rdx
        movhlps %xmm0, %xmm1
        paddq   .LC0(%rip), %xmm0
        movq    %rcx, %rax
        movzbl  (%rdx), %ecx
        movq    %xmm1, %rdx
        movups  %xmm0, (%rax)
        movb    %cl, (%rdx)
        ret

Yeah. OK.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: brucehoult on November 13, 2021, 02:59:31 am
ARMv7 gcc ... all in memory...

Code: [Select]
foo:
        sub     sp, sp, #8
        add     ip, sp, #8
        stmdb   ip, {r1, r2}
        ldrb    ip, [r1], #1    @ zero_extendqisi2
        strb    ip, [r2], #1
        str     r1, [r0]
        str     r2, [r0, #4]
        add     sp, sp, #8
        bx      lr

ARMv7 Clang ... cleaner but return struct it still in memory..

Code: [Select]
foo:
        ldrb    r3, [r1], #1
        strb    r3, [r2], #1
        stm     r0, {r1, r2}
        bx      lr

ARMv8 (er, Aarch64) gcc...

Code: [Select]
foo:
        ldrb    w2, [x0], 1
        strb    w2, [x1], 1
        ret

Better.

Neither ARM assembly language makes it all that clear what the trailing ", 1" or ", #1" does, for those not familiar.

gcc in general seems to be suffering recently. The code base is far more annoying to work on, and getting approved to upstream changes has been a PITA forever. The focus of most compiler developers has moved on to LLVM probably five or six years ago at least, and LLVM has now overtaken gcc in quality for most ISAs.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: PlainName on November 13, 2021, 09:02:19 pm
Quote
A better way can be to just pass the pointer, and pass back the updated pointer:
...
This produces much less machine code:

But it's doing less work - something else, which isn't accounted for here, has to update the pointer to achieve the same effect as the first version.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: brucehoult on November 13, 2021, 09:23:40 pm
Quote
A better way can be to just pass the pointer, and pass back the updated pointer:
...
This produces much less machine code:

But it's doing less work - something else, which isn't accounted for here, has to update the pointer to achieve the same effect as the first version.

"pass back the updated pointer"

Achieving the same result while doing less work is the point.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: PlainName on November 13, 2021, 09:37:50 pm
The point was the example assembler for it is doing less because it's not updating the pointer. It may be passing it back, but that does nothing until the caller uses it. To be equivalent, you'd need to show the assembler for the calling function doing the update.

I'm surprised that wasn't clear so perhaps I've missed something?
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: SiliconWizard on November 13, 2021, 09:41:41 pm
Quote
A better way can be to just pass the pointer, and pass back the updated pointer:
...
This produces much less machine code:

But it's doing less work - something else, which isn't accounted for here, has to update the pointer to achieve the same effect as the first version.

No, it does the exact same thing. Just more elegantly.

The "less work" you're talking about here is just that you need to assign the return value back to the pointer variable. That's a lot of work for sure.
Like: you need to write: "p = foo(p)" instead of "foo(&p)". Okay.

And the added value is that it's more elegant - no double pointer - and most of all, it doesn't exhibit side-effects, which are a major source of bugs. It's close to a functional style, as I said.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: NorthGuy on November 13, 2021, 09:54:08 pm
The same function in x86_64 assembly:
Code: [Select]
foo:
        movdqu  (%rdx), %xmm0
        movq    %xmm0, %rdx
        movhlps %xmm0, %xmm1
        paddq   .LC0(%rip), %xmm0
        movq    %rcx, %rax
        movzbl  (%rdx), %ecx
        movq    %xmm1, %rdx
        movups  %xmm0, (%rax)
        movb    %cl, (%rdx)
        ret

Yeah. OK.

Of course x64 ABI doesn't pass the struct in registers, nor does it return the struct in registers. Thus the ABI feature you're catering to doesn't exist. Hence, the "optimization" produces considerable bloat instead.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: PlainName on November 13, 2021, 10:03:48 pm
Quote
No, it does the exact same thing. Just more elegantly.

It doesn't. It does part of it. More elegantly perhaps, but there is that little bit missing.

Quote
The "less work" you're talking about here is just that you need to assign the return value back to the pointer variable.

Exactly.  And this bit isn't about elegance but about how many instructions (ignoring 'of what kind'). If you're going to the bother of profiling it (which I don't see why - elegance should win over size unless there is some overriding reason) then you need to profile the same thing. Thus the calling statement and return action (if any) should be in the mix. Otherwise you're just saying this pear is smaller than that orange.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: SiliconWizard on November 13, 2021, 10:23:12 pm
Quote
No, it does the exact same thing. Just more elegantly.

It doesn't. It does part of it. More elegantly perhaps, but there is that little bit missing.

Quote
The "less work" you're talking about here is just that you need to assign the return value back to the pointer variable.

Exactly.  And this bit isn't about elegance but about how many instructions (ignoring 'of what kind'). If you're going to the bother of profiling it (which I don't see why - elegance should win over size unless there is some overriding reason) then you need to profile the same thing. Thus the calling statement and return action (if any) should be in the mix. Otherwise you're just saying this pear is smaller than that orange.

We showed that it was usually more efficient in the general case (while being equivalent if the functions are inlined.) But feel free to include both approaches in real context, get the assembly and see for yourself. Maybe we're wrong.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: SiliconWizard on November 13, 2021, 10:54:44 pm
The same function in x86_64 assembly:
Code: [Select]
foo:
        movdqu  (%rdx), %xmm0
        movq    %xmm0, %rdx
        movhlps %xmm0, %xmm1
        paddq   .LC0(%rip), %xmm0
        movq    %rcx, %rax
        movzbl  (%rdx), %ecx
        movq    %xmm1, %rdx
        movups  %xmm0, (%rax)
        movb    %cl, (%rdx)
        ret

Yeah. OK.

Of course x64 ABI doesn't pass the struct in registers, nor does it return the struct in registers. Thus the ABI feature you're catering to doesn't exist. Hence, the "optimization" produces considerable bloat instead.

That's interesting. From what I've seen, the ABI actually allows using up to 6 registers for parameters - if they fit - and two for the return value (rax and rdx).
I didn't find any "hard" rule in the ABI that would prevent a compiler from passing/returning structs into several registers if this fits, or integer parameters wider than 1 register, for that matter.
I've found quite a few threads about this on StackOverflow and others. Still unsure where the "truth" should lie here.

What I've read:
Code: [Select]
The first six arguments to a function are passed in registers. Any additional arguments are passed
on the stack in the memory-argument area (see Figure 2). The %rax register is used to return the
first result and the %rdx register is used to return a second result.

Sure you may understand this blindly as meaning that each parameter of a function can only be passed in ONE register if it fits, or on the stack otherwise. Not sure I see the rationale of preventing splitting one argument to several registers, or returning values in two registers since the ABI allows it. Yeah I've seen some heated arguments about that so, no need to reproduce them here. Just mentioning it.

But anyway, the point was looking at how this approach would be compiled for different targets with the usual compilers, and the best for this appeared to be RISC-V and Aarch64.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: brucehoult on November 13, 2021, 10:59:58 pm
The point was the example assembler for it is doing less because it's not updating the pointer. It may be passing it back, but that does nothing until the caller uses it. To be equivalent, you'd need to show the assembler for the calling function doing the update.

I'm surprised that wasn't clear so perhaps I've missed something?

OK, fine. I'll show the caller too.

The technique of passing the modified pointer(s) back as a function result makes the CALLER simpler as well.

Let's use SiliconWizard's example that copies a byte from one buffer to another to build a memcpy https://godbolt.org/z/ojjzYzT7G :

Code: [Select]
#include <inttypes.h>

typedef struct { uint8_t *out; uint8_t *in; } InOutPtrs_t;

__attribute__((noinline))
InOutPtrs_t copyAchar(InOutPtrs_t InOut)
{
    *InOut.out++ = *InOut.in++;
    return InOut;
}

void mymemcpy(uint8_t *dst, uint8_t *src, long sz){
    InOutPtrs_t InOut = {.in=src, .out=dst};
    while (sz--) InOut = copyAchar(InOut);
}

The RV32 assembly language:

Code: [Select]
copyAchar:                              # @copyAchar
        lb      a3, 0(a1)
        addi    a1, a1, 1
        addi    a2, a0, 1
        sb      a3, 0(a0)
        mv      a0, a2
        ret
mymemcpy:                               # @mymemcpy
        addi    sp, sp, -16
        sw      ra, 12(sp)                      # 4-byte Folded Spill
        sw      s0, 8(sp)                       # 4-byte Folded Spill
        beqz    a2, .LBB1_3
        mv      s0, a2
.LBB1_2:                                # =>This Inner Loop Header: Depth=1
        addi    s0, s0, -1
        call    copyAchar
        bnez    s0, .LBB1_2
.LBB1_3:
        lw      s0, 8(sp)                       # 4-byte Folded Reload
        lw      ra, 12(sp)                      # 4-byte Folded Reload
        addi    sp, sp, 16
        ret

Notice that the loop in mymemcpy() has only three instructions (update the counter, call the function, loop if not zero) and no memory instructions at all. And the copyAchar() function has six instructions and only the necessary two memory instructions to actually load and store the byte being copied.

Now let's try it by passing the in and out pointers as in the OP's code https://godbolt.org/z/W3v5WPWd3 :

Code: [Select]
#include <inttypes.h>

__attribute__((noinline))
void copyAchar(uint8_t **out, uint8_t **in) {
    *(*out)++ = *(*in)++;
}

void mymemcpy(uint8_t *dst, uint8_t *src, long sz){
    while (sz--) copyAchar(&dst, &src);
}

And the generated assembly language...

Code: [Select]
copyAchar:                              # @copyAchar
        lw      a2, 0(a1)
        addi    a3, a2, 1
        sw      a3, 0(a1)
        lw      a1, 0(a0)
        lb      a2, 0(a2)
        addi    a3, a1, 1
        sw      a3, 0(a0)
        sb      a2, 0(a1)
        ret
mymemcpy:                               # @mymemcpy
        addi    sp, sp, -16
        sw      ra, 12(sp)                      # 4-byte Folded Spill
        sw      s0, 8(sp)                       # 4-byte Folded Spill
        sw      a0, 4(sp)
        sw      a1, 0(sp)
        beqz    a2, .LBB1_3
        mv      s0, a2
.LBB1_2:                                # =>This Inner Loop Header: Depth=1
        addi    s0, s0, -1
        addi    a0, sp, 4
        mv      a1, sp
        call    copyAchar
        bnez    s0, .LBB1_2
.LBB1_3:
        lw      s0, 8(sp)                       # 4-byte Folded Reload
        lw      ra, 12(sp)                      # 4-byte Folded Reload
        addi    sp, sp, 16
        ret

Now the calling code has to first store the pointers arguments a0 and a1 into memory at SP and SP+4, and the loop needs five instructions instead of three because it has to regenerate the function arguments each time.  And the called code now needs two extra memory loads and two extra memory stores.

Passing the pointers by value in a struct, and returning their updated values in a struct, improves the code in BOTH the called and calling functions.  We go from 16 to 12 instructions in the caller (from 5 to 3 in the loop) and from 9 to 6 (could have been 5) instructions in the called function. And most importantly on modern machines, from 3 loads and 3 stores per byte copied to 1 load and 1 store.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: Nominal Animal on November 13, 2021, 11:05:20 pm
Of course x64 ABI doesn't pass the struct in registers, nor does it return the struct in registers. Thus the ABI feature you're catering to doesn't exist.
Well, actually both SysV AMD64 ABI (http://math-atlas.sourceforge.net/devel/assembly/abi_sysV_amd64.pdf) (as used on Linux) and ARM ABI (https://github.com/ARM-software/abi-aa/blob/2bcab1e3b22d55170c563c3c7940134089176746/aapcs32/aapcs32.rst#result-return) support returning a structure with two register-sized elements.

That is,
Code: [Select]
struct longpair {
    long  a, b;
};

struct longpair squarepair(const struct longpair p)
{
    const struct longpair r = { p.a * p.a, p.b * p.b };
    return r;
}
which compiles using GCC -O2 on linux x86-64 to
Code: [Select]
squarepair:
        mov     rax, rdi
        mov     rdx, rsi
        imul    rax, rdi
        imul    rdx, rsi
        ret
and armv7-a clang10 to
Code: [Select]
squarepair:
        mul     r3, r2, r2
        mul     r2, r1, r1
        stm     r0, {r2, r3}
        bx      lr

What the ABI does not allow, is compiler automagically returning a pointer instead of modifying it via the pointer passed as a parameter; you need to use the structure (and enable compiler optimizations, so it doesn't play stupid).
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: brucehoult on November 13, 2021, 11:28:52 pm
So what we see there is 32 bit ARM storing the results into a struct in memory, pointed to by an invisible extra argument passed in r0.

RISC-V (both 32 bit and 64 bit) and 64 bit ARM do it all in registers:

Code: [Select]
squarepair:                             # @squarepair
        mul     a0, a0, a0
        mul     a1, a1, a1
        ret

Code: [Select]
squarepair:                             // @squarepair
        mul     x0, x0, x0
        mul     x1, x1, x1
        ret

You can only tell which is which by knowing what register names they use :-)
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: PlainName on November 13, 2021, 11:35:31 pm
Quote
get the assembly and see for yourself. Maybe we're wrong.

You are misrepresenting what I was saying. Nowhere did I say it wouldn't be shorter, smaller, faster, cheaper, more beautiful, higher, or whatever irrelevant measurement you're balancing on a pin head. I don't care, don't know and have no desire to find out any of those.

My SOLE point was that the two examples were not equivalent. One does call-read-add-update-return and the other does call-read-add-return. See? A little bit missing there. That was my sole point and I am rather disappointed that none of you have grasped it but just bang on about how small the damn thing is when that doesn't matter a toss except for the angels on a pin arguments.

Edit:
Quote
OK, fine. I'll show the caller too.

Thank you. Although I really don't care how it turns out, I think it's important to be accurate especially when you're going to call people out for pretty much the same thing elsewhere.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: Nominal Animal on November 13, 2021, 11:41:55 pm
Crap, me fail once again. ARMv8a does compile to
Code: [Select]
squarepair:                             // @squarepair
        mul     x0, x0, x0
        mul     x1, x1, x1
        ret
and RISC-V rv32gc and rv64gc using Clang to
Code: [Select]
squarepair:                             # @squarepair
        mul     a0, a0, a0
        mul     a1, a1, a1
        ret
as expected.

Now, where can I get armeabi-v7a procedure call spec?  Ah yes, IHI 0042J (https://developer.arm.com/docs/ihi0042/latest).  Okay,
Code: [Select]
typedef  long  pair __attribute__((vector_size (2 * sizeof (long))));

pair squarepair(const pair p)
{
    return (pair){ p[0]*p[0], p[1]*p[1] };
}
gets passed in r0-r1 on armv7-a, and a0-a1 on rv32gc and rv64gc.

To make that useful, one would need to hide the internal details of the pair in macros, since on x86-64 it would be passed in xmm0 and on armv8-a in v0, in a rather inefficient manner; there a structure would work better.

I am not sure when I would bother, though.  Probably never; it's not like temporary memory access is that costly (compared to the code complexity and ABI dependency generated).
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: brucehoult on November 13, 2021, 11:56:27 pm
Now, where can I get armeabi-v7a procedure call spec?

Funnily enough, Googling your words returns https://developer.arm.com/documentation/ihi0042/latest

Composite types can be passed in registers (or partially registers, partially stack if the 4 argument registers are exhausted)

A scalar type larger than a register (e.g. long long, or double) is returned in r0 and r1

Composite types larger than a register are returned in memory, with the address of the memory passed in r0. A bit of a shame.

I'd love to see ABIs where composite return values could use all the same registers as arguments can. Why not? Their values are currently undefined after return from a function, so there is nothing to be lost.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: Nominal Animal on November 14, 2021, 12:36:47 am
(Sorry, didn't see your answer as I was editing mine.  Did some godbolt.org testing.)

I'd love to see ABIs where composite return values could use all the same registers as arguments can. Why not? Their values are currently undefined after return from a function, so there is nothing to be lost.
Me too.  I really don't like the C errno convention; it would be much nicer to just return more than one scalar, so one of them could be the error code (and 0 for no error).

There are quite a few cases like reading from a file or device, where returning some data and and an error would make the API so much more useful.

Which ties in to this thread nicely: when returning both a numeric value and a pointer, it actually makes sense to pass the pointer as a parameter, and the pointer to the numeric value to be updated, and return the pointer.  This is because the code then only does one level of pointer dereferencing.

For example, when parsing (command-line) parameters to a numeric type NUMTYPE, I use either
    int  parse_NUMTYPE(const char *from, NUMTYPE *to);
returning 0 if success, nonzero errno error code otherwise, if the from string should be a complete number without any trailing garbage, or
    const char *parse_NUMTYPE(const char *from, NUMTYPE *to);
returning a pointer to the first unparsed character, or NULL if there wasn't a number to be parsed.

This might look odd at first, but both the parsing (I usually use strtod()/strtol()/strtoul()/strtoll()/strtoull()) and calling the parsing function is simpler and more robust (maintenance-wise, simpler code) than passing a double pointer.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: SiliconWizard on November 14, 2021, 12:41:25 am
Of course x64 ABI doesn't pass the struct in registers, nor does it return the struct in registers. Thus the ABI feature you're catering to doesn't exist.
Well, actually both SysV AMD64 ABI (http://math-atlas.sourceforge.net/devel/assembly/abi_sysV_amd64.pdf) (as used on Linux) and ARM ABI (https://github.com/ARM-software/abi-aa/blob/2bcab1e3b22d55170c563c3c7940134089176746/aapcs32/aapcs32.rst#result-return) support returning a structure with two register-sized elements.

Ah, thanks for pointing this out! The example I gave for x86_64 was compiled on Windows. Just did the same on Linux (also x86_64) and I get this instead:

Code: [Select]
        movzbl  (%rdi), %eax
        leaq    1(%rsi), %rdx
        movb    %al, (%rsi)
        leaq    1(%rdi), %rax
        ret

which is much nicer, and closer to what I was expecting.
So lesson learned about the x86_64 ABI on Windows - at least the way it's implemented with both GCC and LLVM.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: SiliconWizard on November 14, 2021, 01:14:52 am
(Sorry, didn't see your answer as I was editing mine.  Did some godbolt.org testing.)

I'd love to see ABIs where composite return values could use all the same registers as arguments can. Why not? Their values are currently undefined after return from a function, so there is nothing to be lost.
Me too.  I really don't like the C errno convention; it would be much nicer to just return more than one scalar, so one of them could be the error code (and 0 for no error).

Oh, I agree. I always do that in my own code.
As we saw, there are a few ways of doing it in C: the most common is to return an error code, and return whatever else the function should return via pointers.
Another, as I suggested here too, is to use structs. Although results vary a bit depending on the target, if you restrict your returned structs to two values which typically fit in registers, it's as efficient, if not more, and more elegant. Drawback is you have to define a struct for each kind of type you want to return beside the error code. Not as elegant as in languages that actively support returning multiple values as tuples, or with some kind of monad mechanism, but it also works.

If you want to return say a double along with an error code:
Code: [Select]
#include <math.h>

enum { NOERROR = 0, INVALID_PARAM = 1 };

typedef struct { int err; double value; } RetDouble_t;

RetDouble_t MySqrt(double x)
{
    RetDouble_t  Ret = { .err = NOERROR };

    if (x < 0.0)
        Ret.err = INVALID_PARAM;
   else
        Ret.value = sqrt(x);

    return Ret;
}
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: NorthGuy on November 14, 2021, 04:10:50 am
Of course x64 ABI doesn't pass the struct in registers, nor does it return the struct in registers. Thus the ABI feature you're catering to doesn't exist.
Well, actually both SysV AMD64 ABI (http://math-atlas.sourceforge.net/devel/assembly/abi_sysV_amd64.pdf) (as used on Linux) and ARM ABI (https://github.com/ARM-software/abi-aa/blob/2bcab1e3b22d55170c563c3c7940134089176746/aapcs32/aapcs32.rst#result-return) support returning a structure with two register-sized elements.

Windows x64 ABI is different - different registers are used, different rules.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: Nominal Animal on November 14, 2021, 08:28:10 am
Of course x64 ABI doesn't pass the struct in registers, nor does it return the struct in registers. Thus the ABI feature you're catering to doesn't exist.
Well, actually both SysV AMD64 ABI (http://math-atlas.sourceforge.net/devel/assembly/abi_sysV_amd64.pdf) (as used on Linux) and ARM ABI (https://github.com/ARM-software/abi-aa/blob/2bcab1e3b22d55170c563c3c7940134089176746/aapcs32/aapcs32.rst#result-return) support returning a structure with two register-sized elements.

Windows x64 ABI is different - different registers are used, different rules.
True.  All other OSes on x86-64 (actually, AMD64) use the SysV ABI.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: PlainName on November 14, 2021, 11:17:44 am
Quote
If you want to return say a double along with an error code:
Code: [Select]

#include <math.h>

enum { NOERROR = 0, INVALID_PARAM = 1 };

typedef struct { int err; double value; } RetDouble_t;

RetDouble_t MySqrt(double x)
{
    RetDouble_t  Ret = { .err = NOERROR };

    if (x < 0.0)
        Ret.err = INVALID_PARAM;
   else
        Ret.value = sqrt(x);

    return Ret;
}

Isn't that going to cause memory issues when the returned struct, which no longer exists, is sampled? Getting around that would be really messy, I think.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: Nominal Animal on November 14, 2021, 11:32:40 am
Isn't that going to cause memory issues when the returned struct, which no longer exists, is sampled? Getting around that would be really messy, I think.
No.

Consider this:
Code: [Select]
typedef struct {
    int  x;
    int  y;
    int  z;
} vec3i;

vec3i Vec3i(const int x, const int y, const int z)
{
    const vec3i result = { .x = x, .y = y, .z = z };
    return result;
}
(Ignore that the code is silly, since C99 and later allow (vec3i){ .x = x, .y = y, .z = z }.  This is just for illustration, a three integer component vector type.)

On architectures and ABIs where the structure is not passed in registers (most of them), the caller reserves memory for the structure, and passes a pointer to it to the function.  (Exactly how –– i.e., which register, or where on the stack that is ––, depends on the ABI.)

In all cases, the actual structure returned therefore has the caller scope, not the called function scope.  So using the structure in the caller is perfectly okay. For example, in
Code: [Select]
int  vec3i_dot(const int x, const int y, const int z)
{
    vec3i  v = Vec3i(x, y, z);
    return v.x*v.x + v.y*v.y + v.z*v.z;
}
either v is passed in registers, or the compiler passes a pointer to v when calling Vec3i(), depending on the ABI.  In either case its lifetime is the caller scope, and the above is completely safe.

(If you were to pore through the abstract machine model of the C standard, you'd find that regardless of how the structure is passed to the caller, its lifetime must be the caller scope, and not the function scope.  So it's completely different than returning a pointer to a local (non-static) variable, which is a bug.)
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: brucehoult on November 14, 2021, 11:55:15 am
Isn't that going to cause memory issues when the returned struct, which no longer exists, is sampled? Getting around that would be really messy, I think.

No.

C function arguments and results are passed by value. That is, (logically) they are copied in the process of passing them.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: PlainName on November 14, 2021, 01:39:55 pm
Quote
On architectures and ABIs where the structure is not passed in registers (most of them), the caller reserves memory for the structure, and passes a pointer to it to the function.  (Exactly how –– i.e., which register, or where on the stack that is ––, depends on the ABI.)

Ah! Thank you :)
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: DiTBho on November 14, 2021, 01:54:27 pm
as reference, three different approaches
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: SiliconWizard on November 14, 2021, 05:57:19 pm
Isn't that going to cause memory issues when the returned struct, which no longer exists, is sampled? Getting around that would be really messy, I think.

No.

C function arguments and results are passed by value. That is, (logically) they are copied in the process of passing them.

And, a third no. :)
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: SiliconWizard on November 14, 2021, 06:02:47 pm
Of course x64 ABI doesn't pass the struct in registers, nor does it return the struct in registers. Thus the ABI feature you're catering to doesn't exist.
Well, actually both SysV AMD64 ABI (http://math-atlas.sourceforge.net/devel/assembly/abi_sysV_amd64.pdf) (as used on Linux) and ARM ABI (https://github.com/ARM-software/abi-aa/blob/2bcab1e3b22d55170c563c3c7940134089176746/aapcs32/aapcs32.rst#result-return) support returning a structure with two register-sized elements.

Windows x64 ABI is different - different registers are used, different rules.
True.  All other OSes on x86-64 (actually, AMD64) use the SysV ABI.

Yep. As we can see, the difference in this example is absolutely mind-boggling. I'd be curious to see actual analysis and "benchmarks" that can evaluate the impact of the ABI in a range of typical applications. If someone happens to have a reference on that. Because as it looks, the Windows ABI seems pretty horrible compared to SysV, but it'd be interesting to see a well conducted analysis on this rather than impressions based on a few examples.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: Nominal Animal on November 14, 2021, 07:15:09 pm
I'd be curious to see actual analysis and "benchmarks" that can evaluate the impact of the ABI in a range of typical applications. If someone happens to have a reference on that. Because as it looks, the Windows ABI seems pretty horrible compared to SysV, but it'd be interesting to see a well conducted analysis on this rather than impressions based on a few examples.
I think the hardest part of that would be choosing the representative set of function signatures to compare.

On SysV AMD64/x86-64 passes up to six 64-bit integer or pointer parameters (or up to three max. 128-bit aggregates) and up to 8 128-bit xmm vectors in xmm registers (including single doubles as a 128-bit xmm vector first component).
Windows x64 passes up to four 64-bit aggregate arguments in standard registers, and up to four floating-point xmm vectors in xmm registers.

No choice of function signatures would be "fair", because each ABI suggests/implies/assumes different approaches to function signatures.  For example, as an X64 programmer, I would prefer passing references (pointers) to structures over 64 bits instead of passing the structures; but on SysV, the limit is 128 bits.  Structures with a 64-bit pointer and a 64-bit size, or a 64-bit pointer and two 32-bit integers, are quite common.  Should one pass those by value or by reference (via a pointer)?
Programmers comfy on one of the ABIs will have pretty strong preferences of stuff like this, because of their experience (which is obviously colored by the performance and quirks of the ABI they use so much).  And because of cache effects, register allocation, and so on, even tiny differences in the function call and return, can cause significant performance difference in the surrounding code.

It would be more fair to examine the code generated and efficiency of different function signature approaches for each ABI and compiler family and version.
That way you wouldn't be trying to compare ABI-versus-ABI, but approach-on-ABI-and-compiler.  If you microbenchmark the approaches on the two different ABIs on the same hardware, you can perhaps make some kind of qualitative statements about the relative efficiencies of the ABIs, but I wouldn't bother; the relative merits and downsides of the function signature approaches would be much more useful.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: SiliconWizard on November 14, 2021, 07:34:46 pm
Having to adapt your code style to the underlying ABI is not a fantastic idea IMHO. You might have to resort to that when performance is critical, but I favor portable approaches otherwise. But I certainly do agree, comparisons are difficult to make relevant. Yeah it's all a matter of compromises too. If more registers are used to pass arguments and return values, then sure, fewer will be available for other uses, and thus more register saving will be needed... So sure, on targets with more general-purpose registers, registers will be favored.

Just a quick summary of the differences: https://sourceforge.net/p/mingw-w64/wiki2/MinGW%20x64%20Software%20convention/

No clue how the exact differences explain the point, but I've certainly noticed, for instance, that some applications are significantly faster on Linux than on Windows on similar hardware (such as compilers, for instance.) Could also largely be due to differences in how memory is managed, rather than the ABI itself.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: Nominal Animal on November 14, 2021, 08:04:55 pm
Having to adapt your code style to the underlying ABI is not a fantastic idea IMHO.
No, but it is something that is inherent in non-portable code.  Those who know the ABI will be affected by it, and such approaches spread, like fads.  Anyone doing microbenchmarks verifies "they are better", so an approach gets adopted.  This happens.

It is actually the opposite that I'm interested in: what are the patterns one should avoid in portable code.

(I've mentioned before that whenever I look at e.g. the code generated by my compilers, I'm not really interested whether it is optimal or not, because that way lies madness.  What I am interested in, is whether a compiler or a set of options causes it to generate horrible code.  I prefer an approach that is not optimal, but not horrible anywhere, over approaches that are optimal somewhere but horrible somewhere else, because chasing optimality is a fool's errand, but avoiding horrible behaviour is a sensible precaution.  In my experience and opinion, that is.)

In the olden era of Fortran and compiler-specific ABIs, when parameters were passed on the stack, using global variables instead of passing them as parameters was a way to speed up critical code.  Now, the opposite is true, but old habits die hard, especially since They Are Written In Reliable Books I Trust, So Why Should I Trust Some Pseudonymous Person Saying Otherwise?
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: brucehoult on November 14, 2021, 08:21:10 pm
as reference, three different approaches
  • 6800 (only one CPU-register, therefore the ram is massively used)

Two accumulators and one index register.

The concept of ABI hadn't really formed in 6800 / 6502 / 8080 / z80 days and very little code was written using compiled languages. Assembly language programmers felt free to make up whatever calling convention they wanted on a function by function basis, but they definitely used registers as much as they could, certainly for the most low-level functions.

Quote
  • 68k (eight registers for data, eight registers for address, compromise between stack and registers)

The 68000 and x86 calling conventions I remember passed all arguments on the stack, and only return value in a register. Same with PDP-11 and VAX.

If ARM with 16 registers (kind of 8 + 8 for Thumb, though in a different way to 68k) can manage to pass 4 arguments in registers, and x86_64 with 16 registers can manage to pass 4 (Windows) or 6 (everything else) arguments in registers when 68k and VAX should have been able to also. But somehow no one thought of that in the late 1970s.

68k could easily have used, for example, up to three data registers and three address registers for function arguments.
[/list]
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: SiliconWizard on November 14, 2021, 08:32:07 pm
The 68000 and x86 calling conventions I remember passed all arguments on the stack, and only return value in a register. Same with PDP-11 and VAX.

Well seeing how the x86_64 ABI is different for Windows and SysV, I was precisely wondering if it wasn't just for legacy reasons on Windows, coming from the x86 era?
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: DiTBho on November 14, 2021, 08:51:41 pm
Two accumulators and one index register.

I do often program with 68HC11 in assembly. When I use the Avocet C compiler I see that "push" and "pop" uses don't use A(8bit), B(8bit) and A+B=D (16bit) in the same way to pass arguments to a function and, and practically you don't have all the instructions able to use nor B neither IX/IY; the ISA is not orthogonal and the register A is the most supported and used.

Practically in my assembly sources I only use reg A for that. It makes things neat.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: DiTBho on November 14, 2021, 09:08:56 pm
The 68000 and x86 calling conventions I remember passed all arguments on the stack, and only return value in a register. Same with PDP-11 and VAX.

My Avocet and Sierra C compilers have two working models

My IDT-C for MIPS64 C compiler has similar working models
The returned values can be structured.

I find it very nice!
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: DiTBho on November 14, 2021, 09:11:55 pm
(this only works for leaf-functions that doesn't need any next calling.
Otherwise they have to save and restore things on the stack before and after the function call.)
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: NorthGuy on November 14, 2021, 09:23:34 pm
Windows x64 passes up to four 64-bit aggregate arguments in standard registers, and up to four floating-point xmm vectors in xmm registers.

It is more "or" than "and". It passes up to 4 parameters (whether integer or floating-point) in registers. Moreover, the caller must reserve 32 bytes of stack space, which is intended as a place where the callee can save the parameters if needed. Such dedicated space might be useful if the callee wants to produce a pointer to the parameter and pass it somewhere.

Or you can use this space for temporaries, same as you would use the space below the stack pointer on Linux.

Well seeing how the x86_64 ABI is different for Windows and SysV, I was precisely wondering if it wasn't just for legacy reasons on Windows, coming from the x86 era?

No. Nothing in common. There wasn't much of ABI on x86. It was mostly stack based. And there was no single standard. There were C calling conventions, Pascal calling conventions, but also Fast calling conventions which used register - but it was only 7 accessible registers anyway. This was especially weird for C which used C calling conventions for internal functions and Pascal calling conventions to call Windows API. And when calling a DLL you would have to know which calling conventions it uses. At least x64 doesn't have that.

Using registers surely makes things much faster, but not always. Variadic functions become a nightmare, while they were very efficient with the stack.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: cfbsoftware on November 14, 2021, 10:45:53 pm
as reference, three different approaches
  • 6800 (only one CPU-register, therefore the ram is massively used)
  • 68k (eight registers for data, eight registers for address, compromise between stack and registers)
  • RISCV (31 registers, if the structure is small enough, it's passed via registers)
For those interested in the various arguments for and against, register and / or stack usage when implementing a programming language on CISC and RISC machines see
ETH Technical Report 174: The Oberon System Family

https://www.research-collection.ethz.ch/handle/20.500.11850/68908 (https://www.research-collection.ethz.ch/handle/20.500.11850/68908)

It provides a detailed comparison of the various code-generation techniques used for the NS32000, MC68020, SPARC, MIPS, RS/6000 architectures to implement a portable Oberon compiler in the 1990's.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: brucehoult on November 15, 2021, 02:23:46 am
For those interested in the various arguments for and against, register and / or stack usage when implementing a programming language on CISC and RISC machines see
ETH Technical Report 174: The Oberon System Family

https://www.research-collection.ethz.ch/handle/20.500.11850/68908 (https://www.research-collection.ethz.ch/handle/20.500.11850/68908)

It provides a detailed comparison of the various code-generation techniques used for the NS32000, MC68020, SPARC, MIPS, RS/6000 architectures to implement a portable Oberon compiler in the 1990's.

I like Wirth's language designs in general, but especially Oberon. It's simplistic is good ways. The compiler generates quite decent code very very quickly.

Probably the most out of date thing in that paper (and compiler) is the idea that what the programmer calls a "variable" has a fixed location throughout execution of a function -- for example, the idea that a variable lives either in a particular register or at a particular place in the stack frame. Modern practice is to call it a new variable (an "SSA variable" or a "live range") every time it is assigned to -- and sometimes to split the live range at certain important points. All of which allows the register allocator to do a better job, sometimes at the cost of moving a value between memory and a register or between different registers. Conversely, of course, different variables with non-overlapping lifetimes can share the same register at different times.
Title: Re: [C] Ow, pointers are making my brain hurt...
Post by: SiliconWizard on November 15, 2021, 02:43:25 am
Yes of course, it's a bit out-of-date in that area. And regarding register vs stack use, in the end they take a pretty simple approach, which is roughly to favor the stack for target processors with few general-purpose registers, and use registers more for those with more GP registers...

But otherwise yes, it's always a pleasure to read about anything Wirth has done or supervised.