views:

315

answers:

8

In order to promote good programming habits and increase the efficiency of my code (Read: "My brother and I are arguing over some code"), I propose this question to experienced programmers:

Which block of code is "better"? For those who can't be bothered to read the code, is it worth putting a conditional within a for-loop to decrease the amount of redundant code than to put it outside and make 2 for-loops? Both pieces of code work, the question is efficiency vs. readability.

    - (NSInteger)eliminateGroup {
            NSMutableArray *blocksToKill = [[NSMutableArray arrayWithCapacity:rowCapacity*rowCapacity] retain];
            NSInteger numOfBlocks = (NSInteger)[self countChargeOfGroup:blocksToKill];
            Block *temp;
            NSInteger chargeTotal = 0;

//Start paying attention here

            if (numOfBlocks > 3) 
             for (NSUInteger i = 0; i < [blocksToKill count]; i++) {
              temp = (Block *)[blocksToKill objectAtIndex:i];
              chargeTotal += temp.charge;
              [temp eliminate];
              temp.beenCounted = NO;
             }
            }
            else {
             for (NSUInteger i = 0; i < [blocksToKill count]; i++) {
              temp = (Block *)[blocksToKill objectAtIndex:i];
              temp.beenCounted = NO;
             }
            } 
            [blocksToKill release];
            return chargeTotal;
        }

Or...

        - (NSInteger)eliminateGroup {
            NSMutableArray *blocksToKill = [[NSMutableArray arrayWithCapacity:rowCapacity*rowCapacity] retain];
            NSInteger numOfBlocks = (NSInteger)[self countChargeOfGroup:blocksToKill];
            Block *temp;
            NSInteger chargeTotal = 0;

//Start paying attention here

            for (NSUInteger i = 0; i < [blocksToKill count]; i++) {
                temp = (Block *)[blocksToKill objectAtIndex:i];
                if (numOfBlocks > 3) {
                    chargeTotal += temp.charge;
                    [temp eliminate];
                }
                temp.beenCounted = NO;
            }
            [blocksToKill release];
            return chargeTotal;
        }

Keep in mind that this is for a game. The method is called anytime the user double-taps the screen and the for loop normally runs anywhere between 1 and 15 iterations, 64 at maximum. I understand that it really doesn't matter that much, this is mainly for helping me understand exactly how costly conditional statements are. (Read: I just want to know if I'm right.)

+9  A: 

The first code block is cleaner and more efficient because the check numOfBlocks > 3 is either true or false throughout the whole iteration.

The second code block avoids code duplication and might therefore pose lesser risk. However, it is conceptually more complicated.

The second block can be improved by adding

bool increaseChargeTotal = (numOfBlocks > 3)

before the loop and then using this boolean variable instead of the actual check inside the loop, emphasizing the fact that during the iteration it doesn't change.

Personally, in this case I would vote for the first option (duplicated loops) because the loop bodies are small and this shows clearly that the condition is external to the loop; also, it's more efficient and might fit the pattern "make the common case fast".

antti.huima
Looks like my brother wins this one. Thanks for the speed and quality of your response! This site is amazing.
Tozar
If he's going to make that kind of optimization (prematurely :) shouldn't he also move the call to count out of both loops?
jmucchiello
+8  A: 

All other things being equal, having two separate loops will generally be faster, because you do the test once instead of every iteration of the loop. The branch inside the loop each iteration will often slow you down significantly due to pipeline stalls and branch mispredictions; however, since the branch always goes the same way, the CPU will almost certainly predict the branch correctly for every iteration except for the first few, assuming you're using a CPU with branch prediction (I'm not sure if the ARM chip used in the iPhone has a branch predictor unit).

However, another thing to consider is code size: the two loops approach generates a lot more code, especially if the rest of the body of the loop is large. Not only does this increase the size of your program's object code, but it also hurts your instruction cache performance -- you'll get a lot more cache misses.

All things considered, unless the code is a significant bottleneck in your application, I would go with the branch inside of the loop, as it leads to clearer code, and it doesn't violate the don't repeat yourself principle. If you make a change to once of the loops and forget to change the other loop in the two-loops version, you're in for a world of hurt.

Adam Rosenfield
I prefer this answer because it means I'm right, is that so wrong?
Tozar
+5  A: 

