Live data from Hacker News

Python sets and dictionaries can have quadratic-time performance

lemire.me

51–60 of 67 posts

Re: Python sets and dictionaries can have quadratic-time performance

#51
I call shenanigans on this.

    import timeit
    def test(M,n):
        values = [i * M for i in range(1, n + 1)]
        s = set(values)
        sum(v in s for v in values)
    M = (1 ", timeit.timeit(lambda: test(M,n), number=3))
    for n in [1000, 2000, 4000, 8000, 16000]:
        print(f"M=1,      {n=:5d} ->", timeit.timeit(lambda: test(1,n), number=3))
Magically, when you stop using BIGINTS as the set members and just use regular ints, there is no such quadratic explosion.

    M=2^61-1, n= 1000 -> 0.13144792200182565
    M=2^61-1, n= 2000 -> 0.48016051898594014
    M=2^61-1, n= 4000 -> 2.058760045998497
    M=2^61-1, n= 8000 -> 7.843778470996767
    M=2^61-1, n=16000 -> 40.01485426299041
    M=1,      n= 1000 -> 0.000748768012272194
    M=1,      n= 2000 -> 0.0015750699967611581
    M=1,      n= 4000 -> 0.003037029004190117
    M=1,      n= 8000 -> 0.006664915999863297
    M=1,      n=16000 -> 0.012693285010755062
The runtime is being spent hashing bigints, comparing candidate bigint(s) against reference bigints, and summing bigints. And there's also some set lookups.

Re: Python sets and dictionaries can have quadratic-time performance

#52
post #46

Earlier quoted context omitted.

The part where he chooses his inputs to hit worst case behavior in Python's hash function.

He doesn't. Inputs are: M = (1 which are effectively random from the hash function's point of view, especially with a randomized seed (the default on current versions).

His inputs are large numbers that don't fit in a standard integer. Bigints. The set inclusion test not only has a hash lookup but an equality test, which will be a bigint comparision rather than integer comparison, and bitint comparison is itself O(n) based on the size of the bignum. And the code that tests each bignum is in the set also _sums_ those bignums, which itself is an O(n) operation based on the size of the bignums being summed.

So he's not testing dict/set performance, he's testing bignum performance, because of the inputs he deliberately chose

https://news.ycombinator.com/item?id=49650737

Re: Python sets and dictionaries can have quadratic-time performance

#53
post #46

Earlier quoted context omitted.

The part where he chooses his inputs to hit worst case behavior in Python's hash function.

He doesn't. Inputs are: M = (1 which are effectively random from the hash function's point of view, especially with a randomized seed (the default on current versions).

CPython has the unfortunate property that ints aren’t covered by hash randomization, and `hash(x) == x % ((1 << 61) - 1)` always.

Re: Python sets and dictionaries can have quadratic-time performance

#54

Raymond Hettinger has a great talk about how much python's dict has improved over the years. So this is super interesting and will probably just make the builtin dict better eventually. The lesson of the talk is that if you are idiomatic then you will benefit as the language improves. https://www.youtube.com/watch?v=npw4s1QTmPg

Dave Beazley has a great talk about using Python built ins.

Def worth a watch as he is funny but also super cool to see someone use a REPL this way:

https://www.youtube.com/watch?v=lyDLAutA88s

Re: Python sets and dictionaries can have quadratic-time performance

#55

Uh, what is going on with this benchmark? Why is M so big? Why does it cross the maxint boundary? Why is constructing the list comprehension part of the benchmark? Why are we summing the set? Why are we only measuring 5 values for n?

Yea if I cast the large calculated integers to strings, performance is O(1) `values = [str(i * M) for i in range(1, n + 1)]` or `values = [i * M % 1_000_000_000_000_000 for i in range(1, n + 1)]`

Part of the secret explained elsewhere on this HN post is that the OP is selecting values that all collide. Most hash tables handle collisions with linked lists that would be linear insert. It's O(1) average case but O(n) if you pull an "oops all collisions on the same bucket" stunt.

Re: Python sets and dictionaries can have quadratic-time performance

#56

I call shenanigans on this. import timeit def test(M,n): values = [i * M for i in range(1, n + 1)] s = set(values) sum(v in s for v in values) M = (1 ", timeit.timeit(lambda: test(M,n), number=3)) for n in [1000, 2000, 4000, 8000, 16000]: print(f"M=1, {n=:5d} ->", timeit.timeit(lambda: test(1,n), number=3)) Magically, when you stop using BIGINTS as the set members and just use regular ints, there is no such quadratic…

Actually, as the Google AI just taught me [1], the bad performance results from hash _collisions_, not from using bigints (which the author also mentions):

    import timeit
    
    def test(M, n):
        values = [i * M for i in range(1, n + 1)]
        s = set(values)
        sum(v in s for v in values)
    
    M = (1 ", timeit.timeit(lambda: test(M,n), number=3))
    
    # This runs with normal performance
    for n in [1000, 2000, 4000, 8000, 16000]:
        print(f"N=2^61+42, {n=:5d} ->", timeit.timeit(lambda: test(N,n), number=3))
(1 This is not completely theoretical; hash-DoS attacks make use of that. For this reason, there is hash salting since Python 3.3 for strings, bytes, and datetime objects [2], but not for integers, because the most common attack surface is JSON, but JSON keys are strings, and hash salting would slow down the performance of math operations.

[1] https://share.google/aimode/cXQyw0SDPr5FnhBc5, available for seven days

[2] See the grey info box here: https://docs.python.org/3/reference/datamodel.html#object.__...

Re: Python sets and dictionaries can have quadratic-time performance

#57

I call shenanigans on this. import timeit def test(M,n): values = [i * M for i in range(1, n + 1)] s = set(values) sum(v in s for v in values) M = (1 ", timeit.timeit(lambda: test(M,n), number=3)) for n in [1000, 2000, 4000, 8000, 16000]: print(f"M=1, {n=:5d} ->", timeit.timeit(lambda: test(1,n), number=3)) Magically, when you stop using BIGINTS as the set members and just use regular ints, there is no such quadratic…

Actually, as the Google AI just taught me [1], the bad performance results from hash _collisions_, not from using bigints (which the author also mentions): import timeit def test(M, n): values = [i * M for i in range(1, n + 1)] s = set(values) sum(v in s for v in values) M = (1 ", timeit.timeit(lambda: test(M,n), number=3)) # This runs with normal performance for n in [1000, 2000, 4000, 8000, 16000]: print(f"N=2^61+4…

> (1 So this hinges on a contrived set of integer keys, which python's hashing algorithm is susceptible to?

It's not super clear from the article that the choice of key was specifically chosen to generate these hash collisions (though it is more evident on a re-read). The article leads one to believe that the likelihood of this collision is common:

> "I can ‘easily’ make my version of Python crumble"

> "To put it differently, saying that a hash table is O(1) or constant time is a model. It can be true, maybe even often, but it is not reality."

It feels very misleading to say that "Python sets and dictionaries can have quadratic-time performance", as though this may be a common occurrence in the wild. Perhaps if this behaviour had been accidentally discovered in the wild, that would make for an interesting anecdote? It feels like the lesson is more accurately put: "hash tables are susceptible to hash collisions".

I guess ultimately I come to a different conclusion than the original blog post. They say: "Some models are useful but none of them is reality. Be mindful of cognitive biases." It reads to me as having an air of "you can't trust anything." I think I would describe this conclusion more like "abstractions are leaky, and it is helpful to have a basic understanding of what's happening under the hood. Even for something as elemental as a dict."

And in that sense, if I were making this point with regards to computer science I might lean on a more common false assumption like "the network is reliable". (Or establish early-on in the article that we're identifying a similar false assumption about dicts.)

Anyway, I think I'm sensitive to articles picking on python.. but perhaps the title was clickbait. Is there another language with a clearly superior approach that python should emulate?

Re: Python sets and dictionaries can have quadratic-time performance

#60
post #46

Earlier quoted context omitted.

He doesn't. Inputs are: M = (1 which are effectively random from the hash function's point of view, especially with a randomized seed (the default on current versions).

CPython has the unfortunate property that ints aren’t covered by hash randomization, and `hash(x) == x % ((1 << 61) - 1)` always.

Ouch, that's a big footgun. Why was the lack of randomization considered a vulnerability, but not this?
Post reply on HN