[CALCITE-3786] Add Digest interface to enable efficient hashCode(equa…#2016
Conversation
| return Objects.hash(collect(rel, contents)); | ||
| } | ||
|
|
||
| private static Object[] collect(RelNode rel, List<Pair<String, Object>> contents) { |
There was a problem hiding this comment.
This seems to be resource-consuming. Should the result is not cached in the constructor?
There was a problem hiding this comment.
This is used to resolve the hash code conflict, which expects to be rare case, the cache would not bring much.
There was a problem hiding this comment.
equals is always executed (== in case hashCode matches), and equals would be called multiple times in case of a hash collision, so it is worth making both equals and hashCode fast.
There was a problem hiding this comment.
I run all the cases and there are only 8 tests that have hash conflict, i would try to promote.
There was a problem hiding this comment.
Finally i decide to keep it as is, because the conflict is very few, i also remove the row type when computing hashcode, because row type diff for equiv node is also rare case(usually because of nullability changes).
8284356 to
a51dc07
Compare
02eebf9 to
545743d
Compare
laurentgo
left a comment
There was a problem hiding this comment.
I have to say that I don't see how this pull request makes hash code more efficient (precomputation of hashcode?). For sure it looks to increase memory usage and also remove lots of flexibility for Calcite users extending on RelNode and RexnNode...
| } | ||
|
|
||
| @Override public int compareTo(Digest other) { | ||
| return this.rel.getId() - other.rel.getId(); |
There was a problem hiding this comment.
This breaks the contract that if 2 objects are equal, then the method should return 0.
There was a problem hiding this comment.
This is only for debugging, strictly to say, i have no idea how to compare 2 nodes that are semantically equivalent (#equals) while have different members.
There was a problem hiding this comment.
why is Comparable needed for debugging? Right now, I don't see how debuggability can justify breaking the contract
There was a problem hiding this comment.
Well, i have fixed the consistency of #equals.
There was a problem hiding this comment.
Maybe it would have been simpler not to implement Comparable? You didn't clarify why this is needed for debugging purposes...
There was a problem hiding this comment.
That was proposed initially by Julian in the issue, i think it is needed when we want to sort on that.
| private boolean deepEquals(Digest other) { | ||
| Object[] thisItems = collect(this.rel, this.items, true); | ||
| Object[] thatItems = collect(other.rel, other.items, true); | ||
| if (thisItems.length != thatItems.length) { |
There was a problem hiding this comment.
The whole check + loop could be replaced with Arrays.equals(Object[], Object[])
There was a problem hiding this comment.
Sorry, i would choose current code because it's simpler.
There was a problem hiding this comment.
are you telling that 9 lines are simpler that one line of code which is return Arrays.equals(thisItems, thatItems)?
There was a problem hiding this comment.
Did you see the code in Arrays.equals(thisItems, thatItems) ? Here the thisItems and thatItems are both never null.
There was a problem hiding this comment.
Considering null checks are often optimized out by the JVM, I'm not sure that the possible extra optimization is worth the extra code you wrote...
There was a problem hiding this comment.
It is often but not always, if you are not sure also, let's keep it as it is, which is also straight-forward code.
There was a problem hiding this comment.
Sorry, I am at a loss for arguments. The original reply was that the code was simpler than Arrays.equals(Object[], Object[]), a standard JDK method, whereas in fact, it is basically the same code, except for the check for null, which is often suppressed by the JVM compiler (cf https://shipilev.net/jvm/anatomy-quarks/25-implicit-null-checks/) and even so has minimal impact compared to the two calls to the #collect method where a list is created and converted later into an array...
There was a problem hiding this comment.
The test here #2034 shows that there is negligible performance affect with ArrayList.toArray, so still, i would not follow you.
| return false; | ||
| } | ||
| for (int i = 0; i < thisItems.length; i++) { | ||
| if (!Objects.equals(thisItems[i], thatItems[i])) { |
There was a problem hiding this comment.
should Arrays.deepEquals be used if thisItems[i] is an array?
There was a problem hiding this comment.
I checked all the rels, they all have no array attributes in the digest, even it has, the #equals would output false which does not affect the semantics. So i choose current impl.
There was a problem hiding this comment.
for now? what about external rels?
There was a problem hiding this comment.
I said outputs false does not affect the semantics, then what's the problem there, i do not want to make the logic a mess just for a non-sure assumption.
If you have strong opinion, please give specific cases, only assumption makes no sense to me.
There was a problem hiding this comment.
Yes, there has some "ifs" there. There are 2 assumptions here as well:
- there's no rel node using an array as a digest field, which is true for Calcite code, and probably true for code using the library although it's harder to verify
- even so, return false when 2 array contents are identical has no impact on the semantic.
For the first point, maybe a more opinionated choice could be to disallow arrays as digest item?
For the second point, I'm not sure that the assumption holds: if planner cannot identify that 2 rel nodes are identical (and so collapse them), it might cause some kind of infinite loop/plan explosion where the same node is created over and over.
There was a problem hiding this comment.
- I don't think we should have any limitation on what should be a digest item just because of the digest code implementation
- Did we encounter any case that has infinite loop just because of an array item there ? I need to see the case to make decision, not an assumption. I prefer to fix that when we really encounter that, i think such case would be very rare.
| private static String simpleRelDigest(RelNode rel) { | ||
| StringBuilder digest = new StringBuilder(rel.getRelTypeName()); | ||
| digest.append('#'); | ||
| if (rel instanceof RelSubset) { |
There was a problem hiding this comment.
Why not let the rel node itself handle how to create the digest vs having a non-extendable/overridable method using different behaviors based on the type of rel to do it? This looks like an anti design pattern.
There was a problem hiding this comment.
For current impl, the digest is handled by each rel node. Split the code everywhere is hard to maintain, let the Digest take care how the digest should be generated but the invoker is still each node.
There was a problem hiding this comment.
Considering that method, the digest takes care how the digest should be generated for nodes it already knows about. Based on the current approach, if I create a new RelNode subclass in my project which has a very natural/efficient way of creating a digest, I would have no choice than to fork the Calcite project to modify that method.
I would favor an approach which would let HepRelVertex itself decides what to use for its digest instead of hardcoding in this method that the current rel should be chosen.
There was a problem hiding this comment.
RelNode subclass in my project which has a very natural/efficient way of creating a digest, I would have no choice than to fork the Calcite project to modify that method
Did you notice that the #computeDigest and #explainTerms are both API for overriding the digest ?
HepRelVertex is not a public API, i don't think there is necessity to customize its digest, if you have a sub-class, override the #computeDigest is till feasible.
There was a problem hiding this comment.
why not using the #computeDigest with the vertex id then?
Also it is not possible to "substract" to the default digest. One can only add. The patch you proposed has some behavior which is not possible to change
There was a problem hiding this comment.
The current patch also supports pure string digest, which is same as before, if you think it is impossible, please give an example.
The digest of HepRelVertex and RelSubSet are the same as before, what do you mean by using the #computeDigest with the vertex id then ?
There was a problem hiding this comment.
I guess I missed some update to your change (or missed the second constructor), but it looks like the choice is either:
- provide rel + a list of extra items, and the logic to create the hashcode + digest is totally controlled by the digest class
- provide rel + digest string to skip the whole hash computation logic
I guess nothing in-between...
The digest of HepRelVertex and RelSubSet are the same as before, what do you mean by using the #computeDigest with the vertex id then ?
It's back to my comment about having the logic hardcoded into a single private static method which goes against OOP practices whereas instructingHelpRexVertexandRelSubSetinstructing digest that the only field to consider for digest should beid
There was a problem hiding this comment.
Use the digest string to compute hashcode works well then the string is short, you can use the benchmark #2034 to see the diff. The performance diff is negligible.
Compare to the old code, this patch reduce the memory usage, especially for the |
09bd694 to
ba5d947
Compare
…ls) for RexNode and RelNode * Add class Digest used to identify the node; * There is a pre-computed hashcode to speedup #hashCode and #equals; * Change RexCall to use object#equals instead of pure string digest comparison; * We only support RexInputRef normalization which is the most common case; * Remove RexNode#toStringRaw because it makes the thing complicated, RexNode can always be normalized(default true).
|
Regarding my concern about memory usage, I was talking about |
I have gave the explanation in the JIRA issue about why i keep redundant references of the RelNode items, that still makes sense to me.
It is possible, if you think there is also some thing can do that can promote the |
What do you mean by extra fields ? If possible, can you give a POC code there, the discussion with all kinds of assumptions make me burden out. And if possible, use the code here #2034 to illustrate that you are right. |
| @Override protected Digest computeDigest() { | ||
| return Digest.create(this, getRelTypeName() + '#' + getCurrentRel().getId()); | ||
| } | ||
|
|
There was a problem hiding this comment.
Why do you use getRelTypeName() rather than "HepRelVertex"?
There was a problem hiding this comment.
To unify the logic, we can switch to "HepRelVertex", that's a preference.
| + "PLAN=EnumerableCalc(expr#0..3=[{inputs}], empid=[$t1], name=[$t3])\n" | ||
| + " EnumerableCorrelate(correlation=[$cor1], joinType=[inner], requiredColumns=[{0}])\n" | ||
| + " EnumerableAggregate(group=[{0}])\n" | ||
| + " EnumerableTableScan(table=[[s, depts]])\n" |
There was a problem hiding this comment.
It's weird to add PALN= just for the two cases. It's better to have the same format for all the cases.
There was a problem hiding this comment.
I think we could, thanks ~
| public static Digest initial(RelNode rel) { | ||
| return new Digest(rel, simpleRelDigest(rel)); | ||
| } | ||
| } |
There was a problem hiding this comment.
What's the difference between Digest#create and Digest#initial ? Their results seem the same.
There was a problem hiding this comment.
To distinguish that the digest is just as a initial which should only be used once in the constructor.
There was a problem hiding this comment.
It's really hard to distinguish these two methods. Is it possible to remove Digest#create which seems useless?
There was a problem hiding this comment.
I can remove the #initial and we use the #create uniformly.
| sb.append('.'); | ||
| sb.append(trait.toString()); | ||
| } | ||
|
|
There was a problem hiding this comment.
As you said "List<Pair<String, Object>> items are the properties, e.g. the inputs, the type, the traits and so on", why should we still have to append trait?
There was a problem hiding this comment.
The properties do not include traits, the Digest does.
There was a problem hiding this comment.
Please correct the comment.
| @Override public int hashCode() { | ||
| return Objects.hash(digest, ordinal, distinct); | ||
| return Objects.hash(super.hashCode(), ordinal, distinct, ignoreNulls); | ||
| } |
…ls) for RexNode and RelNode
promoted as reused objects later