Skip to main content
GameDev.net gamedev.net
🔒 Locked

For-loop-insanity

Started by aregee Sep 2, 2014 at 4:31 AM 26 replies 8.9k views
Original Post
aregee
aregee

In a careless thought to make the data type as small as possible, the compiler complained (naturally) of me using a constant that was too big for the type I was testing against:


    for (uint8_t j = 0; j < (1 << 8); j++) {
        ...
    }

Which gave me the idea to come up with this masterpiece:


    for (uint8_t test = 0; test <= 255; test++) {
        printf("%i\n", test);
    }
Ravyne
Ravyne

Maybe more to the point -- for a local variable, one that will likely be allocated as a register (or on the stack, at worst), you don't really gain anything at all by making 'test' small -- there might be reasons of correctness that are worthwhile, but optimization is basically a non-factor. Maybe if the loop appears in a deeply-recursive function, or on a small, embedded system with 4k of memory or something, but not usually. Just use the natural word-size of the machine, usually plain old 'int' -- it will be as fast as (faster, more likely) than the smaller value.

throw table_exception("(? ???)? ? ???");
Bacterius
Bacterius

Classic mistake. There is a generic way to make it work, if you really want to, though:


uint8_t j = 0;

do
{
  printf("%i\n", j);
}
while (++j);

Which is pretty readable, since the while loop is clearly going to terminate as soon as j overflows. And like Ravyne said, it doesn't really make anything faster in general, that said I personally don't see this as an optimization tactic, but more as a type correctness thing, in other words, the loop variable is going to be used as an 8-bit unsigned integer, so let's not lie to ourselves, make it an 8-bit unsigned integer. But with this comes the responsibility of watching out for overflow and other low-level things which generally don't come up when simply using a sufficiently large signed integer like int, people will have their own opinions on that matter smile.png

edit: fixed the termination condition error, thanks

“If I understand the standard right it is legal and safe to do this but the resulting value could be anything.”
HappyCoder
HappyCoder

I think we all have those moments sometimes. Figuring out why those loops would never terminate could possibly make for a very frustrating debug session. biggrin.png

My current game project Platform RPG
DekuTree64
DekuTree64

Classic mistake. There is a generic way to make it work, if you really want to, though:


uint8_t j = 0;

do
{
  printf("%i\n", j);
}
while (j++);

Which is pretty readable, since the while loop is clearly going to terminate as soon as j overflows. And like Ravyne said, it doesn't really make anything faster in general, that said I personally don't see this as an optimization tactic, but more as a type correctness thing, in other words, the loop variable is going to be used as an 8-bit unsigned integer, so let's not lie to ourselves, make it an 8-bit unsigned integer. But with this comes the responsibility of watching out for overflow and other low-level things which generally don't come up when simply using a sufficiently large signed integer like int, people will have their own opinions on that matter smile.png

Don't you mean ++j?

I just tried it on ARM7, which has only 32-bit registers, and it compiled to a compare with 256, branch if not equal. So it doesn't actually force the wrapping, but did get the logic correct, and no different than using an int and comparing with 256.

Ravyne
Ravyne
Classic mistake. There is a generic way to make it work, if you really want to, though:

uint8_t j = 0;

do
{
printf("%i\n", j);
}
while (j++);

To be picky, this works, but it also leaks the loop counter into the surrounding scope (as a "correctness issue"), and might contribute to register pressure for the duration of the scope (as an "optimization issue", though I would hope a modern compiler would do better). If there's a type correctness issue and also a condition at the boundary of that type (

throw table_exception("(? ???)? ? ???");
Ravyne
Ravyne




Don't you mean ++j?

Yes, he does :)

throw table_exception("(? ???)? ? ???");
Bacterius
Bacterius

Huh. I have ++j in my test source file, must've copied it wrong on the forum. Sorry about that, I will edit.




