[C] Ow, pointers are making my brain hurt... - Page 1
Forum section: electronics / general

author: HwAoRrDk
author_rank: Super Contributor
timestamp: November 09, 2021, 09:57:57 pm
content: 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: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.

author: cfbsoftware
author_rank: Regular Contributor
timestamp: November 09, 2021, 10:29:42 pm
content: 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 ArraysofThe C Programming Languageby Kernighan and Ritchie. It's less than 30 pages. If you haven't got the book already then go no further until you have.

author: SiliconWizard
author_rank: Super Contributor
timestamp: November 09, 2021, 10:36:52 pm
content: Quote from: HwAoRrDk on November 09, 2021, 09:57:57 pmYes this is correct.

author: rstofer
author_rank: Super Contributor
timestamp: November 09, 2021, 10:53:44 pm
content: Quote from: HwAoRrDk on November 09, 2021, 09:57:57 pmprintf() is your friend. Make up some sample cases and print the results.

author: golden_labels
author_rank: Super Contributor
timestamp: November 10, 2021, 03:22:21 am
content: No trying to combine as much unrelated operations as possible in a single expression may help:Code:n = **in;++*in;Or even:Code:uint8_t* buf = *in;n = *buf;*in = buf + 1;

author: Kjelt
author_rank: Super Contributor
timestamp: November 10, 2021, 07:17:34 am
content: 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.

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 10, 2021, 10:34:32 am
content: 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 " a pointer to".Thus,const char *volatile p;reads as"pis volatile, a pointer to const char", meaning that the value ofp, 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 saychar *sas a parameter, can modify both the pointersand 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 whysizeof(array)/sizeof(*array)returns the number of elements in arrayarray, 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 correctintuitionthat 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:const char *skip_whitespace(const char *src){if (src) {while (isspace((unsigned char)(*src)))src++;}return src;}or sayCode: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: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 eithertrim_to_spaces(stringvar);orlen = 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 toCode: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 beCode: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: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: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;}}

author: Kjelt
author_rank: Super Contributor
timestamp: November 10, 2021, 11:44:03 am
content: Quote from: Nominal Animal on November 10, 2021, 10:34:32 amThe 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)

author: mfro
author_rank: Regular Contributor
timestamp: November 10, 2021, 12:02:48 pm
content: Quote from: HwAoRrDk on November 09, 2021, 09:57:57 pm"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).

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 10, 2021, 12:26:54 pm
content: Quote from: Kjelt on November 10, 2021, 11:44:03 amTrue.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 theisspace((unsigned char)(*src))is important, as without the(unsigned char)cast, it will fail on non-ASCII characters on OSes and architectures wherecharis 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.)Quote from: mfro on November 10, 2021, 12:02:48 pmVery 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.

author: DavidAlfa
author_rank: Super Contributor
timestamp: November 10, 2021, 02:32:49 pm
content: I've done that recently, yes, can be done, but be very careful when modifying it...Code:Void something (uint8_t **ptr){// Increase pointer pointed by our pointer*ptr = *ptr+1;// *ptr++ doesn't work.}

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 10, 2021, 03:20:27 pm
content: 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.)

author: DavidAlfa
author_rank: Super Contributor
timestamp: November 10, 2021, 05:05:47 pm
content: Nice explanationEverytime I get such specific in-depth details I feel like "Unga bunga, me know program thing"

author: HwAoRrDk
author_rank: Super Contributor
timestamp: November 10, 2021, 05:07:21 pm
content: 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: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: KjeltThis 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 AnimalThe 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.

author: SiliconWizard
author_rank: Super Contributor
timestamp: November 10, 2021, 06:18:03 pm
content: Quote from: DavidAlfa on November 10, 2021, 02:32:49 pm'*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!

author: DavidAlfa
author_rank: Super Contributor
timestamp: November 10, 2021, 08:30:31 pm
content: 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"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?

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 12, 2021, 10:56:07 am
content: Quote from: HwAoRrDk on November 10, 2021, 05:07:21 pmDon'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.Quote from: HwAoRrDk on November 10, 2021, 05:07:21 pmOr, 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 toLinux man-pages online(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.)

author: brucehoult
author_rank: Super Contributor
timestamp: November 12, 2021, 12:14:47 pm
content: Quote from: HwAoRrDk on November 09, 2021, 09:57:57 pmIt'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:#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:foo:lw a5,(a0) # fetch the pointer to the buffer from memoryaddi a4,a5,1 # increment the pointer valuesw a4,(a0) # store the incremented value back to the original pointerlbu a4,(a5) # load the byte pointed to by the original value of the pointerlui a5,%hi(n) # load the high bits of the address of nsb a4,%lo(n)(a5) # store the byte from the buffer into the global nretn:.zero 1An 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/foz6af9nqCode:#include <stdint.h>uint8_t n;uint8_t* foo(uint8_t *in) {n = *in++;return in;}This produces much less machine code:Code:foo:lbu a4,(a0) # get a byte from the bufferlui a5,%hi(n) # load the high bits of the address of nsb a4,%lo(n)(a5) # store the byte to naddi a0,a0,1 # add 1 to the pointer and return itretn:.zero 1This 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.

author: SiliconWizard
author_rank: Super Contributor
timestamp: November 13, 2021, 12:32:06 am
content: 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: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:foo:lbu a5,0(a0)addi sp,sp,-32addi a0,a0,1sb a5,0(a1)addi a1,a1,1addi sp,sp,32jr raNot 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)

author: brucehoult
author_rank: Super Contributor
timestamp: November 13, 2021, 01:50:21 am
content: Quote from: SiliconWizard on November 13, 2021, 12:32:06 amYes, 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.QuoteRight.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).QuoteBug. 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.

author: SiliconWizard
author_rank: Super Contributor
timestamp: November 13, 2021, 01:57:11 am
content: Quote from: brucehoult on November 13, 2021, 01:50:21 amYes, 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 from: brucehoult on November 13, 2021, 01:50:21 amThought so. I'm a bit disappointed currently with GCC for RISC-V. I think it was going better a year or two ago.Quote from: brucehoult on November 13, 2021, 01:50:21 amWell, 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:foo:movdqu (%rdx), %xmm0movq %xmm0, %rdxmovhlps %xmm0, %xmm1paddq .LC0(%rip), %xmm0movq %rcx, %raxmovzbl (%rdx), %ecxmovq %xmm1, %rdxmovups %xmm0, (%rax)movb %cl, (%rdx)retYeah. OK.

author: brucehoult
author_rank: Super Contributor
timestamp: November 13, 2021, 02:59:31 am
content: ARMv7 gcc ... all in memory...Code:foo:sub sp, sp, #8add ip, sp, #8stmdb ip, {r1, r2}ldrb ip, , #1 @ zero_extendqisi2strb ip, , #1str r1, str r2, [r0, #4]add sp, sp, #8bx lrARMv7 Clang ... cleaner but return struct it still in memory..Code:foo:ldrb r3, , #1strb r3, , #1stm r0, {r1, r2}bx lrARMv8 (er, Aarch64) gcc...Code:foo:ldrb w2, , 1strb w2, , 1retBetter.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.

author: PlainName
author_rank: Super Contributor
timestamp: November 13, 2021, 09:02:19 pm
content: QuoteBut 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.

author: brucehoult
author_rank: Super Contributor
timestamp: November 13, 2021, 09:23:40 pm
content: Quote from: dunkemhigh on November 13, 2021, 09:02:19 pm"pass back the updated pointer"Achieving the same result while doing less work is the point.

author: PlainName
author_rank: Super Contributor
timestamp: November 13, 2021, 09:37:50 pm
content: 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?

author: SiliconWizard
author_rank: Super Contributor
timestamp: November 13, 2021, 09:41:41 pm
content: Quote from: dunkemhigh on November 13, 2021, 09:02:19 pmNo, 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.

author: NorthGuy
author_rank: Super Contributor
timestamp: November 13, 2021, 09:54:08 pm
content: Quote from: SiliconWizard on November 13, 2021, 01:57:11 amOf 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.

author: PlainName
author_rank: Super Contributor
timestamp: November 13, 2021, 10:03:48 pm
content: QuoteIt doesn't. It doespartof it. More elegantly perhaps, but there is that little bit missing.QuoteExactly. 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.

author: SiliconWizard
author_rank: Super Contributor
timestamp: November 13, 2021, 10:23:12 pm
content: Quote from: dunkemhigh on November 13, 2021, 10:03:48 pmWe 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.

author: SiliconWizard
author_rank: Super Contributor
timestamp: November 13, 2021, 10:54:44 pm
content: Quote from: NorthGuy on November 13, 2021, 09:54:08 pmThat'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:The ﬁrst six arguments to a function are passed in registers. Any additional arguments are passedon the stack in the memory-argument area (see Figure 2). The %rax register is used to return theﬁrst 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.

