Skip to content

Fix #3965: Make higher-kinded equality correct and efficient #3978

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Feb 20, 2018

Conversation

odersky
Copy link
Contributor

@odersky odersky commented Feb 9, 2018

#3970 fixed equality for higher-kinded types but came at a steep performance penalty. This PR is trying to avoid the penalty.

Investigation showed that the main culprit was that equality reverted to eq for some ProtoType case classes that inherit from type. Since the multiple copies of these case class instances all had the same hashcode, hashtables saw a significant drop in performance. This shows the danger of redefining equality without also redefining hashcode at the same time. We now dropped the shortcut of having a global equals in Type, preferring to overwrite equals in each subclass where necessary.

To make detection of such issues easier in the future e9388bc adds the numbers of hash-cons table accesses and collisions in the statistics output.

@odersky
Copy link
Contributor Author

odersky commented Feb 9, 2018

Test performance please

@liufengyun
Copy link
Contributor

test performance please

@dottybot
Copy link
Member

dottybot commented Feb 9, 2018

performance test scheduled: 1 job(s) in queue, 1 running.

@dottybot
Copy link
Member

dottybot commented Feb 9, 2018

Performance test finished successfully:

Visit http://dotty-bench.epfl.ch/3978/ to see the changes.

Benchmarks is based on merging with master (5943331)

@odersky
Copy link
Contributor Author

odersky commented Feb 9, 2018

Test performance please

@odersky
Copy link
Contributor Author

odersky commented Feb 9, 2018

test performance please

@dottybot
Copy link
Member

dottybot commented Feb 9, 2018

performance test scheduled: 1 job(s) in queue, 0 running.

@dottybot
Copy link
Member

Performance test finished successfully:

Visit http://dotty-bench.epfl.ch/3978/ to see the changes.

Benchmarks is based on merging with master (5943331)

@odersky
Copy link
Contributor Author

odersky commented Feb 10, 2018

test performance please

@dottybot
Copy link
Member

performance test scheduled: 1 job(s) in queue, 0 running.

@odersky odersky changed the title Trying to isolate where performance loss comes from Fix #3965: Make higher-kinded equality correct and efficient Feb 10, 2018
@dottybot
Copy link
Member

Performance test finished successfully:

Visit http://dotty-bench.epfl.ch/3978/ to see the changes.

Benchmarks is based on merging with master (5943331)

@odersky
Copy link
Contributor Author

odersky commented Feb 10, 2018

test performance please

@dottybot
Copy link
Member

performance test scheduled: 1 job(s) in queue, 0 running.

@dottybot
Copy link
Member

Performance test finished successfully:

Visit http://dotty-bench.epfl.ch/3978/ to see the changes.

Benchmarks is based on merging with master (5943331)

@odersky
Copy link
Contributor Author

odersky commented Feb 10, 2018

It seems that caching MethodTypes and PolyTypes is not worth it, so we'll revert the last commit.

@odersky
Copy link
Contributor Author

odersky commented Feb 10, 2018

test performance please

@dottybot
Copy link
Member

performance test scheduled: 1 job(s) in queue, 0 running.

@liufengyun
Copy link
Contributor

Performance test finished successfully:

Visit http://dotty-bench.epfl.ch/3978/ to see the changes.

Benchmarks is based on merging with master (5943331)

@odersky
Copy link
Contributor Author

odersky commented Feb 11, 2018

OK, I think performance is roughly on par with what it was before and I don't see any immediate ways to improve. So, ready for review.

@odersky odersky requested a review from allanrenucci February 11, 2018 10:36
@allanrenucci allanrenucci requested review from smarter and removed request for allanrenucci February 12, 2018 13:36
@allanrenucci allanrenucci assigned smarter and unassigned allanrenucci Feb 12, 2018
@smarter
Copy link
Member

smarter commented Feb 14, 2018

Reduce number of calls to Type#equals

Is this useful at all given that it just forwards to referential equality?

protected def iso(that: Any, bs: BinderPairs): Boolean = this.equals(that)