To be picky, this works, but it also leaks the loop counter into the surrounding scope (as a "correctness issue"), and might contribute to register pressure for the duration of the scope (as an "optimization issue", though I would hope a modern compiler would do better)

About leaking the loop counter in the surrounding scope, I am curious if there is an elegant way to achieve the same result with a for loop, so that the loop counter would be limited to the scope of the loop.

“If I understand the standard right it is legal and safe to do this but the resulting value could be anything.”
Ravyne
Ravyne




I am curious if there is an elegant way to achieve the same result with a for loop, so that the loop counter would be limited to the scope of the loop.

Elegant, I don't think so. This is the least ugly thing I could come up with.

for (uint8_t test = 1; test > 0; test++) {
printf("%i\n", (test - 1));
}

But please don't try this at home :)

You could also just create a scope around the variable definition and do-while in your example. It would limit the scope 'appropriately', but its ugly, non-idiomatic, and another programmer who comes along might be tempted to delete the extra braces and not think anything of it -- likely that just puts you back in the same potentially-sub-optimal boat as before, but it could break a build or change the behavior of the code if the variable name aliased another.

throw table_exception("(? ???)? ? ???");
Hodgman
Hodgman

The elegant way is to just use an int.

CPUs don't have 8bit registers, or don't want to operate on 8bit values these days, which means that when you write this:

uint8_t j = 0; do {...} while (++j);

You're actually asking the compiler to transform it into something like this abomination:
int j = 0; do {...} while( (j=(j+1)&0xFF) != 0 );

...and then also asking the compiler to try and optimize such horrible code for you!

After wasting a bunch of time optimizing your code, it will be the same as if you'd just written what DekuTree posted, which is a perfectly idiomatic loop without any clever obfuscation applied:

for( int i=0; i!=256; ++i ) {...}

If there is a more optimal way of looping on a specific platform, a clever compiler will be able to transform any of the above loops to it, seeing they're all exactly equivalent in behavior!

aregee
aregee

Maybe more to the point -- for a local variable, one that will likely be allocated as a register (or on the stack, at worst), you don't really gain anything at all by making 'test' small -- there might be reasons of correctness that are worthwhile, but optimization is basically a non-factor. Maybe if the loop appears in a deeply-recursive function, or on a small, embedded system with 4k of memory or something, but not usually. Just use the natural word-size of the machine, usually plain old 'int' -- it will be as fast as (faster, more likely) than the smaller value.

I totally agree with this. I do usually use int, but sometimes it happens that I want to ensure a certain size just because of the value I am testing against. This is not the case here though. Those cases would more likely be uint64_t, even if I would never make anything that iterates that many times...

Classic mistake. There is a generic way to make it work, if you really want to, though:


uint8_t j = 0;

do
{
  printf("%i\n", j);
}
while (++j);

Which is pretty readable, since the while loop is clearly going to terminate as soon as j overflows. And like Ravyne said, it doesn't really make anything faster in general, that said I personally don't see this as an optimization tactic, but more as a type correctness thing, in other words, the loop variable is going to be used as an 8-bit unsigned integer, so let's not lie to ourselves, make it an 8-bit unsigned integer. But with this comes the responsibility of watching out for overflow and other low-level things which generally don't come up when simply using a sufficiently large signed integer like int, people will have their own opinions on that matter smile.png

edit: fixed the termination condition error, thanks

Ah this is actually quite clever, lol... I would never write anything like that though, but it is a interesting point of view. :D My solution was, of course, to use an int instead.

Ohforf sake
Ohforf sake
Since we are trying to come up with new, obscure, and complicated ways for counting up, how about this:

#include <stdio.h>
template<typename Type>
class Range {
    public:
        Range(const Type &first, const Type &last) : m_first(first), m_last(last) { }
        class const_iterator {
            public:
                const_iterator(Type curr) : m_current(curr) { }
                inline const Type &operator*() const { return m_current; }                
                inline bool operator!=(const const_iterator &other) { return m_current != other.m_current; }
                inline const_iterator &operator++() { m_current++; return *this; }
            private:
                Type m_current;
        };
        inline const_iterator begin() const {
            return const_iterator(m_first);
        }
        inline const_iterator end() const {
            return const_iterator(m_last+1);
        }
    private:
        Type m_first, m_last;
};