author: brucehoult
author_rank: Super Contributor
timestamp: November 13, 2021, 10:59:58 pm
content: Quote from: dunkemhigh on November 13, 2021, 09:37:50 pmOK, 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 memcpyhttps://godbolt.org/z/ojjzYzT7G:Code:#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:copyAchar: # @copyAcharlb a3, 0(a1)addi a1, a1, 1addi a2, a0, 1sb a3, 0(a0)mv a0, a2retmymemcpy: # @mymemcpyaddi sp, sp, -16sw ra, 12(sp) # 4-byte Folded Spillsw s0, 8(sp) # 4-byte Folded Spillbeqz a2, .LBB1_3mv s0, a2.LBB1_2: # =>This Inner Loop Header: Depth=1addi s0, s0, -1call copyAcharbnez s0, .LBB1_2.LBB1_3:lw s0, 8(sp) # 4-byte Folded Reloadlw ra, 12(sp) # 4-byte Folded Reloadaddi sp, sp, 16retNotice 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 codehttps://godbolt.org/z/W3v5WPWd3:Code:#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:copyAchar: # @copyAcharlw a2, 0(a1)addi a3, a2, 1sw a3, 0(a1)lw a1, 0(a0)lb a2, 0(a2)addi a3, a1, 1sw a3, 0(a0)sb a2, 0(a1)retmymemcpy: # @mymemcpyaddi sp, sp, -16sw ra, 12(sp) # 4-byte Folded Spillsw s0, 8(sp) # 4-byte Folded Spillsw a0, 4(sp)sw a1, 0(sp)beqz a2, .LBB1_3mv s0, a2.LBB1_2: # =>This Inner Loop Header: Depth=1addi s0, s0, -1addi a0, sp, 4mv a1, spcall copyAcharbnez s0, .LBB1_2.LBB1_3:lw s0, 8(sp) # 4-byte Folded Reloadlw ra, 12(sp) # 4-byte Folded Reloadaddi sp, sp, 16retNow 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.

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 13, 2021, 11:05:20 pm
content: Quote from: NorthGuy on November 13, 2021, 09:54:08 pmWell, actually bothSysV AMD64 ABI(as used on Linux) andARM ABIsupport returning a structure with two register-sized elements.That is,Code: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 toCode:squarepair:mov rax, rdimov rdx, rsiimul rax, rdiimul rdx, rsiretand armv7-a clang10 toCode:squarepair:mul r3, r2, r2mul r2, r1, r1stm r0, {r2, r3}bx lrWhat 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).

author: brucehoult
author_rank: Super Contributor
timestamp: November 13, 2021, 11:28:52 pm
content: 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:squarepair: # @squarepairmul a0, a0, a0mul a1, a1, a1retCode:squarepair: // @squarepairmul x0, x0, x0mul x1, x1, x1retYou can only tell which is which by knowing what register names they use :-)

author: PlainName
author_rank: Super Contributor
timestamp: November 13, 2021, 11:35:31 pm
content: QuoteYou 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:QuoteThank 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.

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 13, 2021, 11:41:55 pm
content: Crap, me fail once again. ARMv8a does compile toCode:squarepair: // @squarepairmul x0, x0, x0mul x1, x1, x1retand RISC-V rv32gc and rv64gc using Clang toCode:squarepair: # @squarepairmul a0, a0, a0mul a1, a1, a1retas expected.Now, where can I get armeabi-v7a procedure call spec? Ah yes,IHI 0042J. Okay,Code:typedef long pair __attribute__((vector_size (2 * sizeof (long))));pair squarepair(const pair p){return (pair){ p*p, p*p };}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 surewhenI would bother, though. Probably never; it's not like temporary memory access isthatcostly (compared to the code complexity and ABI dependency generated).

author: brucehoult
author_rank: Super Contributor
timestamp: November 13, 2021, 11:56:27 pm
content: Quote from: Nominal Animal on November 13, 2021, 11:41:55 pmFunnily enough, Googling your words returnshttps://developer.arm.com/documentation/ihi0042/latestComposite 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 r1Composite 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.

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 14, 2021, 12:36:47 am
content: (Sorry, didn't see your answer as I was editing mine. Did some godbolt.org testing.)Quote from: brucehoult on November 13, 2021, 11:56:27 pmMe too. I really don't like the Cerrnoconvention; 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 dataandand 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 typeNUMTYPE, I use eitherint parse_NUMTYPE(const char *from,NUMTYPE*to);returning 0 if success, nonzero errno error code otherwise, if thefromstring should be a complete number without any trailing garbage, orconst char *parse_NUMTYPE(const char *from,NUMTYPE*to);returning a pointer to the first unparsed character, orNULLif 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.

author: SiliconWizard
author_rank: Super Contributor
timestamp: November 14, 2021, 12:41:25 am
content: Quote from: Nominal Animal on November 13, 2021, 11:05:20 pmAh, 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:movzbl (%rdi), %eaxleaq 1(%rsi), %rdxmovb %al, (%rsi)leaq 1(%rdi), %raxretwhich 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.

author: SiliconWizard
author_rank: Super Contributor
timestamp: November 14, 2021, 01:14:52 am
content: Quote from: Nominal Animal on November 14, 2021, 12:36:47 amOh, 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:#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;elseRet.value = sqrt(x);return Ret;}

author: NorthGuy
author_rank: Super Contributor
timestamp: November 14, 2021, 04:10:50 am
content: Quote from: Nominal Animal on November 13, 2021, 11:05:20 pmWindows x64 ABI is different - different registers are used, different rules.

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 14, 2021, 08:28:10 am
content: Quote from: NorthGuy on November 14, 2021, 04:10:50 amTrue. All other OSes on x86-64 (actually, AMD64) use the SysV ABI.

author: PlainName
author_rank: Super Contributor
timestamp: November 14, 2021, 11:17:44 am
content: QuoteIsn'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.

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 14, 2021, 11:32:40 am
content: Quote from: dunkemhigh on November 14, 2021, 11:17:44 amNo.Consider this:Code: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), thecallerreserves 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, theactualstructure returned therefore has the caller scope, not the called function scope. So using the structure in the caller is perfectly okay. For example, inCode: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;}eithervis passed in registers, or the compiler passes a pointer tovwhen callingVec3i(), 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'scompletelydifferent than returning a pointer to a local (non-static) variable, which is a bug.)

