Magic Optimisation

Collapse
This topic is closed.
X
X
 
  • Time
  • Show
Clear All
new posts
  • simonwittber@gmail.com

    #1

    Magic Optimisation

    Hello People.

    I've have a very tight inner loop (in a game app, so every millisecond
    counts) which I have optimised below:

    def loop(self):
    self_pool = self.pool
    self_call_exit_ funcs = self.call_exit_ funcs
    self_pool_pople ft = self.pool.pople ft
    self_pool_appen d = self.pool.appen d
    check = self.pool.__len __
    while check() > 0:
    task = self_pool_pople ft()
    try:
    task.next()
    except StopIteration:
    self_call_exit_ funcs(task)
    return
    self_pool_appen d(task)

    This style of optimisation has shaved _seconds_ from my iteration
    cycle, esp. when I have many registered tasks, so this style of
    optimisation is very important to me.

    However, it is very ugly. Does anyone have any tips on how I could get
    this optimisation to occor magically, via a decorator perhaps?

    Sw.

  • Paul McGuire

    #2
    Re: Magic Optimisation

    This isn't much prettier, but what if you extract the try-except
    overhead out from the while loop? You only expect the exception to
    fire one time, at the end of the list. You can also eliminate any
    localization of variables for calls that are not called in the loop,
    such as self_pool (which does not seem to be used at all), and
    self_call_exit_ funcs.

    -- Paul

    def loop(self):
    self_pool_pople ft = self.pool.pople ft
    self_pool_appen d = self.pool.appen d
    check = self.pool.__len __
    try:
    while check() > 0:
    task = self_pool_pople ft()
    task.next()
    self_pool_appen d(task)
    except StopIteration:
    self.call_exit_ funcs(task)
    return

    Comment

    • Paul Rubin

      #3
      Re: Magic Optimisation

      simonwittber@gm ail.com writes:[color=blue]
      > However, it is very ugly. Does anyone have any tips on how I could get
      > this optimisation to occor magically, via a decorator perhaps?[/color]

      Have you tried psyco?

      Comment

      • simonwittber@gmail.com

        #4
        Re: Magic Optimisation

        I guess it is hard to see what the code is doing without a complete
        example.

        The StopIteration is actually raised by task.next(), at which point
        task is removed from the list of generators (self.pool). So the
        StopIteration can be raised at any time.

        The specific optimisation I am after, which will clean up the code a
        lot, is a way to auto-magically create self_attribute local variables
        from self.attribute instance variables.

        Sw.

        Comment

        • simonwittber@gmail.com

          #5
          Re: Magic Optimisation

          Yes. It slows down the loop when there are only a few iterators in the
          pool, and speeds it up when there are > 2000.

          My use case involves < 1000 iterators, so psyco is not much help. It
          doesn't solve the magic creation of locals from instance vars either.

          Sw.

          Comment

          • Paul Rubin

            #6
            Re: Magic Optimisation

            simonwittber@gm ail.com writes:[color=blue]
            > My use case involves < 1000 iterators, so psyco is not much help. It
            > doesn't solve the magic creation of locals from instance vars either.[/color]

            How about using __slots__ to put those instance vars at fixed offsets
            in the pool object (self then needs to be a new-style class instance).
            That might or might not avoid the dict lookups.

            Comment

            • simonwittber@gmail.com

              #7
              Re: Magic Optimisation

              > def loop(self):[color=blue]
              > self_pool = self.pool
              > self_call_exit_ funcs = self.call_exit_ funcs
              > self_pool_pople ft = self.pool.pople ft
              > self_pool_appen d = self.pool.appen d
              > check = self.pool.__len __
              > while check() > 0:
              > task = self_pool_pople ft()
              > try:
              > task.next()
              > except StopIteration:
              > self_call_exit_ funcs(task)
              > return
              > self_pool_appen d(task)[/color]

              Stupid me. the 'return' statement above should be 'continue'. Sorry for
              the confusion.

              Comment

              • Tomasz Lisowski

                #8
                Re: Magic Optimisation

                simonwittber@gm ail.com napisał(a):[color=blue][color=green]
                >> def loop(self):
                >> self_pool = self.pool
                >> self_call_exit_ funcs = self.call_exit_ funcs
                >> self_pool_pople ft = self.pool.pople ft
                >> self_pool_appen d = self.pool.appen d
                >> check = self.pool.__len __
                >> while check() > 0:
                >> task = self_pool_pople ft()
                >> try:
                >> task.next()
                >> except StopIteration:
                >> self_call_exit_ funcs(task)
                >> return
                >> self_pool_appen d(task)[/color]
                >
                >
                > Stupid me. the 'return' statement above should be 'continue'. Sorry for
                > the confusion.
                >[/color]

                Then you can avoid continue by writing:

                while check() > 0:
                task = self_pool_pople ft()
                try:
                task.next()
                except StopIteration:
                self_call_exit_ funcs(task)
                else:
                self_pool_appen d(task)

                Tomasz Lisowski

                Comment

                • Paul McGuire

                  #9
                  Re: Magic Optimisation

                  I still think there are savings to be had by looping inside the
                  try-except block, which avoids many setup/teardown exception handling
                  steps. This is not so pretty in another way (repeated while on
                  check()), but I would be interested in your timings w.r.t. your current
                  code.

                  def loop(self):
                  self_pool_pople ft = self.pool.pople ft
                  self_pool_appen d = self.pool.appen d
                  self_call_exit_ funcs = self.call_exit_ funcs
                  check = self.pool.__len __
                  while check() > 0:
                  try:
                  while check() > 0:
                  task = self_pool_pople ft()
                  task.next()
                  self_pool_appen d(task)
                  except StopIteration:
                  self_call_exit_ funcs(task)

                  -- Paul

                  Comment

                  • Paul McGuire

                    #10
                    Re: Magic Optimisation

                    Raymond Hettinger posted this recipe to the Python Cookbook. I've not
                    tried it myself, but it sounds like what you're looking for.



                    -- Paul

                    Comment

                    • Bengt Richter

                      #11
                      Re: Magic Optimisation

                      On 5 Sep 2005 07:27:41 -0700, "Paul McGuire" <ptmcg@austin.r r.com> wrote:
                      [color=blue]
                      >I still think there are savings to be had by looping inside the
                      >try-except block, which avoids many setup/teardown exception handling
                      >steps. This is not so pretty in another way (repeated while on
                      >check()), but I would be interested in your timings w.r.t. your current
                      >code.
                      >
                      > def loop(self):
                      > self_pool_pople ft = self.pool.pople ft
                      > self_pool_appen d = self.pool.appen d
                      > self_call_exit_ funcs = self.call_exit_ funcs
                      > check = self.pool.__len __
                      > while check() > 0:
                      > try:
                      > while check() > 0:
                      > task = self_pool_pople ft()
                      > task.next()
                      > self_pool_appen d(task)
                      > except StopIteration:
                      > self_call_exit_ funcs(task)
                      >[/color]

                      Why not let popleft trigger an exception out of while True instead,
                      and prevent tasks from raising StopIteration, and let them yield a None
                      to indicate keep scheduling with no special action, and something else
                      for optional differentiation of various exit options, e.g., zero for die,
                      and nonzero for suspension waiting for event(s) E.g., a returned integer
                      could be an event mask or single index (+ vs -) for thing(s) to wait for.
                      If you work things right, event check in the loop can be an if like
                      if waitedfor&event s: process_events( ), which most of the time is a fast no-op).

                      Then (without event stuff, and untested ;-) maybe something like:

                      def loop(self):
                      self_pool_pople ft = self.pool.pople ft
                      self_pool_appen d = self.pool.appen d
                      self_call_exit_ funcs = self.call_exit_ funcs
                      try:
                      while True:
                      task = self_pool_pople ft()
                      if task.next() is None:
                      self_call_exit_ funcs(task)
                      else:
                      self_pool_appen d(task)
                      except Indexerror:
                      pass

                      You could even consider putting the bound task.next methods in
                      the deque instead of the task, and using deque rotation instead
                      of popping and appending. Then, if you put the task.next's in
                      reverse order in the deque to start with, self_pool[-1] will be
                      the first, and self_pool_rotat e() will bring the next into position.
                      Which would make it look like (untested!):

                      def loop(self):
                      self_pool_pop = self.pool.pop
                      self_call_exit_ funcs = self.call_exit_ funcs
                      self_pool_rotat e = self.pool.rotat e
                      try:
                      while True:
                      if self.pool[-1]() is None:
                      self_call_exit_ funcs(self_pool _pop())
                      else:
                      self_pool_rotat e()
                      except Indexerror:
                      pass

                      IWT if the pool remains unchanged most of the time, pool_rotate() ought
                      to be faster than popping and appending. Note that exit_funcs will need
                      a mapping of task.next -> task most likely, unless communication is
                      entirely via a mutable task state object, and all that's needed is to
                      me map to that and mess with exit state there and trigger a final .next()
                      to wrap up the generator.

                      Regards,
                      Bengt Richter

                      Comment

                      • Bengt Richter

                        #12
                        Re: Magic Optimisation

                        On Mon, 05 Sep 2005 21:39:31 GMT, bokr@oz.net (Bengt Richter) wrote:
                        [color=blue]
                        >On 5 Sep 2005 07:27:41 -0700, "Paul McGuire" <ptmcg@austin.r r.com> wrote:
                        >[color=green]
                        >>I still think there are savings to be had by looping inside the
                        >>try-except block, which avoids many setup/teardown exception handling
                        >>steps. This is not so pretty in another way (repeated while on
                        >>check()), but I would be interested in your timings w.r.t. your current
                        >>code.
                        >>
                        >> def loop(self):
                        >> self_pool_pople ft = self.pool.pople ft
                        >> self_pool_appen d = self.pool.appen d
                        >> self_call_exit_ funcs = self.call_exit_ funcs
                        >> check = self.pool.__len __
                        >> while check() > 0:
                        >> try:
                        >> while check() > 0:
                        >> task = self_pool_pople ft()
                        >> task.next()
                        >> self_pool_appen d(task)
                        >> except StopIteration:
                        >> self_call_exit_ funcs(task)
                        >>[/color]
                        >
                        >Why not let popleft trigger an exception out of while True instead,
                        >and prevent tasks from raising StopIteration, and let them yield a None
                        >to indicate keep scheduling with no special action, and something else
                        >for optional differentiation of various exit options, e.g., zero for die,
                        >and nonzero for suspension waiting for event(s) E.g., a returned integer
                        >could be an event mask or single index (+ vs -) for thing(s) to wait for.
                        >If you work things right, event check in the loop can be an if like
                        >if waitedfor&event s: process_events( ), which most of the time is a fast no-op).
                        >
                        >Then (without event stuff, and untested ;-) maybe something like:
                        >
                        > def loop(self):
                        > self_pool_pople ft = self.pool.pople ft
                        > self_pool_appen d = self.pool.appen d
                        > self_call_exit_ funcs = self.call_exit_ funcs
                        > try:
                        > while True:
                        > task = self_pool_pople ft()
                        > if task.next() is None:
                        > self_call_exit_ funcs(task)[/color]
                        ^^^^^^^^^^^^^^^ ^^^^^^^ oops, need to switch if and else branches[color=blue]
                        > else:
                        > self_pool_appen d(task)[/color]
                        ^^^^^^^^^^^^^^^ ^^^^^^^ oops, need to switch if and else branches[color=blue]
                        > except Indexerror:
                        > pass
                        >
                        >You could even consider putting the bound task.next methods in
                        >the deque instead of the task, and using deque rotation instead
                        >of popping and appending. Then, if you put the task.next's in
                        >reverse order in the deque to start with, self_pool[-1] will be
                        >the first, and self_pool_rotat e() will bring the next into position.
                        >Which would make it look like (untested!):
                        >
                        > def loop(self):
                        > self_pool_pop = self.pool.pop
                        > self_call_exit_ funcs = self.call_exit_ funcs
                        > self_pool_rotat e = self.pool.rotat e
                        > try:
                        > while True:
                        > if self.pool[-1]() is None:
                        > self_call_exit_ funcs(self_pool _pop())[/color]
                        ^^^^^^^^^^^^^^^ ^^^^^^^ oops, need to switch if and else branches[color=blue]
                        > else:
                        > self_pool_rotat e()[/color]
                        ^^^^^^^^^^^^^^^ ^^^^^^^ oops, need to switch if and else branches[color=blue]
                        > except Indexerror:
                        > pass
                        >
                        >IWT if the pool remains unchanged most of the time, pool_rotate() ought
                        >to be faster than popping and appending. Note that exit_funcs will need
                        >a mapping of task.next -> task most likely, unless communication is
                        >entirely via a mutable task state object, and all that's needed is to
                        >me map to that and mess with exit state there and trigger a final .next()
                        >to wrap up the generator.
                        >[/color]
                        Sorry. I wonder what else I goofed up ;-)

                        Regards,
                        Bengt Richter

                        Comment

                        • simonwittber@gmail.com

                          #13
                          Re: Magic Optimisation


                          Paul McGuire wrote:[color=blue]
                          > I still think there are savings to be had by looping inside the
                          > try-except block, which avoids many setup/teardown exception handling
                          > steps. This is not so pretty in another way (repeated while on
                          > check()), but I would be interested in your timings w.r.t. your current
                          > code.[/color]

                          Your suggested optimisation worked nicely. It shaved 0.02 seconds from
                          a loop over 10000 iterators, and about 0.002 seconds from a loop over
                          1000 iterators.

                          Comment

                          • ABO

                            #14
                            Re: Magic Optimisation

                            Bengt Richter wrote:[color=blue]
                            > On 5 Sep 2005 07:27:41 -0700, "Paul McGuire" <ptmcg@austin.r r.com> wrote:
                            >[color=green]
                            > >I still think there are savings to be had by looping inside the
                            > >try-except block, which avoids many setup/teardown exception handling
                            > >steps. This is not so pretty in another way (repeated while on
                            > >check()), but I would be interested in your timings w.r.t. your current[/color][/color]
                            [...][color=blue]
                            > Why not let popleft trigger an exception out of while True instead,
                            > and prevent tasks from raising StopIteration, and let them yield a None
                            > to indicate keep scheduling with no special action, and something else[/color]
                            [...]

                            The rule of thumb with exceptions is use them for infrequent events. If
                            you keep to this you end up with clean and fast code.

                            Provided task.next() raises StopIteration less than about 25% of the
                            time it is called, it is cleaner and more efficient to use an exception
                            than to return None. It is also more efficient to handle terminating by
                            allowing popleft trigger an exception. Try the following;

                            def loop(self):
                            self_call_exit_ funcs = self.call_exit_ funcs
                            self_pool_pople ft = self.pool.pople ft
                            self_pool_appen d = self.pool.appen d
                            while True:
                            try:
                            task = self_pool_pople ft()
                            task.next()
                            self_pool_appen d(task)
                            except StopIteration:
                            self_call_exit_ funcs(task)
                            except IndexError:
                            break

                            There are other "optimisati ons" that could be applied that make this
                            code faster but uglier. For example, putting another "while True: loop
                            inside the try block to avoid the try block setup each iteration. Also,
                            exception handling is slower when you specify the exception class (it
                            has to check if the exception matches), so you might be able to arrange
                            this with an anonymous accept: block around the task.next() to handle
                            the StopIteration exception.

                            Another thing that disturbs me is the popfirst/append every iteration.
                            Depending on how many loops through all the tasks before one
                            "finishes", you might find it more efficient to do this;

                            def loop(self):
                            self_pool = self.pool
                            self_pool_remov e = self_pool.remov e
                            self_call_exit_ funcs = self.call_exit_ funcs
                            while self_pool:
                            try:
                            for task in self_pool:
                            task.next()
                            except:
                            self_pool_remov e(task)
                            self_call_exit_ funcs(task)

                            Comment

                            • Terry Reedy

                              #15
                              Re: Magic Optimisation


                              "ABO" <abo@google.com > wrote in message
                              news:1126696724 .760484.311670@ g47g2000cwa.goo glegroups.com.. .[color=blue]
                              > There are other "optimisati ons" that could be applied that make this
                              > code faster but uglier. For example, putting another "while True: loop
                              > inside the try block to avoid the try block setup each iteration.[/color]

                              In CPython, 'setting up' try-blocks is (intentionally) very fast, perhaps
                              as fast or faster than one interation loop.

                              tjr



                              Comment

                              Working...