I would go with the second option. If all of the logic in the loop was completely different, then it would make sense to make 2 for loops, but the case is that some of the logic is the same, and some is additional based upon the conditional. So the second option is cleaner.

The first option would be faster, but marginally so, and I would only use it if I found there to be a bottleneck there.

Gerald
+3  A: 

My vote is strongly in favor of the second block.

The second block makes clear what the difference in logic is, and shares the same looping structure. It is both more readable and more maintainable.

The first block is an example of premature optimization.

As for using a bool to "save" all those LTE comparisons--in this case, I don't think it will help, the machine language will likely require exactly the same number and size of instructions.

richardtallent
+3  A: 

The overhead of the "if" test is a handful of CPU instructions; way less than a microsecond. Unless you think the loop is going to run hundreds of thousands of times in response to user input, that's just lost in the noise. So I would go with the second solution because the code is both smaller and easier to understand.

In either case, though, I would change the loop to be

for (temp in blocksToKill) { ... }

This is both cleaner-reading and considerably faster than manually getting each element of the array.

Jens Alfke
This looks interesting, does (temp in blocksToKill) do the same thing as for (NSUInteger i = 0; i < [blocksToKill count]; i++)?
Tozar
Essentially yes, it may do something a bit more complicated depending on the collection class though. For instance, it may fetch the next 100 objects via one message send then iterate over those pointers, fetching batches of objects in turn.
Ashley Clark
+8  A: 
Roger Pate
code readability and code maintainability is usually the most important, since it leads to fewer bugs and more stable code. (And let the compiler optimize for you)...
Johan
I didn't specify what I meant by "better" for a reason. I already knew the pros and cons of both blocks of code. I wanted to know what was "better" in the eyes of the other programmers here. But you get the green check for having the most profound response.
Tozar
Johan: I'd say those lead to *easier development*, which is naturally important to developers. Of course, other things lead to easier development too, and easier development itself leads to fewer bugs, faster turnaround (incl. on bugfixes), and better code. However, "usually" in your comment is still significant, and it's trivial to state "code readability and maintainability are important requirements", *rank that against other requirements* (such as "the code always works", which is often most important but unstated), and then measure how you meet your requirements.
Roger Pate
A: 

Readability (and thus maintainability) can and should be sacrificed in the name of performance, but when, and only when, it's been determined that performance is an issue.

The second block is more readable, and unless/until speed is an issue, it is better (in my opinion). During testing for your app, if you find out this loop is responsible for unacceptable performance, then by all means, seek to make it faster, even if it becomes harder to maintain. But don't do it until you have to.

zpasternack
+5  A: 

You would probably waste more time in the pointless and unnessesary [blocksToKill retain]/[blocksToKill release] at the start/end of the method than the time taken to execute a few dozens comparisons. There is no need to retain the array since you wont need it after you return and it will never be cleaned up before then.

IMHO, code duplication is a leading cause of bugs which should be avoided whenever possible.

Adding Jens recomendation to use fast enumeration and Antti's recomendation to use a clearly named boolean, you'd get something like:

    - (NSInteger)eliminateGroup {
        NSMutableArray *blocksToKill = [NSMutableArray arrayWithCapacity:rowCapacity*rowCapacity];
        NSInteger numOfBlocks = (NSInteger)[self countChargeOfGroup:blocksToKill];
        NSInteger chargeTotal = 0;

        BOOL calculateAndEliminateBlocks = (numOfBlocks > 3);
        for (Block* block in blocksToKill) {
            if (calculateAndEliminateBlocks) {
                chargeTotal += block.charge;
                [block eliminate];
            }
            block.beenCounted = NO;
        }
        return chargeTotal;
    }

If you finish your project and if your program is not running fast enough (two big ifs), then you can profile it and find the hotspots and then you can determine whether the few microseconds you spend contemplating that branch is worth thinking about — certainly it is not worth thinking about at all now, which means that the only consideration is which is more readable/maintainable.

Peter N Lewis
I totally agree with you, Peter. In fact, people quibbling about evaluating `numOfBlocks > 3` each time are not only second-guessing the compiler (which can optimize by hoisting tests of unmodified variables), but generally miss the point that calling -objectAtIndex: each time through the loop is MUCH slower than a for-in loop. You kill both birds with one stone in this code, and it's a concise and fast solution. You deserve a lot more votes for this answer.
Quinn Taylor