Skip to content

tuples should remember their hash value #53894

Closed

Description

@dtorp
mannequin
BPO 9685
Nosy @tim-one, @birkenfeld, @rhettinger, @mdickinson, @tiran, @benjaminp, @meadori, @serhiy-storchaka, @asottile
Files
  • tuplehash.patch
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = None
    closed_at = <Date 2013-01-07.19:35:41.245>
    created_at = <Date 2010-08-25.19:33:15.558>
    labels = ['interpreter-core', 'easy', 'performance']
    title = 'tuples should remember their hash value'
    updated_at = <Date 2020-03-15.20:46:20.362>
    user = 'https://bugs.python.org/dtorp'

    bugs.python.org fields:

    activity = <Date 2020-03-15.20:46:20.362>
    actor = 'Anthony Sottile'
    assignee = 'none'
    closed = True
    closed_date = <Date 2013-01-07.19:35:41.245>
    closer = 'benjamin.peterson'
    components = ['Interpreter Core']
    creation = <Date 2010-08-25.19:33:15.558>
    creator = 'dtorp'
    dependencies = []
    files = ['28605']
    hgrepos = []
    issue_num = 9685
    keywords = ['patch', 'easy']
    message_count = 14.0
    messages = ['114929', '114930', '114932', '114936', '114937', '114939', '179202', '179235', '179248', '179250', '179254', '179277', '179281', '364258']
    nosy_count = 11.0
    nosy_names = ['tim.peters', 'georg.brandl', 'rhettinger', 'mark.dickinson', 'dtorp', 'christian.heimes', 'benjamin.peterson', 'meador.inge', 'python-dev', 'serhiy.storchaka', 'Anthony Sottile']
    pr_nums = []
    priority = 'low'
    resolution = 'rejected'
    stage = 'patch review'
    status = 'closed'
    superseder = None
    type = 'performance'
    url = 'https://bugs.python.org/issue9685'
    versions = ['Python 3.4']

    Activity

    1. dtorp commented on Aug 25, 2010

      dtorpmannequin
      MannequinAuthor

      Dictionary keys are commonly numbers, strings, or tuples. Python has optimized numbers and strings to remember their hash values on successive calls. Tuples should do this too since their recursive hash function can take a long time to compute. Tuples are Python's official record type and the one obvious way of making non-scalar dictionary keys.

      The code to do this in stringobject.c is short and sweet, so this major speed boost should be an easy thing to.

      static long
      string_hash(PyStringObject *a)
      {
      register Py_ssize_t len;
      register unsigned char *p;
      register long x;

          if (a->ob_shash != -1)               <==
              return a->ob_shash;              <==
          len = Py_SIZE(a);
          p = (unsigned char *) a->ob_sval;
          x = *p << 7;
          while (--len >= 0)
              x = (1000003*x) ^ *p++;
          x ^= Py_SIZE(a);
          if (x == -1)                         <==
              x = -2;                          <==
          a->ob_shash = x;
          return x;
      }

      The code in tupleobject.c would just need to add the four lines marked above. Here's what is looks like now.

      static long
      tuplehash(PyTupleObject *v)
      {
          register long x, y;
          register Py_ssize_t len = Py_SIZE(v);
          register PyObject **p;
          long mult = 1000003L;
          x = 0x345678L;
          p = v->ob_item;
          while (--len >= 0) {
              y = PyObject_Hash(*p++);
              if (y == -1)
                  return -1;
              x = (x ^ y) * mult;
              /* the cast might truncate len; that doesn't change hash stability */
              mult += (long)(82520L + len + len);
          }
          x += 97531L;
          if (x == -1)
              x = -2;
          return x;
      }

      Thank you guys for all of your work.

      *David

    2. rhettinger commented on Aug 25, 2010

      @rhettinger
      Contributor

      This seems reasonable. Will look at it in the next few days.

    3. self-assigned this
      on Aug 25, 2010
    4. tim-one commented on Aug 25, 2010

      @tim-one
      Member
      • Tuple objects don't currently reserve space to store their hash code, so it's likely this would increase the size of every tuple.

      • It's unclear to me which natural use patterns would actually enjoy a major speed boost. Note that dicts remember the hash codes of keys already, regardless of whether the key type remembers them too. A tuple is typically constructed right before being used in a dict lookup, so for a one-shot use no time would be saved. If the new tuple is used in multiple dict lookups, sure - but is that common?

    5. benjaminp commented on Aug 25, 2010

      @benjaminp
      Contributor

      FWIW, I'm -1 on this without a demonstrable improvement on some real-world cases.

    6. rhettinger commented on Aug 25, 2010

      @rhettinger
      Contributor

      Hello Tim! If you have a chance, please also take a look at bpo-9685 which I was planning to work on in the next couple of weeks.

      For memoizing tuple hashes, I'm inclined to think the one extra field is worth it. That would help all the cases where people are concerned about double accesses to dicts in a look-before-you-leap pattern or for a pattern of fetch-item-update-value-store-new-item.

      It looks like the code for collections.OrderedDict() would benefit because it does multiple lookups and stores on the same key: http://svn.python.org/view/python/branches/release27-maint/Lib/collections.py?revision=84148&view=markup

      It would also help the multiple lookups and stores in caching code such as that at http://code.activestate.com/recipes/498245-lru-and-lfu-cache-decorators

      I suppose we could prepare a patch, instrument it, and try it with Twisted, SQLalchemy, and Django to find-out how many tuple hash calculations would be saved by memoizing.

    7. benjaminp commented on Aug 25, 2010

      @benjaminp
      Contributor

      2010/8/25 Raymond Hettinger <[email protected]>:

      I suppose we could prepare a patch, instrument it, and try it with Twisted, SQLalchemy, and Django to find-out how many tuple hash calculations would be saved by memoizing.

      You should also check memory usage.

    8. removed their assignment
      on Oct 31, 2010
    9. added
      interpreter-core(Objects, Python, Grammar, and Parser dirs)
      performancePerformance or resource usage
      and removed
      performancePerformance or resource usage
      on Jan 4, 2013
    10. serhiy-storchaka commented on Jan 6, 2013

      @serhiy-storchaka
      Member

      Sorry, Raymond. It was a bad day for Roundup.

    11. removed their assignment
      on Jan 6, 2013
    12. serhiy-storchaka commented on Jan 6, 2013

      @serhiy-storchaka
      Member

      My apologies once again, Raymond. I mistakenly thought that I unassigned the issue from you (it was a Roundup bug at this day).

      As for the issue, I totally agree with Tim.

    13. mdickinson commented on Jan 7, 2013

      @mdickinson
      Member

      Given the responses so far, I suggest closing this as rejected.

    14. tiran commented on Jan 7, 2013

      @tiran
      Member

      I'm not too worried about the slightly increased memory usage. For example one of our largest application instances consumes about 8 GB memory right now. It has just about 22k tuples in gc.get_objects(). An additional Py_hash_t in tuple's struct would increase the memory usage by less than 200kB.

      I've attached a simple patch.

    15. birkenfeld commented on Jan 7, 2013

      @birkenfeld
      Member

      Still, actual benefits in some kind of benchmark will be needed to show that this is not a premature optimization.

    16. tiran commented on Jan 7, 2013

      @tiran
      Member

      Benchmark doesn't show any serious improvements. Some test cases are even slower.

      Report on Linux freimann 3.2.0-35-generic #55-Ubuntu SMP Wed Dec 5 17:42:16 UTC 2012 x86_64 x86_64
      Total CPU cores: 6

      ### call_simple ###
      Min: 0.201824 -> 0.208248: 1.03x slower
      Avg: 0.210608 -> 0.217300: 1.03x slower
      Significant (t=-2.23)
      Stddev: 0.00818 -> 0.00829: 1.0134x larger
      Timeline: b'http://tinyurl.com/axqoqp4'

      ### go ###
      Min: 0.534641 -> 0.550004: 1.03x slower
      Avg: 0.537874 -> 0.552782: 1.03x slower
      Significant (t=-11.89)
      Stddev: 0.00184 -> 0.00211: 1.1495x larger
      Timeline: b'http://tinyurl.com/b5k3ua4'

      ### pathlib ###
      Min: 0.121589 -> 0.117025: 1.04x faster
      Avg: 0.126679 -> 0.122279: 1.04x faster
      Significant (t=3.64)
      Stddev: 0.00429 -> 0.00427: 1.0048x smaller
      Timeline: b'http://tinyurl.com/acbb69o'

      ### spectral_norm ###
      Min: 0.280749 -> 0.305213: 1.09x slower
      Avg: 0.281194 -> 0.305390: 1.09x slower
      Significant (t=-120.69)
      Stddev: 0.00044 -> 0.00011: 4.1101x smaller
      Timeline: b'http://tinyurl.com/awyeejp'

      The following not significant results are hidden, use -v to show them:
      call_method, call_method_slots, call_method_unknown, chaos, fannkuch, fastpickle, fastunpickle, float, formatted_logging, iterative_count, json_dump_v2, json_load, meteor_contest, nbody, normal_startup, nqueens, pidigits, raytrace, regex_compile, regex_effbot, regex_v8, richards, silent_logging, simple_logging, startup_nosite, telco, threaded_count, unpack_sequence.

    17. python-dev commented on Jan 7, 2013

      python-devmannequin
      Mannequin

      New changeset 17038de56fd4 by Christian Heimes in branch 'default':
      Add a comment about *not* caching the hash value. Issue bpo-9685 suggested to memorize the hash value, but the feature request was rejected because no speed ups were found.
      http://hg.python.org/cpython/rev/17038de56fd4

    18. asottile commented on Mar 15, 2020

      asottilemannequin
      Mannequin

      hit this today unfortunately -- I'm working with some pretty complex (and nested) typing.NamedTuple objects and the lack of caching here results in quite the slowdown (in my macro benchmarks it's the difference between a render pass taking 180ms and 25ms)

      the class in question is being used as a cache key:

      https://github.com/asottile/babi/blob/1be4e80eddc1bff0eb8047cc89337fdf006ad148/babi/highlight.py#L217

      with the hash cached:
      
      μs      event
      123744  startup
      27833   kEND5
      2859    ^X
      

      without the hash cached:

      μs      event
      122575  startup
      180415  kEND5
      3725    ^X
      

      (my hash cache of course being slower than it could be in C)

      @@ -214,10 +214,21 @@ class Region(NamedTuple):
           scope: Scope
       
       
      +_state_hash_cache = {}
      +
      +
       class State(NamedTuple):
           entries: Tuple['Entry', ...]
           while_stack: Tuple[Tuple['WhileRule', int], ...]
       
      +    def __hash__(self):
      +        k = id(self)
      +        try:
      +            return _state_hash_cache[k]
      +        except KeyError:
      +            ret = _state_hash_cache[k] = super().__hash__()
      +            return ret
      +
           @classmethod
           def root(cls, entry: 'Entry') -> 'State':
               return cls((entry,), ())
      

      I get that this is a pretty extreme case and it's unlikely to change the resolution of this issue, but figured I'd point it out in case anyone else is hitting similar issues

    19. transferred this issue fromon Apr 10, 2022
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Metadata

    Metadata

    Assignees

    No one assigned

      Labels

      easyinterpreter-core(Objects, Python, Grammar, and Parser dirs)performancePerformance or resource usage

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions