adding new feature requires 99 code changes!

Started by
39 comments, last by Norman Barrows 10 years, 7 months ago

something is wrong with the editor. it clipped everything after the code block!

3 paragraphs of reply lost!

briefly:

optimization of update hasn't been required yet, but probably will be, to support accelerated time faster than 30x.

code cache behavior will determine if one big loop, one loop per update frequency, or one loop per update type will be fastest.

and it looks like i need to do code cache behavior research.

Norm Barrows

Rockland Software Productions

"Building PC games since 1989"

rocklandsoftware.net

PLAY CAVEMAN NOW!

http://rocklandsoftware.net/beta.php

Advertisement


You can continue removing cavemen as you used to, but spawn new "corpse" entitties and process them in your render pass.

yes, i was contemplating this as a workaround, creating a "dead PC" object. but the "alive" flag is probably the more elegant way.

Norm Barrows

Rockland Software Productions

"Building PC games since 1989"

rocklandsoftware.net

PLAY CAVEMAN NOW!

http://rocklandsoftware.net/beta.php


typedef std::function< bool ( Caveman& ) > Filter_t;
typedef std::function< void ( Caveman& ) > CavemanUpdate_t;
IteratateCavemen( Cavemen& cm, Filter_t filter, CavemanUpdate_t update ) {
for( Caveman& c : cm ) if( filter( c ) ) update( c );
}So, the benefit of this is to get rid of that "if( active ) etc" stuff and move it to a helper class. So you can write something as follows:

struct UpdateFilters
{
static bool IsLiving( Caveman& cm ) {return cm.alive && cm.active;}
};

IterateCavemen( cavemenList, std::bind( &UpdateFilters::IsLiving, std::placeholders::_1 ), []( Caveman& c )
{
// Do your update code here.
} );
};

well that will nicely encapsulate the "filerting test" in a generic manner. however, "is_active && is_alive" is the only filter used, except in the render call where its just "is _active".

99 little for loops is probably inefficient, and is the root cause of so many changes being required.

but its fast enough for the moment, so maintainability and modifiability are the only reasons to refactor at this point.

OTOH, it will probably need to go faster before its all over. so i think i need to take a little time out, read up on code cache behavior, then decide how to refactor the loop layout (if at all), and THEN do the changes for "is_alive".

worst case, i know 99 little loops is the most cache friendly, and i just add "if (! cm[a].alive) continue;" everywhere.

best case, one big loop or "whatever is easiest to code and maintain" is sufficiently cache friendly, i refactor, then make my "is_alive" change in just three places (update, init, and render).

Norm Barrows

Rockland Software Productions

"Building PC games since 1989"

rocklandsoftware.net

PLAY CAVEMAN NOW!

http://rocklandsoftware.net/beta.php

Watch and understand this video: http://channel9.msdn.com/Events/GoingNative/2013/Cpp-Seasoning

Your life will improve.

Wielder of the Sacred Wands
[Work - ArenaNet] [Epoch Language] [Scribblings]

well that will nicely encapsulate the "filerting test" in a generic manner. however, "is_active && is_alive" is the only filter used, except in the render call where its just "is _active".


Yeah, I figured with all the other comments that maybe something more directed at the specific case would be useful. But I personally think it has more utility than simply removing the filtering issue. By abstracting the loop itself in addition to the filter, other ways of organizing the data becomes possible without rewriting it in 99 different locations. For instance, if you separated the lists as someone mentioned, then you would only modify the new abstract list iterator to handle that case. At that point the filter abstraction becomes nothing more than a "tag" which identifies which sublists to include. Or, as you go into below:

99 little for loops is probably inefficient, and is the root cause of so many changes being required.

but its fast enough for the moment, so maintainability and modifiability are the only reasons to refactor at this point.


That is always the key, if it is fast enough, don't mess with it. Though, with the abstraction you are protecting yourself from hunting down the 99 cases again in the future. Change them all in one place. Even if you decide to simply push the changes to be performed all at once later, you can modify that function to simply store the lambda for later use. The abstraction is a bit more flexible than you might think initially.

OTOH, it will probably need to go faster before its all over. so i think i need to take a little time out, read up on code cache behavior, then decide how to refactor the loop layout (if at all), and THEN do the changes for "is_alive".

worst case, i know 99 little loops is the most cache friendly, and i just add "if (! cm[a].alive) continue;" everywhere.

best case, one big loop or "whatever is easiest to code and maintain" is sufficiently cache friendly, i refactor, then make my "is_alive" change in just three places (update, init, and render).

Cache behavior is going to depend on a very large number of things, saying it is faster with the small loops is not a guarantee, especially if there are many things going on between the different loops. I.e. run a loop, do stuff that starts evicting objects from cache, run another loop, do other stuff that starts evicting object data repeat. On the other hand, the small loops are generally easier to later merge as possible and even apply multi-core distribution to. Either way, unless you have really sat down with performance counters and measured cache miss rates and all that, I almost always find my initial assumptions to be way off the mark. smile.png


Just a little note as a general encouragement. On a certain game which was mentioned originally in your message there was a 38,000 line file which consisted of a single function. Every variation of if/else/for/while/switch/case crap you've ever seen was packed into this massive monolithic function. It was the most disgusting piece of code I ever have seen, it shipped a game and many expansion packs without ever being cleaned up, just further 'hacked'... I'd prefer to have 99 places in the code to clean up than a single monolithic interdependent disaster area function. smile.png

ok - checked into instruction caches.

got a boatload of good links. i'll post them in my game development journal. it'll be a nice companion to my journal entry on data cache info.

long and short of it, the instruction cache situation is similar to that of data caches.

and let us hope that none of us ever has to go there when it comes to optimization! <g>.

it looks like in my case, the best approach will be to combine loops as much as possible. this should improved modify-ability.

then - if and when the time comes, i can optimize. but odds are that i won't be forced to go to an " architecture (drives->) cache design (drives->) data structure design (drives->) code layout " type of optimization - which, of course, is the the ultimate way to write high performance code. but it also doesn't really consider any other coding issues like maintainability and such.

i decided this is probably the way to go based on what i learned, and on the issues mentioned by AllEightUp, which were also mentioned in the info i found online. IE SERIOUS cache optimization is not easy, although writing cache friendlier code is not so hard.

ApochPiQ:

I DL'd the pdf, but haven't had a chance to watch the whole video yet. i take it the general idea (you know how slides are) of the first part of the presentation is to create a generic iterator (to replace raw loops) that can (in my case) be passed the various update functions.

So i've started coding up a new update_all() routine for band members.

at first, no problem.

i implemented "the swap with last and decrement count" for deactivating a band member. so now i can just iterate from zero through num_bandmembers - 1, and don't need to check for " .active=1 ". but i do need a check for " .alive=1 " now.

then i started adding the various update calls:

(in CScript code...)

' band member update all

fn v BMupdate_all
i a
4 a num_bandmembers
== cm[a].alive 0
>>
.
' do update stuff here
c BMdecrease_fatigue a
c BMmodel_attack a
c BMmodel_surprise a
c BMmodel_full_fatigue a
c run_bandmember a
c BMmove_raft a
.
.

so now i'm at move raft - and i've hit a snag - or a complication to be more precise....

i'm doing an update_all_rafts() type thing. and in update_raft(a), i have to do a call to update_bandmember_aboard(b,dx,dz) where b is the bandmember number, and dx,dz is the raft movement. so it looks like that loop cant be consolidated (easily). i'd have to update all rafts first, and save their movement deltas, then update band members, and use the saved deltas in a routine such as BMmodel_raft_movement(a).

so now the question is:

1. should the raft movement "event" get processed immediately - requiring another iterator?

-or-

2. should the raft movement "event" be stored for later "batch processing" with the rest of the band member updates - which keeps things more in one place - but requires storing deltas?

when looking at update_raft, storing deltas for later processing doesn't make it obvious what the downstream effects of handling the event are.

when looking at BMupdate_all, processing raft movement immediately leaves you no clue that there's additional updates going on outside of BMupdate_all.

guess that's what comments are for eh? <g>.

i've started coding a new update_all() routine.

Norm Barrows

Rockland Software Productions

"Building PC games since 1989"

rocklandsoftware.net

PLAY CAVEMAN NOW!

http://rocklandsoftware.net/beta.php


Just a little note as a general encouragement. On a certain game which was mentioned originally in your message there was a 38,000 line file which consisted of a single function. Every variation of if/else/for/while/switch/case crap you've ever seen was packed into this massive monolithic function. It was the most disgusting piece of code I ever have seen, it shipped a game and many expansion packs without ever being cleaned up, just further 'hacked'... I'd prefer to have 99 places in the code to clean up than a single monolithic interdependent disaster area function.

last night while i was collapsing loops together, i became curious, what did the function do?

Norm Barrows

Rockland Software Productions

"Building PC games since 1989"

rocklandsoftware.net

PLAY CAVEMAN NOW!

http://rocklandsoftware.net/beta.php

Just a little note as a general encouragement. On a certain game which was mentioned originally in your message there was a 38,000 line file which consisted of a single function. Every variation of if/else/for/while/switch/case crap you've ever seen was packed into this massive monolithic function. It was the most disgusting piece of code I ever have seen, it shipped a game and many expansion packs without ever being cleaned up, just further 'hacked'... I'd prefer to have 99 places in the code to clean up than a single monolithic interdependent disaster area function.


last night while i was collapsing loops together, i became curious, what did the function do?


It was basically just an update function, of course it did everything from pathing to animation blending and probably 50% of it was dead code that could never be reached. Of course, going through the first thousand or so lines you are generally so lost in possible branches that there was simply no way to clean it up without a top down refactor which no one was willing to allow.

ok, new update_all is coded. but not hooked up yet. one loop to rule them all.

here's what the new code looks like (CScript code). i've added comments in red to make it easier to follow. i'll post the C++ translation output as well.

( since embedded code seems to clip messages of late, i'll just use font and color )

band member update all:



'   must call move_rafts first!       // i decided to store raft dx,dz and process them here with the rest of the updates
                                         //  so move_rafts must be called first to set raft dx,dz
fn v BMupdate_all
i a                                         // int a     loop counter     the band member number
4 a num_bandmembers                    // it now only iterates over active band members, not all band members (active or inactive).
    == cm[a].alive 0                       // here's the check for "is_alive".     if alive==0 continue.
        >>
        .
    '   do update stuff here - do global frame
    c BMdecrease_fatigue a                           // call BMdecrease_fatigue with "a" as a parameter.
    c BMmodel_attack a
    c BMmodel_surprise a
    c BMmodel_full_fatigue a
    c run_bandmember a
    c BMmodel_raft_movement a
    c BMmodel_paddling_fatigue a
    c BMmodel_falling a
    c BMmodel_sneak_detection a
    != frame 0                                        // if frame != 0 
        >>                                             // continue
        .                                               // end if code block
    ' do global second
    c BMmodel_fatigue_dueto_damage a
    c BMmodel_fatigue_dueto_encumbrance a
    c BMrun_quests a
    c BMclear_rockshelter_enccouter_flags a
    c BMcheck_rockshelter_encounters a
    c BMclear_cave_encounter_flags a
    c BMcheck_cave_encounters a
    c BMclear_hut_encouter_flags a
    c BMcheck_hut_encounters a
    c BMcheck_hut_takeover  a
    c BMcheck_climbing_animals a
    != second 0
        >>
        .
    '   do global minute
    c BMcheck_animal_encounters a
    c BMcheck_caveman_encounters a
    c BMmodel_intox a
    c BMextinguish_torches a
    == minute%7 0                                             // if (minute % 7) == 0
        c BMreduce_hygiene_dueto_movement a
        .
    == minute%10 0
        c BMreduce_water a
        c BMreduce_sleep a
        .
    == minute%15 0
        c BMreduce_food a
        c BMaffect_mood a
        .
    != minute 0
        >>
        .
    '   do global hour
    c BMmodel_dehydration a
    c BMmodel_food_spoilage a
    c BMmodel_exposure a
    c BMmodel_heatstroke a
    c BMmodel_drown_in_flood a
    c BMmodel_wearNtear a    
    c BMmodel_perm_shelter_raids a
    c BMmoodboost_nature_lover a
    == hour%2 0
        c BMmodel_damage_dueto_illness a
        .
    ? ((hour>7)&&(hour<19))                               // if hour > 7 and hour < 19
        c  BMcheck_quest_encounters a
        .
    != hour 0
        >>
        .
    '   do global day
    c BMmodel_starvation a
    c BMmodel_getting_sick a
    c BMmodel_background_radiation a
    c BMmodel_perm_shel_weathering a
    c BMzero_god_relations a
    c BMreduce_social a
    c BMmodel_traps a
    c BMmodel_skill_reduction a
    .                                                   // end for loop
.                                                     // end function definition
 

i'm thinking this gives me an opportunity to run side by side timing tests of the two loop layouts. to see what the difference is. results might be interesting.

so the next step is to make a version of run_simulation (whose code appears in a previous in this htread) that does everything except the BMupdate_all stuff.

then hook the two versions of run_simulation up to a hotkey, add timers, and stir! <g>.

not sure how i'd call it. the one big loop eliminates a lot of iterator overhead. but the loop bodies of the 99 little loops tend to be small and instruction cache friendly.

see what happens.....

i also implemented a "find first / find next" system for both active and alive band members, as a low overhead way to de-couple the iterator from the code being iterated. that too has yet to be hooked up. i should probably go back and see if some standard C++ data structure or template can do the same thing with no type check or other overhead.

Norm Barrows

Rockland Software Productions

"Building PC games since 1989"

rocklandsoftware.net

PLAY CAVEMAN NOW!

http://rocklandsoftware.net/beta.php


there was simply no way to clean it up without a top down refactor which no one was willing to allow.

ouch! rats nest AND mission critical code!

have't had that kind of thing since the 2 screen assembly code blitter for GAMMA Wing!

fortunately, today's pc's are fast enough that one can get fast enough code while still maintaining some semblance of order in the code.

that's probably part of the reason for the greater emphasis on internal code design in games these days. we now have (at least a few) clock cycles we can afford to spend on design considerations.

Norm Barrows

Rockland Software Productions

"Building PC games since 1989"

rocklandsoftware.net

PLAY CAVEMAN NOW!

http://rocklandsoftware.net/beta.php

This topic is closed to new replies.

Advertisement