Oh I remember this from the classic SO post, but it was just presented as a funny trick. I never realized that it solves the issue mentioned in the article and there is a legitimate use-case for post-decrement in the comparison itself.
if `i` ever gets modified inside the loop, and skips 0 (e.g., got decremented twice when it is at 1), then you might end up with an infinite loop?
Usually, this won't happen of course, but because `i` is not declared inside a for-loop, it must have been declared outside, and thus might even look like a regular variable that could be used by some "clever" coding to multipurpose it!
In any case, it's a niche worry i have, so i usually don't do it if i know there's a for loop that would do.
There is nothing clearer about your way unless someone is just starting with C.
'while (i--)k is a shortcut, which might seem terse when one first encounters it, it is a well-established and trivial to understand construct. On the other hand, your embellishment only adds pointless friction when reading the code.
I don't think it's too clever at all. Notably it works with both signed and unsigned i. Frankly if I see anyone doing anything other than this, they're wrong.
The part I like is starting from the count rather than count-1. If n is 0, `i --> 0` never enters. Do `size_t i = n - 1` and a zero-length array wraps to SIZE_MAX first, which is the case that actually runs off the front.
As someone who only writes C or C++ occasionally, I'd consider this a good submission for an obfuscated C contest.
It really does look simple at first glance.
Then you start to wonder what the stop condition is.
Then you realize the i-- is the stop condition.
Then you wonder how the whole thing works at all.
(The description explained the trick well, and the construct makes sense once you understand it, but I still think this is one example of something that is "too simple" and becomes cryptic)
After all the stuff you hear about undefined behavior, I'm also honestly surprised that wraparound on unsigned types is not undefined behavior.
Hard disagree. Obfuscated code is code that does something different from what it seems to be doing at first glance. This backwards loop does exactly the thing you'd expect it to do - in code review I'd only double-check whether it's actually correct - I can see someone making a mistake here and creating an infinite loop.
> I'm also honestly surprised that wraparound on unsigned types is not undefined behavior.
I appreciate Zig's approach, where the runtime complains when over/underflow occurs with the regular `+` operator but explicitly wraps with `+%` (or even saturates with `+|`). Takes a little getting used to (that and explicit unsigned/signed casts) but it's much clearer.
> Now your index variable is signed, which can cause problems when used in expressions due to C's conversion rules.
I don't see this. What problems would those be?
> ssize_t is a POSIX thing so you'd have to define it yourself on windows.
True. But I think "int" would be the right choice anyway in almost all case.
> If your type is 32 bit, you run into promotion problems and might even run into truncation problems.
What promotion problems? If your loop exceeds 2G than yes, you run into overflow (not for backwards loop though). But with a signed type you can automatically get a trap or diagnostic for overflow. This is far easier to debug and protect against compared to mistakes with unsigned types.
I was unclear in the article, but in my defense I wrote it in a hurry on the couch after seeing the convoluted backwards loop an llm had extruded. I meant if you are actually doing something with the index variable other than indexing. Mixing signed and unsigned types, possibly of different sizes, has surprising behavior to many programmers who don’t have the “usual arithmetic conversions” memorized. If you’re working in size_t the result simplifies: everything becomes size_t. And you generally have size_t anyway as you’re using the result of sizeof or an allocation size passed to your allocation function.
In general I want to avoid signed types for counts and sizes as then you have to consider both boundary conditions which is easy to mess up.
I agree that mixing signed and unsigned types creates friction. After trying to use unsigned for sizes and indices for a while, I changed my mind and now use signed types almost everywhere, except for modulo arithmetic and bit masks, and I found this removes all problems I had before.
I don't think the usual arithmetic conversions are very problematic, IMHO there is a lot of FUD about this. The rule is also very simple: "Operands are converted to the higher ranked type but at least to the rank of int, and to the signed type if it covers all values of both types and otherwise to the unsigned version." This is the complete rule.
I like the your final version, but it is a bit too clever for my taste.
Ubsan is a misnomer here from a technical perspective. However overflow unsigned or not is unexpected in many cases. As such, sanitizing it (and many other hard to reason about behaviors) by default is great practice. For an example, when unsigned values are used to count, overflows from summing over 2**N often result in incorrect behavior. This may be incrementing or even counting beans.
We usually get away with ignoring unsigned rollover because most application counts don't exceed 2**32 or 2**64. However, many can exceed these values, and in those cases, explicitly handling this case in critical systems is often better than ignoring potentially erroneous behavior.
Another concern, some people might have about this is the performance implications or what to do when you want to allow overflow. In that case you can locally disable overflow protections.
That's the price of innovation, I guess; before C invented its "for"-loop, other languages had their equivalents generally looking like this:
FOR var = start [DOWN] TO end BY step DO ... END
No custom increment expressions, and "step" even usually was only allowed to be a constant (in which case, the language usually didn't have DOWN specifier and used the step's sign instead). Then Dennis Ritchie invented C, and ditched the traditional "do"-loop (Fortran started that construct with DO, so it was called DO loop; Algol used FOR instead, but most people preferred the Fortran's nomenclature... until C came to dominate, but that wouldn't happen for yet another 20 years) in favour of his invention because he felt that easily iterating through linked lists or strings was a much more pressing concern:
There is no right or wrong, only what works and what does not.
Obviously the author never fixed 10 y.o. bugs at 0430 hrs in a room full of people watching his every finger. In moments like that, the programmer wants to work with the most boring, straightforward code.
Artistry contests is sometging else. I like doing this (iteration [0..9] backwards):
The fact that such a trivial need require to go into such an esoteric details speak more about the qualities of C than the author proposal actually.
And the kind of situation you describe sure can happen, but if it's more than once in a team life time, there are organizational issues at stake which are far more of a concern than any accumulation of code artistry pushed border line with occultism.
If you want -3 -2 -1 0 and +3 +2 +1 0 from the same operation, you'll need something more complicated (either some nonsense to give you a signed stride/step value among other things — or perhaps just writing the damn loop twice instead of getting too clever.)
However, if you just want -1 to not iterate down to `std::numeric_limits<ssize_t>::min()` — and then invoke undefined behavior by signed integer underflow if `i` happens to be `ssize_t` instead of `size_t` — I'll note that the `i --> 0` will terminate immediately, unlike `i --` as otherwise seen. (unless you feed it `min()` exactly. Then it'll underflow a signed integer before the comparison, so that's still undefined behavior.)
This is why I prefer to use C++ and its reversed iterators. The rbegin and rend accessors are there in the containers from the beginning. It’s less error prone than the C version using indices. (Of course some situations require me to use indices, so I do use the i-->0 trick, but it is much rarer.)
It would be if pos were dereferenced in that state. However, since the postfix decrement operator is used in the inequality test, pos pointing to an undefined location is never used.
I would consider his version "not ideal code" in the sense that 99% of people reading it would go "huhh?" and be confused, they'd think that it has a typo because it isn't using the for-structure in the classic way, etc.
Code is not only for machines to read, it's for humans to read, and this one works but it doesn't communicate effectively to humans. It's great if you work alone, or if you work with a team that uses this type of loop and is used to it. But otherwise, I don't think it's ideal at all.
For the uninformed: counting backwards in a for loop is faster because comparing an unsigned integer against 0 is generally faster than comparing it against any other value.
It also says not to use `for (i >= 0)` because any unsigned integer is >= 0, and the loop will never end.
You should always check the compiled assembly with Compiler Explorer because compilers sometimes avoid certain optimizations for convoluted reasons, which are hard to predict.
> For the uninformed: counting backwards in a for loop is faster because comparing an unsigned integer against 0 is generally faster than comparing it against any other value.
No it's not. It generally makes no difference at all.
If you disagree, I dare you to write a benchmark that demonstrates a difference between forward and backward iteration on a modern CPU.
Hmm. I think both of you are both right and wrong!
That is, I claim that while it is no faster to execute a comparison against zero, it still can be faster to finish a loop at zero. This is because internal to the processor, the comparison and the branch are two separate ops. The comparison essentially does an arithmetic operation that sets a flag but throws away the result, and then the branch acts on it.
But if you finish at zero, the flag you are using is already set by the decrement, so no additional comparison op is necessary. Now normally this doesn't matter, since the processor is multiscalar with sufficient execution ports to fit in an extra comparison with no increase in total cycles spent. But in a tiny and highly optimized loop, occasionally you can get about 20% gain from having one fewer op to execute.
Getting this to actually happen can be complicated because modern processors do op-fusion that can "fuse" the CMP and branch back into something that can be executed on a single port. But DEC-JNZ can be fused too, which still gives it an advantage. More details on this here: https://chipswiki.org/Macro-operation_fusion
I quote from "Optimizing Software in C++" by Agner Fog:
"Comparing an integer to zero is sometimes more efficient than comparing it to any other number. Therefore, it is slightly more efficient to make a loop count down to zero than to make it count up to some positive value, n. But not if the loop counter is used as an array index. The data cache is optimized for accessing arrays forwards, not backwards."
Yet that text was written for older CPUs. Modern CPUs are better at prefetching backwards access patterns, but if you try a microbenchmark with backwards array traversal, you may get misleading results.
Agner Fog is also discussing CPU architecture, not high-level language. The whole point is that the optimizer _already_ takes knows these things and rewrites your code automatically. If you write a zero-to-100 loop in C, that doesn’t mean the assembly will literally show a value going from zero to 100; it may unroll, or reverse the loop order, or any weird combination of things that the optimizer has decided will be equivalent and fast.
That's not guaranteed. An array or variable could be modified by another thread, meaning that reversing the order of operations could produce a different result. The compiler cannot always assume that such transformations are safe, and that's why the compiler avoids implementing a lot of those optimizations that you expect to be done automagically.
So you cannot expect the compiler to automatically perform every possible optimization. The programmer still needs to understand how the code behaves and write it with performance in mind.
You may be perfectly sure that no other thread modifies the data, but the compiler doesn't know, so it avoids all kind of optimizations.
If you expect the compiler to perform those optimizations automatically, you still have to do things like declare certain variable variables and or function parameters as constant (since the compiler may otherwise have to assume that their values could be modified by side effects/threads, preventing certain optimizations), use specific attributes or annotations on functions, and provide specific compilation flags. That's more complex than simply coding the for loop backwards.
And the worst thing is that frequently the compiler assumes a constant could be modified anyway, and avoids optimizing just in case.
> ...counting backwards in a for loop is faster...
Maybe in 1990 ;)
(it's one of many cargo cult wisdom snippets that have become mostly obsolete once C compilers got proper optimizer passes and CPUs got all sorts of pipeling tricks)
A cargo cult wisdom snippet is the belief that compilers automagically apply every possible optimization. But they don't, because compilers don't know everything you know. Compilers have to assume that your code can be subject to side effects you never thought of, that would change the result of the code, so a lot of possible optimizations are avoided.
Well you check the compiler output and profile of course. But "generalized" performance advice like "use reverse loops" hasn't been relevant for a long time, it's a similar myth like "pre-increment is faster than post-increment".
His final solution is so tricky. I needed to re-read it a few times to understand the trick of using post decrement in the for loop condition. Personally, I would never write code like that. I would always find a way to use ssize_t.
This part was an interesting requirement:
> ssize_t is a POSIX thing so you'd have to define it yourself on windows.
That seems like a lot of mental gymnastics to avoid this very simple macro:
> i >=0 is tautologically true (modern compilers will warn).
Note that a tautology in language design is an opportunity. Here the compiler could see what the programmer meant and do the right thing using the overflow flag (though the way this is written doesn't favor this).
I'm not recommending, I'm just saying compilers traditionally have just thrown their hands in the air, kicked the developer around with a stupid error message and called it a day. Apparently only recently they started emitting a warning.
The "best" developer was the one who took the abuse the "best tools" gave him in stride. Nowadays, in their "good faith", compilers don't warn you when they detect undefined behavior. They then go on to reinterpret your code, to show you they read the standards book and you didn't. They never questioning if the standards are wrong or out of date, they don't give you a optional warning like "maybe you didn't intend this, but i'm doing things behind your back?". They placate to the legalistic people that don't actually do the job.
The amount of abuse the developer is willing to take has been the self selecting bias in the industry for many decades. It's made us the heroes of the misplaced comma, changing the meaning of a sentence. A symptom is the lack of women in our profession. I welcome AI for liberating us from what's been hell more often than not.
Well, the array indices are allowed to be any sorts of integers, and the standard is worded in such a way that such addition must work correctly without overflowing — provided, of course, that the array you're indexing into is large enough. So in practice, you're bounded by the width of size_t.
Oh, and did you know that the width of ptrdiff_t is not required to have any relation to the width of size_t? There is even an explicit provision in standard for the case when subtracting two pointers into the same very large array can't fit into ptrdiff_t:
When two pointers are subtracted, both shall point to elements of the same array object, or one past
the last element of the array object; the result is the difference of the subscripts of the two array
elements. The size of the result is implementation-defined, and its type (a signed integer type) is
ptrdiff_t defined in the <stddef.h> header. If the result is not representable in an object of that
type, the behavior is undefined.
So yeah; you should try to work strictly with indices only, they're guaranteed to be correctly addible to pointers no matter the sizes of your arrays.
C doesn't have an opinion, it allows negative indexing just fine when the index is a signed type (indexing-syntax is just syntax-sugar for pointer arithmetic).
Eh I don't know, the straightforward solution is still to use a signed loop counter (and signed indices) and only ever use unsigned integer types for bit twiddling. Also, enable `-Wsign-conversion` (it's not in -Wall -Wextra) to catch all the places where the compiler implicitly converts between signed and unsigned behind your back.
I myself write loops with goto. It gives precise control, allows to reuse the knowledge gained from operations inside the loop, avoids artificial "while true" and is generally more convenient. It is also makes the language simpler as you can forget 4 to 5 loop-related keywords and hard-to parse operators designed primarily to look nice in loop conditions while also modifying their arguments.
I know it's "too clever", but if I'm writing code no one else will ever see,
easy to read (if not understand), and correct!Oh I remember this from the classic SO post, but it was just presented as a funny trick. I never realized that it solves the issue mentioned in the article and there is a legitimate use-case for post-decrement in the comparison itself.
https://stackoverflow.com/questions/1642028/what-is-the-oper...
There is a reason postfix is the idiomatic way in C, it is the correct choice most of the time.
Swift (Lattner) was right, imo, to exclude both.
It's just spacing. X-- > 0 and X --> 0 are the same thing in C.
It's a well-known (ish) idiom that nobody uses. It's very entertaining though, for sure. In some circles it's known as the down-to operator.
I always use unsigned values and do:
It's more clear than the most idiomatic way, which is to drop the explicit comparison: All of it is better than the clunky old for-loop syntax.if `i` ever gets modified inside the loop, and skips 0 (e.g., got decremented twice when it is at 1), then you might end up with an infinite loop?
Usually, this won't happen of course, but because `i` is not declared inside a for-loop, it must have been declared outside, and thus might even look like a regular variable that could be used by some "clever" coding to multipurpose it!
In any case, it's a niche worry i have, so i usually don't do it if i know there's a for loop that would do.
There is nothing clearer about your way unless someone is just starting with C.
'while (i--)k is a shortcut, which might seem terse when one first encounters it, it is a well-established and trivial to understand construct. On the other hand, your embellishment only adds pointless friction when reading the code.
And from this day, all C code will look like this because LLMs scraped the most popular SO articles.
Praise to the old gods and the new, why?!
I don't think it's too clever at all. Notably it works with both signed and unsigned i. Frankly if I see anyone doing anything other than this, they're wrong.
Code like this is why i prefer human code more than llm.
Reading code should be enjoyable.
This is why we never escape the cycle of rebirth.
Always.
(Hi Ken!)
The part I like is starting from the count rather than count-1. If n is 0, `i --> 0` never enters. Do `size_t i = n - 1` and a zero-length array wraps to SIZE_MAX first, which is the case that actually runs off the front.
> for(size_t i = count; i--; ) { /* find needle */ }
As someone who only writes C or C++ occasionally, I'd consider this a good submission for an obfuscated C contest.
It really does look simple at first glance.
Then you start to wonder what the stop condition is.
Then you realize the i-- is the stop condition.
Then you wonder how the whole thing works at all.
(The description explained the trick well, and the construct makes sense once you understand it, but I still think this is one example of something that is "too simple" and becomes cryptic)
After all the stuff you hear about undefined behavior, I'm also honestly surprised that wraparound on unsigned types is not undefined behavior.
Hard disagree. Obfuscated code is code that does something different from what it seems to be doing at first glance. This backwards loop does exactly the thing you'd expect it to do - in code review I'd only double-check whether it's actually correct - I can see someone making a mistake here and creating an infinite loop.
> I'm also honestly surprised that wraparound on unsigned types is not undefined behavior.
I consider it common knowledge TBH
> I consider it common knowledge TBH
For a seasoned C developer, I’d agree with you, but parent declares himself
> As someone who only writes C or C++ occasionally
… so I think it’s fair enough to not know such details.
C semantics has a few details that often surprise non-regulars, despite being such a small and simple language.
I appreciate Zig's approach, where the runtime complains when over/underflow occurs with the regular `+` operator but explicitly wraps with `+%` (or even saturates with `+|`). Takes a little getting used to (that and explicit unsigned/signed casts) but it's much clearer.
Sounds quite interesting! I do prefer to be explicit in low level programming.
> Obfuscated code is code that does something different from what it seems to be doing at first glance.
What you're describing is called "underhanded" code, see The Underhanded C Contest [0].
[0]: https://underhanded-c.org/_page_id_2.html
> Now your index variable is signed, which can cause problems when used in expressions due to C's conversion rules.
I don't see this. What problems would those be?
> ssize_t is a POSIX thing so you'd have to define it yourself on windows.
True. But I think "int" would be the right choice anyway in almost all case.
> If your type is 32 bit, you run into promotion problems and might even run into truncation problems.
What promotion problems? If your loop exceeds 2G than yes, you run into overflow (not for backwards loop though). But with a signed type you can automatically get a trap or diagnostic for overflow. This is far easier to debug and protect against compared to mistakes with unsigned types.
I was unclear in the article, but in my defense I wrote it in a hurry on the couch after seeing the convoluted backwards loop an llm had extruded. I meant if you are actually doing something with the index variable other than indexing. Mixing signed and unsigned types, possibly of different sizes, has surprising behavior to many programmers who don’t have the “usual arithmetic conversions” memorized. If you’re working in size_t the result simplifies: everything becomes size_t. And you generally have size_t anyway as you’re using the result of sizeof or an allocation size passed to your allocation function.
In general I want to avoid signed types for counts and sizes as then you have to consider both boundary conditions which is easy to mess up.
I agree that mixing signed and unsigned types creates friction. After trying to use unsigned for sizes and indices for a while, I changed my mind and now use signed types almost everywhere, except for modulo arithmetic and bit masks, and I found this removes all problems I had before.
I don't think the usual arithmetic conversions are very problematic, IMHO there is a lot of FUD about this. The rule is also very simple: "Operands are converted to the higher ranked type but at least to the rank of int, and to the signed type if it covers all values of both types and otherwise to the unsigned version." This is the complete rule.
I like the your final version, but it is a bit too clever for my taste.
> True. But I think "int" would be the right choice anyway in almost all case.
It's the worst choice. int is 32-bit on most 64-bit architecture/compiler-versions that matter.
I know. This does not make it a bad choice.
I thought most people used the explicit version:
You can also do something like: to not have to use i-1. I believe the compiler optimizes that out anyway to be identical.I agree that more explicit is almost always better in C. It’s difficult enough as is :D
I would do it exactly like you suggest, but using an explicitly sized type like uint32_t instead.
If I write firmware or low-level code, I almost exclusively use integers with explicit bitwidth.
Yes, this is the best answer. This also works with `-fsanitize=unsigned-integer-overflow`.
Unsigned integer overflow is well defined, though. Not something that should be sanitized. A lot of code out there relies on it.
Ubsan is a misnomer here from a technical perspective. However overflow unsigned or not is unexpected in many cases. As such, sanitizing it (and many other hard to reason about behaviors) by default is great practice. For an example, when unsigned values are used to count, overflows from summing over 2**N often result in incorrect behavior. This may be incrementing or even counting beans.
One particularly famous example of a bug: https://projectzero.google/2023/01/exploiting-null-dereferen...
We usually get away with ignoring unsigned rollover because most application counts don't exceed 2**32 or 2**64. However, many can exceed these values, and in those cases, explicitly handling this case in critical systems is often better than ignoring potentially erroneous behavior.
Another concern, some people might have about this is the performance implications or what to do when you want to allow overflow. In that case you can locally disable overflow protections.
That's the price of innovation, I guess; before C invented its "for"-loop, other languages had their equivalents generally looking like this:
No custom increment expressions, and "step" even usually was only allowed to be a constant (in which case, the language usually didn't have DOWN specifier and used the step's sign instead). Then Dennis Ritchie invented C, and ditched the traditional "do"-loop (Fortran started that construct with DO, so it was called DO loop; Algol used FOR instead, but most people preferred the Fortran's nomenclature... until C came to dominate, but that wouldn't happen for yet another 20 years) in favour of his invention because he felt that easily iterating through linked lists or strings was a much more pressing concern: The downside, of course, is that now you can't straightforwardly writeThere is no right or wrong, only what works and what does not.
Obviously the author never fixed 10 y.o. bugs at 0430 hrs in a room full of people watching his every finger. In moments like that, the programmer wants to work with the most boring, straightforward code.
Artistry contests is sometging else. I like doing this (iteration [0..9] backwards):
size_t i = 9;
for ( ; i < 10; i -- ) ...
The fact that such a trivial need require to go into such an esoteric details speak more about the qualities of C than the author proposal actually.
And the kind of situation you describe sure can happen, but if it's more than once in a team life time, there are organizational issues at stake which are far more of a concern than any accumulation of code artistry pushed border line with occultism.
Those situations, albeit rare, are /highly instructive/; they shove the programmer's nose right into a misplaced artistry.
I prefer:
That's a certain kind of taste... I checked out your home page, game dev, check, and this work of art is worth linking (no irony, I love it):
http://maulingmonkey.com/brainfuck/
That looks like the brainfuck mandelbrot set by Erik Bosman, but the example is missing the header line with the credit. The one I saw came from here:
https://github.com/thaliaarchi/bfcorpus/blob/main/ebosman/ma...
Be sure to check out other files in that directory for how the program is assembled.
(Erik Bosman's mandelbrot set is a personal favorite of mine, very useful for benchmarking brainfuck interpreters)
Cute but wouldn't work with ssize_t :)
VHDL's array/vector syntax actually works a similar way:
-- gives 0, 1, 2, 3:
for i in 0 to 3 loop
-- gives 3, 2, 1, 0:
for i in 3 downto 0 loop
-- Source - https://stackoverflow.com/a/7642026 -- Posted by Kerrek SB -- Retrieved 2026-10-29, License - CC BY-SA 3.0
Does work with ssize_t, just not necessary.
Depends on your definition of "work".
For me it does not work because I'd want to use it with values < 0, but the for loop checks for > 0, so I would have to modify that first.
If you want -3 -2 -1 0 and +3 +2 +1 0 from the same operation, you'll need something more complicated (either some nonsense to give you a signed stride/step value among other things — or perhaps just writing the damn loop twice instead of getting too clever.)
However, if you just want -1 to not iterate down to `std::numeric_limits<ssize_t>::min()` — and then invoke undefined behavior by signed integer underflow if `i` happens to be `ssize_t` instead of `size_t` — I'll note that the `i --> 0` will terminate immediately, unlike `i --` as otherwise seen. (unless you feed it `min()` exactly. Then it'll underflow a signed integer before the comparison, so that's still undefined behavior.)
I do this in another way:
It's in my opinion better than using i-- as condition, since I generally avoid using results of -- and ++, because doing so is bug-prone.This is why I prefer to use C++ and its reversed iterators. The rbegin and rend accessors are there in the containers from the beginning. It’s less error prone than the C version using indices. (Of course some situations require me to use indices, so I do use the i-->0 trick, but it is much rarer.)
Why not use the C/C++ idiom of pointer math?
Most compilers will be able to optimize the index version into this.
Isn't decrementing pos beyond start of haystack technically undefined behaviour?
It would be if pos were dereferenced in that state. However, since the postfix decrement operator is used in the inequality test, pos pointing to an undefined location is never used.
I would consider his version "not ideal code" in the sense that 99% of people reading it would go "huhh?" and be confused, they'd think that it has a typo because it isn't using the for-structure in the classic way, etc.
Code is not only for machines to read, it's for humans to read, and this one works but it doesn't communicate effectively to humans. It's great if you work alone, or if you work with a team that uses this type of loop and is used to it. But otherwise, I don't think it's ideal at all.
As long as you make sure the end points are valid, this is most intuitive for me. Just using i as a backwards step.
for (int i = 1; i <= N; i++) {
Conventionally, loops with <= is very much a Pascal thing, not C.
This fails on overflow: if N == INT_MAX then your loop doesn't terminate and actually invokes undefined behavior in C.
The safe way to write it is:
(or with size_t instead of int depending on the type of N)For the uninformed: counting backwards in a for loop is faster because comparing an unsigned integer against 0 is generally faster than comparing it against any other value.
It also says not to use `for (i >= 0)` because any unsigned integer is >= 0, and the loop will never end.
Optimizers eat stuff like this up. It'll restructure your loop however it wants.
You should always check the compiled assembly with Compiler Explorer because compilers sometimes avoid certain optimizations for convoluted reasons, which are hard to predict.
> For the uninformed: counting backwards in a for loop is faster because comparing an unsigned integer against 0 is generally faster than comparing it against any other value.
No it's not. It generally makes no difference at all.
If you disagree, I dare you to write a benchmark that demonstrates a difference between forward and backward iteration on a modern CPU.
Hmm. I think both of you are both right and wrong!
That is, I claim that while it is no faster to execute a comparison against zero, it still can be faster to finish a loop at zero. This is because internal to the processor, the comparison and the branch are two separate ops. The comparison essentially does an arithmetic operation that sets a flag but throws away the result, and then the branch acts on it.
But if you finish at zero, the flag you are using is already set by the decrement, so no additional comparison op is necessary. Now normally this doesn't matter, since the processor is multiscalar with sufficient execution ports to fit in an extra comparison with no increase in total cycles spent. But in a tiny and highly optimized loop, occasionally you can get about 20% gain from having one fewer op to execute.
Getting this to actually happen can be complicated because modern processors do op-fusion that can "fuse" the CMP and branch back into something that can be executed on a single port. But DEC-JNZ can be fused too, which still gives it an advantage. More details on this here: https://chipswiki.org/Macro-operation_fusion
Now does anyone want to finish the "ground speed readout" routine (https://theaviationgeekclub.com/sr-71-blackbird-pilot-tells-...) by pointing out actually how with modern processors I'm actually wrong too?
I quote from "Optimizing Software in C++" by Agner Fog:
"Comparing an integer to zero is sometimes more efficient than comparing it to any other number. Therefore, it is slightly more efficient to make a loop count down to zero than to make it count up to some positive value, n. But not if the loop counter is used as an array index. The data cache is optimized for accessing arrays forwards, not backwards."
Yet that text was written for older CPUs. Modern CPUs are better at prefetching backwards access patterns, but if you try a microbenchmark with backwards array traversal, you may get misleading results.
Agner Fog is also discussing CPU architecture, not high-level language. The whole point is that the optimizer _already_ takes knows these things and rewrites your code automatically. If you write a zero-to-100 loop in C, that doesn’t mean the assembly will literally show a value going from zero to 100; it may unroll, or reverse the loop order, or any weird combination of things that the optimizer has decided will be equivalent and fast.
That's not guaranteed. An array or variable could be modified by another thread, meaning that reversing the order of operations could produce a different result. The compiler cannot always assume that such transformations are safe, and that's why the compiler avoids implementing a lot of those optimizations that you expect to be done automagically.
So you cannot expect the compiler to automatically perform every possible optimization. The programmer still needs to understand how the code behaves and write it with performance in mind.
You may be perfectly sure that no other thread modifies the data, but the compiler doesn't know, so it avoids all kind of optimizations.
If you expect the compiler to perform those optimizations automatically, you still have to do things like declare certain variable variables and or function parameters as constant (since the compiler may otherwise have to assume that their values could be modified by side effects/threads, preventing certain optimizations), use specific attributes or annotations on functions, and provide specific compilation flags. That's more complex than simply coding the for loop backwards.
And the worst thing is that frequently the compiler assumes a constant could be modified anyway, and avoids optimizing just in case.
> ...counting backwards in a for loop is faster...
Maybe in 1990 ;)
(it's one of many cargo cult wisdom snippets that have become mostly obsolete once C compilers got proper optimizer passes and CPUs got all sorts of pipeling tricks)
A cargo cult wisdom snippet is the belief that compilers automagically apply every possible optimization. But they don't, because compilers don't know everything you know. Compilers have to assume that your code can be subject to side effects you never thought of, that would change the result of the code, so a lot of possible optimizations are avoided.
Well you check the compiler output and profile of course. But "generalized" performance advice like "use reverse loops" hasn't been relevant for a long time, it's a similar myth like "pre-increment is faster than post-increment".
His final solution is so tricky. I needed to re-read it a few times to understand the trick of using post decrement in the for loop condition. Personally, I would never write code like that. I would always find a way to use ssize_t.
This part was an interesting requirement:
That seems like a lot of mental gymnastics to avoid this very simple macro: Read about the Win32 macro SSIZE_T here: https://learn.microsoft.com/en-us/windows/win32/winprog/wind...To me, this solution is more readable and maintainable:
Alternatively, you can use a while loop where the condition appears at the end of the loop:> for(size_t i = count - 1; i >= 0; i--){
> i >=0 is tautologically true (modern compilers will warn).
Note that a tautology in language design is an opportunity. Here the compiler could see what the programmer meant and do the right thing using the overflow flag (though the way this is written doesn't favor this).
I'm not recommending, I'm just saying compilers traditionally have just thrown their hands in the air, kicked the developer around with a stupid error message and called it a day. Apparently only recently they started emitting a warning.
The "best" developer was the one who took the abuse the "best tools" gave him in stride. Nowadays, in their "good faith", compilers don't warn you when they detect undefined behavior. They then go on to reinterpret your code, to show you they read the standards book and you didn't. They never questioning if the standards are wrong or out of date, they don't give you a optional warning like "maybe you didn't intend this, but i'm doing things behind your back?". They placate to the legalistic people that don't actually do the job.
The amount of abuse the developer is willing to take has been the self selecting bias in the industry for many decades. It's made us the heroes of the misplaced comma, changing the meaning of a sentence. A symptom is the lack of women in our profession. I welcome AI for liberating us from what's been hell more often than not.
too bad, no one write code anymore. a nice trick sadly become irrelevant .
I've been using this method for more than 30 years!
> Now your index variable is signed, which can cause problems when used in expressions due to C's conversion rules.
Array indices are already signed. This shouldn't be a problem.
No they aren't.
Well, the array indices are allowed to be any sorts of integers, and the standard is worded in such a way that such addition must work correctly without overflowing — provided, of course, that the array you're indexing into is large enough. So in practice, you're bounded by the width of size_t.
Oh, and did you know that the width of ptrdiff_t is not required to have any relation to the width of size_t? There is even an explicit provision in standard for the case when subtracting two pointers into the same very large array can't fit into ptrdiff_t:
So yeah; you should try to work strictly with indices only, they're guaranteed to be correctly addible to pointers no matter the sizes of your arrays.C doesn't have an opinion, it allows negative indexing just fine when the index is a signed type (indexing-syntax is just syntax-sugar for pointer arithmetic).
Eh I don't know, the straightforward solution is still to use a signed loop counter (and signed indices) and only ever use unsigned integer types for bit twiddling. Also, enable `-Wsign-conversion` (it's not in -Wall -Wextra) to catch all the places where the compiler implicitly converts between signed and unsigned behind your back.
TL;DR: "alomost always signed!" :)
[flagged]
[flagged]
[dead]
I myself write loops with goto. It gives precise control, allows to reuse the knowledge gained from operations inside the loop, avoids artificial "while true" and is generally more convenient. It is also makes the language simpler as you can forget 4 to 5 loop-related keywords and hard-to parse operators designed primarily to look nice in loop conditions while also modifying their arguments.