author: brucehoult
author_rank: Super Contributor
timestamp: November 14, 2021, 11:55:15 am
content: Quote from: dunkemhigh on November 14, 2021, 11:17:44 amNo.C function arguments and results are passed by value. That is, (logically) they are copied in the process of passing them.

author: PlainName
author_rank: Super Contributor
timestamp: November 14, 2021, 01:39:55 pm
content: QuoteAh! Thank you

author: DiTBho
author_rank: Super Contributor
timestamp: November 14, 2021, 01:54:27 pm
content: as reference, three different approaches6800 (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)

author: SiliconWizard
author_rank: Super Contributor
timestamp: November 14, 2021, 05:57:19 pm
content: Quote from: brucehoult on November 14, 2021, 11:55:15 amAnd, a third no.

author: SiliconWizard
author_rank: Super Contributor
timestamp: November 14, 2021, 06:02:47 pm
content: Quote from: Nominal Animal on November 14, 2021, 08:28:10 amYep. 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.

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 14, 2021, 07:15:09 pm
content: Quote from: SiliconWizard on November 14, 2021, 06:02:47 pmI 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.

author: SiliconWizard
author_rank: Super Contributor
timestamp: November 14, 2021, 07:34:46 pm
content: 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.

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 14, 2021, 08:04:55 pm
content: Quote from: SiliconWizard on November 14, 2021, 07:34:46 pmNo, 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?

author: brucehoult
author_rank: Super Contributor
timestamp: November 14, 2021, 08:21:10 pm
content: Quote from: DiTBho on November 14, 2021, 01:54:27 pmTwo 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.QuoteThe 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.

author: SiliconWizard
author_rank: Super Contributor
timestamp: November 14, 2021, 08:32:07 pm
content: Quote from: brucehoult on November 14, 2021, 08:21:10 pmWell 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?

author: DiTBho
author_rank: Super Contributor
timestamp: November 14, 2021, 08:51:41 pm
content: Quote from: brucehoult on November 14, 2021, 08:21:10 pmI 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.

author: DiTBho
author_rank: Super Contributor
timestamp: November 14, 2021, 09:08:56 pm
content: Quote from: brucehoult on November 14, 2021, 08:21:10 pmMy Avocet and Sierra C compilers have two working modelsif the number of arguments and the number of values returned areless than or equal to 2(two registers, 32bit each), then the machine layer uses four registersotherwise it passes everything on the stackMy IDT-C for MIPS64 C compiler has similar working modelsif the number of arguments and the number of values returned areless than or equal to 4(four registers, 64bit each), then the machine layer uses two registersotherwise it passes everything on the stackThe returned values can be structured.I find it very nice!

author: DiTBho
author_rank: Super Contributor
timestamp: November 14, 2021, 09:11:55 pm
content: (this only works forleaf-functionsthat doesn't need any next calling.Otherwise they have to save and restore things on the stack before and after the function call.)

author: NorthGuy
author_rank: Super Contributor
timestamp: November 14, 2021, 09:23:34 pm
content: Quote from: Nominal Animal on November 14, 2021, 07:15:09 pmIt 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.Quote from: SiliconWizard on November 14, 2021, 08:32:07 pmNo. 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.

author: cfbsoftware
author_rank: Regular Contributor
timestamp: November 14, 2021, 10:45:53 pm
content: Quote from: DiTBho on November 14, 2021, 01:54:27 pmFor those interested in the various arguments for and against, register and / or stack usage when implementing a programming language on CISC and RISC machines seeETH Technical Report 174:The Oberon System Familyhttps://www.research-collection.ethz.ch/handle/20.500.11850/68908It 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.

author: brucehoult
author_rank: Super Contributor
timestamp: November 15, 2021, 02:23:46 am
content: Quote from: cfbsoftware on November 14, 2021, 10:45:53 pmI 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.

author: SiliconWizard
author_rank: Super Contributor
timestamp: November 15, 2021, 02:43:25 am
content: 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.