/** Equality used for hash-consing; uses `eq` on all recursive invocations,
* except where a BindingType is inloved. The latter demand a deep isomorphism check.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: inloved -> involved

final def equals(that: Any, bs: BinderPairs): Boolean =
(this `eq` that.asInstanceOf[AnyRef]) || this.iso(that, bs)

/** Is `this` isomorphic to that, using comparer `e`?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The e parameter doesn't exist anymore

*/
override def identityHash(bs: Binders) = {
def recur(n: Int, tp: BindingType, rest: Binders): Int =
if (this `eq` tp) finishHash(hashing.mix(hashSeed, n), 1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation is off, should be two characters but is only one

else recur(n + 1, rest.tp, rest.next)
avoidSpecialHashes(
if (bs == null) System.identityHashCode(this)
else recur(1, bs.tp, bs.next))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to start at 1 and not 0?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just don't know how a 0 behaves for hashing.mix

@@ -1394,15 +1394,30 @@ object Types {
*/
def simplified(implicit ctx: Context) = ctx.simplify(this, null)

final def equals(that: Any, bs: BinderPairs): Boolean =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be good to list the laws or invariant relating the various equality related methods we now have, because it's getting rather complicated to follow. For example when iso is overridden, do we need override def equals(that: Any) = equals(that, null) ? I see it in some, but not all, cases.

@@ -2774,7 +2835,7 @@ object Types {
abstract case class MethodType(paramNames: List[TermName])(
paramInfosExp: MethodType => List[Type],
resultTypeExp: MethodType => Type)
extends CachedGroundType with MethodOrPoly with TermLambda with NarrowCached { thisMethodType =>
extends MethodOrPoly with TermLambda with NarrowCached { thisMethodType =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is there no iso implementation for MethodType ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point; we need one since MethodTypes can appear in refinements.

override def equals(that: Any) = that match {
case that: ParamRef => binder.eq(that.binder) && paramNum == that.paramNum
override def iso(that: Any, bs: BinderPairs) = that match {
case that: ParamRef => binder.equalBinder(that.binder, bs) && paramNum == that.paramNum
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably cheaper to do paramNum == that.paramNum first.

if (state.constraint contains tl) {
var paramInfos = tl.paramInfos
if (tl.isInstanceOf[HKLambda]) {
// HKLambdas care hash-consed, need to create an artificial difference by adding
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: care -> are

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would something break if we did tl.clone() instead to get a different instance?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, they would be assumed as equals, so e.g. pendingSubtypes would not distinguish between them.

@@ -1394,15 +1394,30 @@ object Types {
*/
def simplified(implicit ctx: Context) = ctx.simplify(this, null)

final def equals(that: Any, bs: BinderPairs): Boolean =
(this `eq` that.asInstanceOf[AnyRef]) || this.iso(that, bs)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not completely convinced that the default equals should call iso. Why not explicitly use iso in the few cases where we need structural equality (monitored subtyping checks, constraint handing, anything else ?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just a shorthand to avoid the repeated eq tests everywhere it is called.

@smarter smarter assigned odersky and unassigned smarter Feb 16, 2018
Since correct hashing under binders seems to be very expensive (see performance
data for scala#3970), let's try have fewer types that require this.
When compiling dotc/typer/*.scala we observe a reduction of the number
of calls from ~3.7m to ~1.0m.
Now shows also total umber of accesses and hash collisions for each
unique types set.
Dependent types RecType and HKLambda were generative. Any two such
types were considered to be different, even if they had the same structure.
This causes problems for subtyping and constraint solving. For instance,
we cannot detect that a Hk-Lambda has already been added to a constraint,
which can cause a cycle. Also, monitoredIsSubtype would not work if
the compared types are dependent. Test cases are i3695.scala and i3965a.scala.

To fix this, we need to have a notion of hashing and equality which
identifies isomorphic HkLambdas and RecTypes. Since these are very
frequently called operations, a lot of attention to detail is needed
in order not to lose performance.
We need to add structural `equals` since MethodOrPoly's can be part of
RefinedTypes.
@odersky odersky merged commit 9053964 into scala:master Feb 20, 2018
@Blaisorblade Blaisorblade deleted the fix-#3965-v3 branch February 20, 2018 13:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants