Open rakudrama opened 6 years ago
Explicit is
checks and implicit casts are presented in two different ways in IR. is
checks do type profiling and are specialized based on that. Implicit casts don't do type profiling.
0x7fe6e58d71e2 cmpl rax,0x40a
0x7fe6e58d71e7 jz 0x00007fe6e58d71f4
0x7fe6e58d71e9 cmpl rax,0x40c
0x7fe6e58d71ee jnz 0x00007fe6e58d72c2
This chunk of code comes from ClassCheck
that we insert above AssertAssignable
(this is done by FlowGraphTypePropagator::StrengthenAsserts
).
The expectation is that AssertAssignable
would be then eliminated by type propagation - however our type propagation lattice is not smart enough to represent the type we need here.
StrengthenAsserts
work as expected for polymorphic class checks. is
and as
.I made a standalone Dart program that illustrates the issues reported in this bug. Hopefully helpful while analyzing and fixing this further.
Expected output:
Instance of 'B'
Instance of 'F'
passed
Program:
import 'dart:collection';
class A {}
class B {}
abstract class C implements A { B _b; }
class D extends C { }
class E extends C { }
class F extends B { }
class X extends MapBase<A, B> {
// Pick this one:
//B operator [](Object c) {
// if (c is C) {
// return c._b;
// }
// throw new UnimplementedError('no [] C: $c');
//}
// Or this one:
B operator [](covariant C c) {
return c._b;
}
operator []=(Object c, B b) {
if (c is C) {
c._b = b;
return;
}
throw new UnimplementedError('no []= C: $c');
}
// Don't care.
Iterable<A> get keys { return null; }
B remove(Object key) { return null; }
void clear() {}
}
main() {
A a = new A();
B b = new B();
D d = new D();
E e = new E();
F f = new F();
X x = new X();
x[d] = b;
x[e] = f;
print("${x[d]}");
print("${x[e]}");
try {
print("${x[a]}");
} catch (e, s) {
print("passed");
}
}
As observed above, under JIT, X vs. Y both do the check-class, but Y does not stop there:
2: B3[target]:0 ParallelMove rcx <- S+2
4: CheckClass:12(v3 Cids[1: D etc. cid 1026-1027])
6: v18 <- LoadField(v3, 8 {_b@17180520} [nullable dynamic]) T{Type: class 'B'?}
7: ParallelMove rax <- rax
8: Return:26(v18)
2: B1[target]:0 ParallelMove rbx <- S+2
4: CheckClass:12(v3 Cids[1: D etc. cid 1026-1027])
5: ParallelMove rax <- rbx, rdx <- C, rcx <- C
6: AssertAssignable:12(v3, Type: class 'C', 'c', instantiator_type_args(v0), function_type_args(v0)) T{Type: class 'C'?}
7: ParallelMove rcx <- S+2
8: v4 <- LoadField(v3 T{Type: class 'C'?}, 8 {_b@17180520} [nullable dynamic]) T{Type: class 'B'?}
9: ParallelMove rax <- rax
10: Return:18(v4)
With a better lattice, type for v3 is improved, and the code for Y becomes the same as for X (generated CheckClass removes the AssetAssignable):
2: B1[target]:0 ParallelMove rcx <- S+2
4: CheckClass:12(v3 Cids[1: D etc. cid 1026-1027])
6: v4 <- LoadField(v3 T{Type: class 'C'}, 8 {_b@17180520} [nullable dynamic]) T{Type: class 'B'?}
7: ParallelMove rax <- rax
8: Return:18(v4)
I made two improvements:
type propagation now uses class hierarchy during union operation, which provides a better type lattice, e.g. taking the union of B,C now yields A rather than resorting to "dynamic", which immediately benefits propagation over phis, but also helps the next improvement A / \ B C
check classes with cid ranges now take union too rather than just dealing with singletons
Overall this yields a 50-80% improvement on the co-variant benchmarks, bringing co-variant on par with explicit is-check (and both improved).
Slava, are your points 2 and 3 still worth pursuing?
armv8 improvements on benchmark (note that both improve, and both are "on par" afterwards):
TypeChecks.IsCheck (Odroid-C2) 22.61%
TypeChecks.CovariantCheck (Odroid-C2) 88.61%
@aartbik I think it might be worth investigating - but probably in the larger context of unifying how assert-assignable-s and is checks work in general. Right now I'd put this on back burner unless we discover more cases where it matters.
(I am considering to keep this as a starter project for the next member of the VM team)
Over to Slava in case he wants to re-assign this as starter.
afaca9f7cfdf0a49bcba8985afec59e97e3019ba speeds up a hot map by using a field hidden behind a
Map
interface.The implementation of the map indexer is:
I'd rather use the simpler formulation:
However, the performance of the 'better' covariant form is bad enough to negate the benefit of using a field.
ClassHierarchyNode operator [](Object cls)
checks for the two ClassID for the concrete classes that have the field, and then loads field:ClassHierarchyNode operator [](covariant ClassHierarchyNodesMapKey cls)
also tests for the two ClassIDs, but then does a lot more work, including calling Subtype1TestCache (according to profiles). All this work is unnecessary, since both tested ClassIDs implement the declared covariant type./cc @mraleph @aartbik