int main()
{
    for (const auto i : Range<int>(0, 255))
        printf("%i\n", i);
}
Zaoshi Kaba
Zaoshi Kaba




Since we are trying to come up with new, obscure, and complicated ways for counting up, how about this

Somehow I can see Java programmer actually doing this.

21st Century Moose
21st Century Moose

Since we are trying to come up with new, obscure, and complicated ways for counting up, how about this

Somehow I can see Java programmer actually doing this.

Needs more boilerplate.

At least an ICountUpAble interface to inherit from, and a CountUpFactory object too. It should also parse XML to do the actual counting.

Direct3D has need of instancing, but we do not. We have plenty of glVertexAttrib calls. 
fastcall22
fastcall22
To be honest, most modern languages have some sort kind of range-based looping constructs...
FRex
FRex

This is hardly obscure, (x)range in Python works that way.

Lua for loop kind of does too, modifying the iterator doesn't matter, so in meaning it's closer to range like that really.

Anyone who knows range loop syntax could read that easily. Only question is if it's inclusive or not (which is a real problem, as shown in header's distribution classes).

While we are in topic of loops, this is an example from one zlib game codebase I found online:


#define loop(v,m) for(int v = 0; v<int(m); v++)
#define loopi(m) loop(i,m)
#define loopj(m) loop(j,m)
#define loopk(m) loop(k,m)
#define loopl(m) loop(l,m)
#define loopirev(v) for(int i = v-1; i>=0; i--)

Another nice example is trying to count back from size-1 to 0 using int with >=0 condition, and then, (because someone complains that you should always use std::size_t for indices because their holy compiler that clearly never is wrong or stupidly overzealous when ran with -Wall said so) changing type to std::size_t with same condition so it wraps around and never ends.

aregee
aregee

This is hardly obscure, (x)range in Python works that way.

Lua for loop kind of does too, modifying the iterator doesn't matter, so in meaning it's closer to range like that really.

Anyone who knows range loop syntax could read that easily. Only question is if it's inclusive or not (which is a real problem, as shown in header's distribution classes).

While we are in topic of loops, this is an example from one zlib game codebase I found online:


#define loop(v,m) for(int v = 0; v<int(m); v++)
#define loopi(m) loop(i,m)
#define loopj(m) loop(j,m)
#define loopk(m) loop(k,m)
#define loopl(m) loop(l,m)
#define loopirev(v) for(int i = v-1; i>=0; i--)

Another nice example is trying to count back from size-1 to 0 using int with >=0 condition, and then, (because someone complains that you should always use std::size_t for indices because their holy compiler that clearly never is wrong or stupidly overzealous when ran with -Wall said so) changing type to std::size_t with same condition so it wraps around and never ends.

Why would one redefine the language like that? Too much to type? It is almost like you need to learn a new language inside the language you already know, lol... Unless I completely missed a point.

FRex
FRex

I guess it's that, the codebase (while technically C++) isn't too commented and is a bit horror-y and very old (10+ years I think) so it's written in very C style with macros, weird casts, relying on memory layout of structs, TONS of globals, etc.

Hodgman
Hodgman

Why would one redefine the language like that? Too much to type? It is almost like you need to learn a new language inside the language you already know, lol... Unless I completely missed a point.

What do you mean? Everyone I know codes in Better C!
aregee
aregee

Why would one redefine the language like that? Too much to type? It is almost like you need to learn a new language inside the language you already know, lol... Unless I completely missed a point.

What do you mean? Everyone I know codes in Better C!

It actually doesn't look that stupid when you put it like that. biggrin.png

I was thinking to add things like:

#define BEGIN {

#define END }

So it looks a bit more like Pascal too.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.