-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Conversation
Test performance please |
test performance please |
performance test scheduled: 1 job(s) in queue, 1 running. |
Performance test finished successfully: Visit http://dotty-bench.epfl.ch/3978/ to see the changes. Benchmarks is based on merging with master (5943331) |
Test performance please |
test performance please |
performance test scheduled: 1 job(s) in queue, 0 running. |
Performance test finished successfully: Visit http://dotty-bench.epfl.ch/3978/ to see the changes. Benchmarks is based on merging with master (5943331) |
5f22db6
to
03377c7
Compare
test performance please |
performance test scheduled: 1 job(s) in queue, 0 running. |
Performance test finished successfully: Visit http://dotty-bench.epfl.ch/3978/ to see the changes. Benchmarks is based on merging with master (5943331) |
test performance please |
performance test scheduled: 1 job(s) in queue, 0 running. |
Performance test finished successfully: Visit http://dotty-bench.epfl.ch/3978/ to see the changes. Benchmarks is based on merging with master (5943331) |
It seems that caching MethodTypes and PolyTypes is not worth it, so we'll revert the last commit. |
test performance please |
performance test scheduled: 1 job(s) in queue, 0 running. |
Performance test finished successfully: Visit http://dotty-bench.epfl.ch/3978/ to see the changes. Benchmarks is based on merging with master (5943331) |
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. |
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. |
There was a problem hiding this comment.
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`? |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 = |
There was a problem hiding this comment.
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 => |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: care -> are
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 ?)
There was a problem hiding this comment.
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.
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.
e96b8ca
to
7cd930b
Compare
We need to add structural `equals` since MethodOrPoly's can be part of RefinedTypes.
#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 globalequals
in Type, preferring to overwriteequals
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.