0

So, first off. Here's my server engine. gilmud.py!

Ok, here's a shortened version of my previous novel sized post.

The link above is our python server engine for our MUD. Noting line 73-75, and 359 on

self.tickThread = threading.Thread(None, self.runTicks, None, ())
self.tickThread.start()

...

def runTicks(self):
    while self.running:
        time.sleep(.1)
        for thing in Thing.List.values():
            if thing:
                if "person" in thing.attrs:
                    if "spawner" in thing.attrs:
                        thing.tick()

you'll probably see the horrid method of giving what we need to be roughly 100 players and 2000 mobs/npcs 'life'. The tick() checks for things like whether they'll move or pick up and item or if they are in combat or being targeted, etc. Same goes for the players, minus a few automated things, of course.

Is there any way we could rewrite a portion, or all of this module in say, C++ to get some better performance? Currently our needed .1 second ticks are at about 3 seconds using python in the method we have now.

(Also, we've tried SEVERAL different threading types and stackless. Nothing did the trick).

Thanks in advance for the help! Any advice is welcome!

jtsmith1287
  • 1,110
  • 2
  • 13
  • 24
  • 3
    This is really chatty .. can you pair it down to a shorter read with some specific questions? – Levon Jul 17 '12 at 20:41
  • 3
    It would be better if your things added what they wanted to do to an array instead of polling them all. (i.e. raised events). Then you wouldnt have to go through all things, only a list of events received from all things since last execution. – Qiau Jul 17 '12 at 22:49

4 Answers4

3

You have a lot of inefficiencies in your code.

Looking in just the Person.tick method:

replace

for spell in self.spellTimers:
            self.spellTimers[spell]['tick'] += 1

with

for spell_timer in self.spellTimers.itervalues():
            spell_timer['tick'] += 1

same thing with at least two fewer dict lookups per value

replace

for thing in Thing.List.values():
            if thing:
                if "person" in thing.attrs:
                    if "spawner" in thing.attrs:
                        thing.tick()

with

for thing in Thing.List.itervalues():
            if thing and "person" in thing.attrs 
                     and "spawner" in thing.attrs:
                    thing.tick()

(ok that one might not be much faster but I like it better)

get:

from spells import cast,spellmaker

out of the tick() method and into the top of the file

replace:

#spell cooldown timer
self.timers['cooldown'] -= 1
if self.timers['cooldown'] < 0:
    self.timers['cooldown'] = 0

with:

#spell cooldown timer
self.timers['cooldown'] = max(0, self.timers['cooldown']-1)

You do that one a lot. You have a lot of code like

    #Check if self is doing ranged attack, and then increase timer (ready their weapon)
    self.timers['ranged'] += 1
    if self.timers['ranged'] >= int((self.stats['dex']*-1.1)+60+2):
        self.timers['ranged'] = int((self.stats['dex']*-1.1)+60+1)
    if self.timers['ranged'] == int((self.stats['dex']*-1.1)+60):
        self.timers['ranged'] = int((self.stats['dex']*-1.1)+60+1)

the second if should be elif if the first condition is true the second condition can NEVER be true.

remember every time you access self.somedict['name'] you are doing two dict lookups (overhead could be even more but use that as a rule of thum). If you have a dictlookup on line after line you speed up your code by assigning it to a "temp" local var.

If you keep going through your code, you'll put in one tweek like one of the above that will bring things inline. Sorry I can't go through it all.

Phil Cooper
  • 5,747
  • 1
  • 25
  • 41
  • I wasn't expecting anyone to really go through it even as thoroughly as you did. I only posted it in case someone was confused with method references. Thanks for pointing all that stuff out. So you're saying the dict lookups slow stuff down a lot? And by doing `dict = self.somedict[x]` and then using `dict` for everything else in that method is faster? – jtsmith1287 Jul 18 '12 at 02:54
  • So, thanks for this! I've gone through my code in just a few places, and I've already seen a dramatic difference in performance. I'm excited to continue optimizing and watching that timer drop. Thanks so much for taking the time to review my code! – jtsmith1287 Jul 28 '12 at 19:38
1

Since you have not posted a small number of lines of relevant Python code, one thing you should know about Python is it will let you write code in a C-style, when an equivalent Python style will do. Often times -- and again I have not seen you relevant code -- an older style of processing data instead of, for example, writing a list comprehension, will cost more time than using a more Pythonic style.

You can get help getting a Pythonic style -- and BTW I need plenty of help -- by posting a small example or examples and asking if there is a better way to speed things up. You would also want to instrument your code to capture times.

octopusgrabbus
  • 10,555
  • 15
  • 68
  • 131
  • 2
    Profiling +1. It takes ages to develop a good intuition for what's really slowing you down. Chances are Python itself isn't the problem. – Daniel Lyons Jul 17 '12 at 20:47
  • I've now posted the lines I'm needing help with. I've left out the time capturing code to make it easier to read. – jtsmith1287 Jul 17 '12 at 21:39
1

It would be possible to rewrite parts of it in C++ or similar, but if your loop is taking 3 seconds to process 2000 or so items then I think it's more likely you've got some serious issues with the way you've coded things rather than an inherent problem with python itself.

So, firstly I'd like to second the recommendation of profiling that was made in the other answer/it's comments - it's really the best way to tell exactly where all the time is disappearing to in your loop. There's some documentation on the profilers provided with python available here.

Secondly, if you're using the standard python distribution then I'd consider staying away from threads if possible. The default implementation of python has a global interpreter lock that prevents any interpreted python code from truly running in parallel, removing a lot of the advantages of bothering with threads in the first place (and in some cases actually making things slower)

Third, I can offer some small advice on the code you've posted. You'll still be best using a profiler to confirm my suspicions (and to find problem areas that I haven't noticed - it looks like you've got quite a lot of code there) but I'd say this would probably help at least:

In your for loop for thing in Thing.List.values(): you'd probably be better calling Thing.List.itervalues(). This will return an iterator over the values rather than allocating a full list of the values. Allocating the list is just un-necessary unless you're planning on adding or removing to the dictionary whilst iterating over it, and might not be too fast if there's 2000 or so items in the dict as you imply. This advice also applies to other times you're iterating over dictionaries in your code, assuming you're not planning on adding/removing entries while iterating (Use .iteritems() instead of .items(), .iterkeys() instead of .keys()).

You might also want to look into a different timing method than calling time.sleep every iteration. There's two problems I can see with it - mainly that time.sleep(.1) doesn't actually guarantee that the sleep will last .1 of a second, and also because it doesn't take into account the length of time that processing will take. For example, say your processing takes 0.05 seconds then you sleep for .1 second, then there's actually been .15 seconds between ticks - and this gap will only grow as processing becomes more intense. I don't have any specific suggestions, but you may at least want to take the processing time into account when passing a number into time.sleep

Finally, you might want to consider using the code review stack exchange at some point. I don't think anyone would want to review your entire game engine but it seems like it might be a good place to put up chunks of your code that you might want feedback on, or that you think could be improved.

Community
  • 1
  • 1
obmarg
  • 9,369
  • 36
  • 59
  • I was really excited to give the itervalues() a try, but immediately ran into a `RuntimeError`. Reason being, this list is constantly being update. If a mob spawns, it goes in the list. If a player logs in, they go in the list. I'm thinking I need to rethink how to do ticks. How on earth do other games do ticks??? – jtsmith1287 Jul 18 '12 at 02:56
  • @jtsmith1287 I'm afraid I'm not sure exactly how you could do ticks better, but I would maybe suggest not updating the list while you're using it - you could have a "new players/mobs" list that you build up while running your loop, then update the main list from the new list after you've finished iterating. – obmarg Jul 18 '12 at 10:40
  • never iterate on your list directly, but on a copy; then swap one for the other when you're done. – georgek Jul 20 '12 at 01:55
1

You've got a lot of great specific feedback already so I'll only add an observation from a mudder's perspective. There are a couple of large mud projects that use Python without speed issues (for example Evennia), so I'm confident in saying you're better off refactoring the code you have than dropping down to C.

georgek
  • 877
  • 6
  • 11
  • I mostly agree with you except that no other mud that I know of has 2000 mobs getting a tick. I can't logically think of how to do this without looping through some form of list and giving each their tick. – jtsmith1287 Jul 18 '12 at 15:53
  • 1
    @Qiau answered that question commenting on the OP; in general avoid polling in favor of a queue of events. For example, say you want to tick a mob every heartbeat because they're berserk and you're counting down their berserk timer. Instead, when they go berserk push a berserk_timer onto your queue with a (future time = game time + berserk duration). Every heartbeat look at that queue, and for every event whose future time is equal to or past the present time, pop that event from the list and execute it. When you hit an event whose future time is yet in the future, stop your iteration. – georgek Jul 19 '12 at 04:11
  • 1
    to add to that, if you want to do everything with your list of mobs, you can check the mob itself for the future time in the same way. You don't have to iterate the entire list. Nevertheless I would prefer a queue of events over iterating the mobs. – georgek Jul 19 '12 at 05:24