The Inbox: Collections-cmm.870.mcz

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
6 messages Options
Reply | Threaded
Open this post in threaded view
|

The Inbox: Collections-cmm.870.mcz

commits-2
Chris Muller uploaded a new version of Collections to project The Inbox:
http://source.squeak.org/inbox/Collections-cmm.870.mcz

==================== Summary ====================

Name: Collections-cmm.870
Author: cmm
Time: 3 January 2020, 7:59:19.257837 pm
UUID: b3e2efab-cf35-426d-9910-adab44aa4ed4
Ancestors: Collections-fn.869, Collections-ul.867

Building on Collections-ul.867, let "Dictionary new: anySize" _never_ be significantly slower than "Dictionary new".

=============== Diff against Collections-fn.869 ===============

Item was changed:
  ----- Method: HashedCollection class>>goodPrimeAtLeast: (in category 'sizing') -----
  goodPrimeAtLeast: lowerLimit
+ "Answer the smallest good prime >= lowerlimit.
+ If lowerLimit is larger than the largest known good prime, just make it odd.
+ Use linear search, and exponential search to speed up cases when lowerLimit is small (<2500 and <100000, respectively)."
- "Answer the next good prime >= lowerlimit.
- If lowerLimit is larger than the largest known good prime,
- just make it odd."
 
+ | high mid low prime primes |
- | low mid high prime primes |
  primes := self goodPrimes.
+ (primes at: primes size) < lowerLimit ifTrue: [
- high := primes size.
- lowerLimit > (primes at: high) ifTrue: [
  ^lowerLimit bitOr: 1 ].
+ lowerLimit < 2500 ifTrue: [
+ "Use linear search when the limit is small. The boundary is based on measurements."
+ high := 1.
+ [
+ (prime := primes at: high) >= lowerLimit ifTrue: [ ^prime ].
+ high := high + 1 ] repeat ].
+ lowerLimit < 100000
+ ifTrue: [
+ "Use exponential search when the limit is not too large. The boundary is based on measurements."
+ high := 1.
+ [ (primes at: high) < lowerLimit ] whileTrue: [
+ high := high * 2 ].
+ low := high // 2 + 1. "high // 2 was smaller than lowerLimit" ]
+ ifFalse: [
+ "Regular binary search."
+ low := 1.
+ high := primes size ].
- low := 1.
  [ high - low <= 1 ] whileFalse: [
  mid := high + low // 2.
  prime := primes at: mid.
  lowerLimit < prime
  ifTrue: [ high := mid ]
  ifFalse: [
  lowerLimit > prime
  ifTrue: [ low := mid ]
  ifFalse: [ ^prime ] ] ].
  (primes at: low) >= lowerLimit ifTrue: [ ^primes at: low ].
  ^primes at: high!

Item was changed:
  ----- Method: HashedCollection class>>sizeFor: (in category 'sizing') -----
  sizeFor: nElements
  "Large enough prime (or odd if too large) size to hold nElements with some slop (see fullCheck)"
 
+ nElements < 6 ifTrue: [ ^5 ].
- nElements < 4 ifTrue: [ ^5 ].
  ^self goodPrimeAtLeast: nElements + 1 * 4 // 3!

Item was changed:
  ----- Method: HashedCollection>>compact (in category 'private') -----
  compact
  "Reduce the size of array so that the load factor will be ~75%."
 
  | newCapacity |
+ newCapacity := self class sizeFor: tally.
- newCapacity := self class goodPrimeAtLeast: tally * 4 // 3.
  self growTo: newCapacity!


Reply | Threaded
Open this post in threaded view
|

Re: The Inbox: Collections-cmm.870.mcz

Chris Muller-3
Hi Levente,

Due to ancestry, the diff of this over Collections-ul.867 is only this:

   +       nElements < 6 ifTrue: [ ^5 ].

The issue I'm facing is, I don't know the size of the Dictionary at compile time, but I do at runtime.

    Dictionary new: runtimeVar

However, I don't know what the runtimeVar values will be.  It could be run millions of times with a value less than the default size given by "Dictionary new", or greater.  The implementation is forcing me to optimize for one or the other at compile time, but I can't.  As developer, I'm "torn" whether to write:

       Dictionary new
       Dictionary new: runtimeVar    (44% speed of Dictionary new when runtimeVar < 6)
or    (runtimeVar < 6) ifTrue: [ Dictionary new ] ifFalse: [ Dictionary new: runtimeVar ]

That conflict is what this last tweak tries to solve.  It improves the second case, above, to 89% of "Dictionary new".

Everything about this looks good and would like to include it in 5.3.

 - Chris

On Fri, Jan 3, 2020 at 7:59 PM <[hidden email]> wrote:
Chris Muller uploaded a new version of Collections to project The Inbox:
http://source.squeak.org/inbox/Collections-cmm.870.mcz

==================== Summary ====================

Name: Collections-cmm.870
Author: cmm
Time: 3 January 2020, 7:59:19.257837 pm
UUID: b3e2efab-cf35-426d-9910-adab44aa4ed4
Ancestors: Collections-fn.869, Collections-ul.867

Building on Collections-ul.867, let "Dictionary new: anySize" _never_ be significantly slower than "Dictionary new".

=============== Diff against Collections-fn.869 ===============

Item was changed:
  ----- Method: HashedCollection class>>goodPrimeAtLeast: (in category 'sizing') -----
  goodPrimeAtLeast: lowerLimit
+       "Answer the smallest good prime >= lowerlimit.
+       If lowerLimit is larger than the largest known good prime, just make it odd.
+       Use linear search, and exponential search to speed up cases when lowerLimit is small (<2500 and <100000, respectively)."
-       "Answer the next good prime >= lowerlimit.
-       If lowerLimit is larger than the largest known good prime,
-       just make it odd."

+       | high mid low prime primes |
-       | low mid high prime primes |
        primes := self goodPrimes.
+       (primes at: primes size) < lowerLimit ifTrue: [
-       high := primes size.
-       lowerLimit > (primes at: high) ifTrue: [
                ^lowerLimit bitOr: 1 ].
+       lowerLimit < 2500 ifTrue: [
+               "Use linear search when the limit is small. The boundary is based on measurements."
+               high := 1.
+               [
+                       (prime := primes at: high) >= lowerLimit ifTrue: [ ^prime ].
+                       high := high + 1 ] repeat ].
+       lowerLimit < 100000
+               ifTrue: [
+                       "Use exponential search when the limit is not too large. The boundary is based on measurements."
+                       high := 1.
+                       [ (primes at: high) < lowerLimit ] whileTrue: [
+                               high := high * 2 ].
+                       low := high // 2 + 1. "high // 2 was smaller than lowerLimit" ]
+               ifFalse: [
+                       "Regular binary search."
+                       low := 1.
+                       high := primes size ].
-       low := 1.
        [ high - low <= 1 ] whileFalse: [
                mid := high + low // 2.
                prime := primes at: mid.
                lowerLimit < prime
                        ifTrue: [ high := mid ]
                        ifFalse: [
                                lowerLimit > prime
                                        ifTrue: [ low := mid ]
                                        ifFalse: [ ^prime ] ] ].
        (primes at: low) >= lowerLimit ifTrue: [ ^primes at: low ].
        ^primes at: high!

Item was changed:
  ----- Method: HashedCollection class>>sizeFor: (in category 'sizing') -----
  sizeFor: nElements
        "Large enough prime (or odd if too large) size to hold nElements with some slop (see fullCheck)"

+       nElements < 6 ifTrue: [ ^5 ].
-       nElements < 4 ifTrue: [ ^5 ].
        ^self goodPrimeAtLeast: nElements + 1 * 4 // 3!

Item was changed:
  ----- Method: HashedCollection>>compact (in category 'private') -----
  compact
        "Reduce the size of array so that the load factor will be ~75%."

        | newCapacity |
+       newCapacity := self class sizeFor: tally.
-       newCapacity := self class goodPrimeAtLeast: tally * 4 // 3.
        self growTo: newCapacity!




Reply | Threaded
Open this post in threaded view
|

Re: The Inbox: Collections-cmm.870.mcz

Levente Uzonyi
Hi Chris,

The problem with that change is that #sizeFor: returns incorrect values
when its argument is 4 or 5. The change breaks #compact, and the contract
of #new: and #sizeFor:, because when you want to store 4 or 5 elements,
you'll get a HashedCollection which can only store 3 elements, so there'll
either be an extra grow involved or the collection will be overfilled.

Here's an example where the change makes #compact break the contract of
HashedCollecions:

  (1 to: 5) asSet compact; includes: 6 "#includes: will raise an error"

The default HashedCollection instances created with #new can hold 3
elements without growing.
If you need a fast way to create a Dictionary that can hold 5 elements
without growing, then you can add a new method and use that in your code:

HashedCollection class >> #newFor5

  ^self basicNew initialize: 7

This way the method guarantees the capacity is a good prime just like
#new does. And it won't affect anything else.

Or we could change #new: to create a larger array by default, but then we
would use more memory. In my image 68.7% of all the HashedCollections have
the default capacity: 5. And 69% of all HashedCollections have fewer than
3 elements, so those could all be shrunk down to capacity 3, saving 232kB
memory (64-bit image).
If we changed the default capacity to 7, the size of the image would grow
by 231kB, which is not much nowadays, but still something.

Perhaps we could create a class variable to hold the default capacity:

HashedCollection class >> #new

  ^self basicNew initialize: DefaultCapacity

And then you could change it to 7 or 11 in your images.


Levente

On Fri, 3 Jan 2020, Chris Muller wrote:

> Hi Levente,
> Due to ancestry, the diff of this over Collections-ul.867 is only this:
>
>    +       nElements < 6 ifTrue: [ ^5 ].
>
> The issue I'm facing is, I don't know the size of the Dictionary at compile time, but I do at runtime.
>
>     Dictionary new: runtimeVar
>
> However, I don't know what the runtimeVar values will be.  It could be run millions of times with a value less than the default size given by "Dictionary new", or greater.  The implementation is forcing me to optimize for one
> or the other at compile time, but I can't.  As developer, I'm "torn" whether to write:
>
>        Dictionary new
>        Dictionary new: runtimeVar    (44% speed of Dictionary new when runtimeVar < 6)
> or    (runtimeVar < 6) ifTrue: [ Dictionary new ] ifFalse: [ Dictionary new: runtimeVar ]
>
> That conflict is what this last tweak tries to solve.  It improves the second case, above, to 89% of "Dictionary new".
>
> Everything about this looks good and would like to include it in 5.3.
>
>  - Chris
>
> On Fri, Jan 3, 2020 at 7:59 PM <[hidden email]> wrote:
>       Chris Muller uploaded a new version of Collections to project The Inbox:
>       http://source.squeak.org/inbox/Collections-cmm.870.mcz
>
>       ==================== Summary ====================
>
>       Name: Collections-cmm.870
>       Author: cmm
>       Time: 3 January 2020, 7:59:19.257837 pm
>       UUID: b3e2efab-cf35-426d-9910-adab44aa4ed4
>       Ancestors: Collections-fn.869, Collections-ul.867
>
>       Building on Collections-ul.867, let "Dictionary new: anySize" _never_ be significantly slower than "Dictionary new".
>
>       =============== Diff against Collections-fn.869 ===============
>
>       Item was changed:
>         ----- Method: HashedCollection class>>goodPrimeAtLeast: (in category 'sizing') -----
>         goodPrimeAtLeast: lowerLimit
>       +       "Answer the smallest good prime >= lowerlimit.
>       +       If lowerLimit is larger than the largest known good prime, just make it odd.
>       +       Use linear search, and exponential search to speed up cases when lowerLimit is small (<2500 and <100000, respectively)."
>       -       "Answer the next good prime >= lowerlimit.
>       -       If lowerLimit is larger than the largest known good prime,
>       -       just make it odd."
>
>       +       | high mid low prime primes |
>       -       | low mid high prime primes |
>               primes := self goodPrimes.
>       +       (primes at: primes size) < lowerLimit ifTrue: [
>       -       high := primes size.
>       -       lowerLimit > (primes at: high) ifTrue: [
>                       ^lowerLimit bitOr: 1 ].
>       +       lowerLimit < 2500 ifTrue: [
>       +               "Use linear search when the limit is small. The boundary is based on measurements."
>       +               high := 1.
>       +               [
>       +                       (prime := primes at: high) >= lowerLimit ifTrue: [ ^prime ].
>       +                       high := high + 1 ] repeat ].
>       +       lowerLimit < 100000
>       +               ifTrue: [
>       +                       "Use exponential search when the limit is not too large. The boundary is based on measurements."
>       +                       high := 1.
>       +                       [ (primes at: high) < lowerLimit ] whileTrue: [
>       +                               high := high * 2 ].
>       +                       low := high // 2 + 1. "high // 2 was smaller than lowerLimit" ]
>       +               ifFalse: [
>       +                       "Regular binary search."
>       +                       low := 1.
>       +                       high := primes size ].
>       -       low := 1.
>               [ high - low <= 1 ] whileFalse: [
>                       mid := high + low // 2.
>                       prime := primes at: mid.
>                       lowerLimit < prime
>                               ifTrue: [ high := mid ]
>                               ifFalse: [
>                                       lowerLimit > prime
>                                               ifTrue: [ low := mid ]
>                                               ifFalse: [ ^prime ] ] ].
>               (primes at: low) >= lowerLimit ifTrue: [ ^primes at: low ].
>               ^primes at: high!
>
>       Item was changed:
>         ----- Method: HashedCollection class>>sizeFor: (in category 'sizing') -----
>         sizeFor: nElements
>               "Large enough prime (or odd if too large) size to hold nElements with some slop (see fullCheck)"
>
>       +       nElements < 6 ifTrue: [ ^5 ].
>       -       nElements < 4 ifTrue: [ ^5 ].
>               ^self goodPrimeAtLeast: nElements + 1 * 4 // 3!
>
>       Item was changed:
>         ----- Method: HashedCollection>>compact (in category 'private') -----
>         compact
>               "Reduce the size of array so that the load factor will be ~75%."
>
>               | newCapacity |
>       +       newCapacity := self class sizeFor: tally.
>       -       newCapacity := self class goodPrimeAtLeast: tally * 4 // 3.
>               self growTo: newCapacity!
>
>
>
>

Reply | Threaded
Open this post in threaded view
|

Re: The Inbox: Collections-cmm.870.mcz

Chris Muller-4
Thanks again Levente, once again, I somehow got my brain stuck :) and thought "Dictionary new" was the same as "Dictionary new: 7", but, in fact, it's the same as "Dictionary new: 3".  Again, I conflated the desired size with the internal array's size needing extra space, sigh...   It was this finally cleared it up for me:

   (1 to: 10) collect: [ : n | Dictionary sizeFor: n ].     " #(5 5 5 7 11 11 11 13 13 17)"

So, back to my "problem", it looks like I'm safe to write:

      Dictionary new: runtimeVar

and, no matter what size, there will never be a performance hit over Dictionary new.  Whew!

Thanks,
  Chris

On Fri, Jan 3, 2020 at 9:25 PM Levente Uzonyi <[hidden email]> wrote:
Hi Chris,

The problem with that change is that #sizeFor: returns incorrect values
when its argument is 4 or 5. The change breaks #compact, and the contract
of #new: and #sizeFor:, because when you want to store 4 or 5 elements,
you'll get a HashedCollection which can only store 3 elements, so there'll
either be an extra grow involved or the collection will be overfilled.

Here's an example where the change makes #compact break the contract of
HashedCollecions:

        (1 to: 5) asSet compact; includes: 6 "#includes: will raise an error"

The default HashedCollection instances created with #new can hold 3
elements without growing.
If you need a fast way to create a Dictionary that can hold 5 elements
without growing, then you can add a new method and use that in your code:

HashedCollection class >> #newFor5

        ^self basicNew initialize: 7

This way the method guarantees the capacity is a good prime just like
#new does. And it won't affect anything else.

Or we could change #new: to create a larger array by default, but then we
would use more memory. In my image 68.7% of all the HashedCollections have
the default capacity: 5. And 69% of all HashedCollections have fewer than
3 elements, so those could all be shrunk down to capacity 3, saving 232kB
memory (64-bit image).
If we changed the default capacity to 7, the size of the image would grow
by 231kB, which is not much nowadays, but still something.

Perhaps we could create a class variable to hold the default capacity:

HashedCollection class >> #new

        ^self basicNew initialize: DefaultCapacity

And then you could change it to 7 or 11 in your images.


Levente

On Fri, 3 Jan 2020, Chris Muller wrote:

> Hi Levente,
> Due to ancestry, the diff of this over Collections-ul.867 is only this:
>
>    +       nElements < 6 ifTrue: [ ^5 ].
>
> The issue I'm facing is, I don't know the size of the Dictionary at compile time, but I do at runtime.
>
>     Dictionary new: runtimeVar
>
> However, I don't know what the runtimeVar values will be.  It could be run millions of times with a value less than the default size given by "Dictionary new", or greater.  The implementation is forcing me to optimize for one
> or the other at compile time, but I can't.  As developer, I'm "torn" whether to write:
>
>        Dictionary new
>        Dictionary new: runtimeVar    (44% speed of Dictionary new when runtimeVar < 6)
> or    (runtimeVar < 6) ifTrue: [ Dictionary new ] ifFalse: [ Dictionary new: runtimeVar ]
>
> That conflict is what this last tweak tries to solve.  It improves the second case, above, to 89% of "Dictionary new".
>
> Everything about this looks good and would like to include it in 5.3.
>
>  - Chris
>
> On Fri, Jan 3, 2020 at 7:59 PM <[hidden email]> wrote:
>       Chris Muller uploaded a new version of Collections to project The Inbox:
>       http://source.squeak.org/inbox/Collections-cmm.870.mcz
>
>       ==================== Summary ====================
>
>       Name: Collections-cmm.870
>       Author: cmm
>       Time: 3 January 2020, 7:59:19.257837 pm
>       UUID: b3e2efab-cf35-426d-9910-adab44aa4ed4
>       Ancestors: Collections-fn.869, Collections-ul.867
>
>       Building on Collections-ul.867, let "Dictionary new: anySize" _never_ be significantly slower than "Dictionary new".
>
>       =============== Diff against Collections-fn.869 ===============
>
>       Item was changed:
>         ----- Method: HashedCollection class>>goodPrimeAtLeast: (in category 'sizing') -----
>         goodPrimeAtLeast: lowerLimit
>       +       "Answer the smallest good prime >= lowerlimit.
>       +       If lowerLimit is larger than the largest known good prime, just make it odd.
>       +       Use linear search, and exponential search to speed up cases when lowerLimit is small (<2500 and <100000, respectively)."
>       -       "Answer the next good prime >= lowerlimit.
>       -       If lowerLimit is larger than the largest known good prime,
>       -       just make it odd."
>
>       +       | high mid low prime primes |
>       -       | low mid high prime primes |
>               primes := self goodPrimes.
>       +       (primes at: primes size) < lowerLimit ifTrue: [
>       -       high := primes size.
>       -       lowerLimit > (primes at: high) ifTrue: [
>                       ^lowerLimit bitOr: 1 ].
>       +       lowerLimit < 2500 ifTrue: [
>       +               "Use linear search when the limit is small. The boundary is based on measurements."
>       +               high := 1.
>       +               [
>       +                       (prime := primes at: high) >= lowerLimit ifTrue: [ ^prime ].
>       +                       high := high + 1 ] repeat ].
>       +       lowerLimit < 100000
>       +               ifTrue: [
>       +                       "Use exponential search when the limit is not too large. The boundary is based on measurements."
>       +                       high := 1.
>       +                       [ (primes at: high) < lowerLimit ] whileTrue: [
>       +                               high := high * 2 ].
>       +                       low := high // 2 + 1. "high // 2 was smaller than lowerLimit" ]
>       +               ifFalse: [
>       +                       "Regular binary search."
>       +                       low := 1.
>       +                       high := primes size ].
>       -       low := 1.
>               [ high - low <= 1 ] whileFalse: [
>                       mid := high + low // 2.
>                       prime := primes at: mid.
>                       lowerLimit < prime
>                               ifTrue: [ high := mid ]
>                               ifFalse: [
>                                       lowerLimit > prime
>                                               ifTrue: [ low := mid ]
>                                               ifFalse: [ ^prime ] ] ].
>               (primes at: low) >= lowerLimit ifTrue: [ ^primes at: low ].
>               ^primes at: high!
>
>       Item was changed:
>         ----- Method: HashedCollection class>>sizeFor: (in category 'sizing') -----
>         sizeFor: nElements
>               "Large enough prime (or odd if too large) size to hold nElements with some slop (see fullCheck)"
>
>       +       nElements < 6 ifTrue: [ ^5 ].
>       -       nElements < 4 ifTrue: [ ^5 ].
>               ^self goodPrimeAtLeast: nElements + 1 * 4 // 3!
>
>       Item was changed:
>         ----- Method: HashedCollection>>compact (in category 'private') -----
>         compact
>               "Reduce the size of array so that the load factor will be ~75%."
>
>               | newCapacity |
>       +       newCapacity := self class sizeFor: tally.
>       -       newCapacity := self class goodPrimeAtLeast: tally * 4 // 3.
>               self growTo: newCapacity!
>
>
>
>


Reply | Threaded
Open this post in threaded view
|

Re: The Inbox: Collections-cmm.870.mcz

Chris Muller-3

So, back to my "problem", it looks like I'm safe to write:

      Dictionary new: runtimeVar

and, no matter what size, there will never be a performance hit over Dictionary new.  Whew!
 

The above is only true with what's currently in trunk.  With Collections-ul.867, there is still the case for

     Dictionary new

vs

     Dictionary new: 3

I hope I'm not missing something again, but because of that I think we should revert your change to HashedCollection class>>#sizeFor:, which is what this new Collections-cmm.870 with uuid c4bf7594-c44e-4d02-9956-2b727d2eb75b does (argh, forgot to up the version #, oh well).

 Best,
  Chris


Reply | Threaded
Open this post in threaded view
|

Re: The Inbox: Collections-cmm.870.mcz

Levente Uzonyi
Hi Chris,

The problem with reverting #sizeFor: is that compaction will not be able
to go below capacity 5, so capacity 3 (suitable for collections of size 0,
1 or 2) would not be available ever.

On Sat, 4 Jan 2020, Chris Muller wrote:

>
>       So, back to my "problem", it looks like I'm safe to write:
>
>       Dictionary new: runtimeVar
>
> and, no matter what size, there will never be a performance hit over Dictionary new.  Whew!

As we discussed before, the allocation cost of #new: can not be lower than
the cost of #new, because #new has a hardcoded prime value, while #new:
has to calculate a good prime. But filling up the object created by #new
will be slower than the one created by #new: when you want to store more
than 3 elements and you know the maximum number of elements you want to
store beforehand.

>
>  
>
> The above is only true with what's currently in trunk.  With Collections-ul.867, there is still the case for
>
>      Dictionary new
>
> vs
>
>      Dictionary new: 3
>
> I hope I'm not missing something again, but because of that I think we should revert your change to HashedCollection class>>#sizeFor:, which is what this new Collections-cmm.870 with uuid c4bf7594-c44e-4d02-9956-2b727d2eb75b does (argh, forgot to up the version #, oh well).
It feels like you're trying to use the "optimization" in #sizeFor: to your
needs. But you should know that the "optimization" is only there, because
for small numbers the formula [elements + 1 * 4 // 3] is not a good enough
approximation of [(elements * 4 / 3) ceiling]. Actually, that formula is
simply wrong. The correct formula is [elements * 4 + 2 // 3]. I'll push a
new version with that formula.
But before I do, can you give me the exact cases you're tring to optimize
for?


Levente

>
>  Best,
>   Chris
>
>