summaryrefslogtreecommitdiff
path: root/ASimpleReply
diff options
context:
space:
mode:
Diffstat (limited to 'ASimpleReply')
-rw-r--r--ASimpleReply/ASimpleReply2.md123
-rw-r--r--ASimpleReply/ASimpleReply3.md127
-rw-r--r--ASimpleReply/ASimpleReplyChain.md16
-rw-r--r--ASimpleReply/DadalReply2.md58
-rw-r--r--ASimpleReply/DadalReply3.md25
5 files changed, 338 insertions, 11 deletions
diff --git a/ASimpleReply/ASimpleReply2.md b/ASimpleReply/ASimpleReply2.md
index 62f1a5ba..45017b61 100644
--- a/ASimpleReply/ASimpleReply2.md
+++ b/ASimpleReply/ASimpleReply2.md
@@ -7,23 +7,128 @@ The whole reply chain:
https://github.com/SeaOfNodes/Simple/blob/main/ASimpleReply/ASimpleReplyChain.md
I find long, involved, multi-part discussions difficult to follow because
-basically we're really having 8 unrelated technical discussions. I broke up my
+basically we're really having 6 unrelated technical discussions. I broke up my
replies so each part is easier to consider on its own.
-# Reply - Using t effect chains based on equivalence classes
+## Reply - "Guard tests for correctness"
-# Reply - "Guard tests for correctness"
+Score one for static typing!
-# Reply - "Graphs are nice for intuition but they don't scale well."
+Here perhaps there is another win, but not really SoN related as least not
+directly. Every "guardable" aspect in Java - except range checks - falls
+neatly into a type *lattice*. This includes all kinds of subtype checks
+(including instanceof, checkcast, user-written, arraystore, etc), null checks,
+and constant bounds. A type *lattice* allows Sparse Conditional Constant
+Propagation on all these types, which makes for a linear time, super cheap
+analysis which wipes out that 90% of checks.
-# Reply - "Use a worklist!"
+
+## Reply - "Graphs are nice for intuition but they don't scale well."
+
+Could be that graph viz tech has improved enough - although I tried hard with
+GraphViz on Simple, and other than some pretty demos its not cut out for
+serious debugging work. Meanwhile, ASCII dumps in C2 run into the 100
+thousands of lines, are instantly quick, land in my editor buffer where they
+are ready to be searched, grep'ed, folded, stapled and mutalilated. Of course
+they appear as a full CFG, with loop structure, typed, backlinks to source code,
+etc. So maybe the answer is: "if you work on your debugging tools long enough,
+they work for you".
+
+
+## Reply - "Use a worklist!"
I think we agree, at least in theory, here.
-# Reply - Late arriving control flow from expanding small "diamonds"
-# Reply - "Data? Perf discussions with facts? Apples to apples comparisons? No?"
+## Reply - "Data? Perf discussions with facts? Apples to apples comparisons? No?"
+
+Yeah, apples & oranges. The obvious observation to make about any comparison
+is that many benchmarkers have a desired outcome in mind... and will pick their
+comparison according. Turboshaft was compared with the V8 variant SoN.
+I'll betcha when doing comparable work C2 is *very* competitive with
+Turboshaft, *especially* on a cache miss basis. This was *the number one
+timing thing* I tracked in during C2's development. "Back in the day" nearly
+all small to medium methods (plus inlines) fit in those processors *L1* cache.
+
+
+## Reply - Late arriving control flow from expanding small "diamonds"
+## Reply - Hard to introduce new control flow
+
+Full optimization of "diamond like things", without being a diamond and
+remaining "pure" (no CFG at least), is done generally via the type lattice on
+higher level common ops. They get lowered after there's no hope of other
+optimization (provable no-progress by the lattice) - which is generally during
+instruction scheduling for C2.
+
+If that's not enough and if you want to introduce control flow - its the same
+problem in a CFG as in SoN (which *has* a CFG embedded). So whatever solution
+you have for your CFG approach... SoN can use directly.
+
+
+## Reply - It's easy in Simple
+
+Yeah, yeah, more opcodes and complexier semantics. C2 also has a zillion
+opcodes, and deopts and re-opts, handles large graphs, multi IR levels, and
+sings and dances.... and Simple has all the right compiler pieces to slot into
+a runtime as complex as a JVM. I don't buy the "its easy because its Simple",
+its easy because... its *simple*. Simplicity is a virtue in its own right, and
+many hard looking things become, well, *simple* when you start from the right
+base.
+
+
+## Reply - Using multiple effect chains based on equivalence classes
+
+Well, this is the biggest failure I see in the V8 SoN effort and deserves the
+most inspection... and has the most room for valid design tradeoffs. So I'm
+saying a little more here, to match your larger writings, but also because
+there's just more stuff to chew on.
+
+
+"It requires typing the whole graph throughout the whole pipeline" - yes. This
+is basically free in Simple/SoN, or at least costs some modest count of cpu
+cycles (and generally no cache misses). Then it sits as a pointer in an IR
+node. There's no bulk copy/bulk read going on, so... it costs nothing to sit
+there. But then, when you need some type info - lo! There it is! C2
+maintains type info end-to-end, because its free to do so and because its
+useful.
+
+"Equivalence classes are much weaker for arrays" - sure. And basically make no
+differece to 99% of all Java programs. 1-d arrays vectorize just fine; stencil
+calculations also fine. Register blocking and tiling? First Java needs to
+change how multi-D arrays are laid out then we can worry about loop carried
+aliasing. Basically, Java was never intended to replace Fortran, although in
+the 1-D space it can hold its own.
+
+"Every call to user-defined functions, as well as most builtin calls have
+arbitrary side effects" - sure same as Java. Doesn't really matter in the
+grand scheme of things. If you can inline, the problem goes away. If not,
+well *There's Yer Problem*. The lack of call aliasing info means you didn't
+optimize some things around the call - all dwarfed by the call overhead itself.
+
+"Memory optimizations also cannot typically float past deoptimization state" -
+plenty of those in Java too. C2 also does not sink stores *below* a deopt
+state. At least for Java, we can float loads *up* past a deopt state (and do
+so plenty).
+
+Also, call inline points *completely* disappear in C2. There is no
+optimization barrier after inlining, even for memory effects. This is
+different from *deoptimization* in this regard.
+
+# "Because of polymorphism.... but rather union types".
+
+Here I honestly don't know. I can fit some kinds of union types in a type
+*lattice*, and then we're back to "basically free types". On the other hand,
+the usefulness of the equivalence class model is based on "since you *cannot*
+alias, the other aliases are not even representable".
+
+This gets into a weak argument from me about how after specialization guards,
+we're back to equivalence classes... but I honestly dunno. Not tried it in JS
+at least (C2 certainly does this, but its not so common as JS).
+Very curious though...
+
+Curious enough for Google to fund a reprise with me lending a hand?
+
+:-)
-# Reply - Hard to introduce new control flow
+Cliff
-# Reply - It's easy in Simple
diff --git a/ASimpleReply/ASimpleReply3.md b/ASimpleReply/ASimpleReply3.md
new file mode 100644
index 00000000..395f4b77
--- /dev/null
+++ b/ASimpleReply/ASimpleReply3.md
@@ -0,0 +1,127 @@
+# A Simple Reply (3)
+
+The whole reply chain:
+
+https://github.com/SeaOfNodes/Simple/blob/main/ASimpleReply/ASimpleReplyChain.md
+
+
+## Unzipping
+
+Starting from Dadal's code, which has inlined some specialization checks:
+
+```
+foo (o) {
+ let x;
+ if (o.map == Map1) {
+ x = RawLoad(o, offset=8);
+ } else if (o.map == Map2) {
+ x = RawLoad(o, offset=16);
+ } else if (o.map == Map3) {
+ x = RawLoad(o, offset=12);
+ } else throw deopt; // Cliff added
+ .... // Cliff adds: call this code "S1"
+ let y;
+ if (o.map == Map1) {
+ y = RawLoad(o, offset=0);
+ } else if (o.map == Map2) {
+ y = RawLoad(o, offset=20);
+ } else if (o.map == Map3) {
+ y = RawLoad(o, offset=4);
+ } else throw depopt; // Cliff added
+}
+```
+
+I added the "throw deopt", which I assume Dadal implies when `x` and `y` are
+none of `Map1, Map2, Map3`.
+
+Dadal then adds:
+
+> "Here, it's not like any of the type-checks when loading `y` could be
+> removed. Well, in some cases, we do things like double-diamond elimination to
+> remove some checks, but often it's not possible."
+
+C2 sees this kind of pattern fairly often as well; generally replacing `o.map
+== Map1` with `o instanceof Map1`; not that it matters. C2 uses a "unzipping"
+xform for repeated checks; I've talked about it publically numerous times.
+Here C2 will look at the `S1` code above, and based on a heuristic (mostly
+size) it will clone `S1` to remove the excess checks:
+
+```
+foo (o) {
+ let x,y;
+ if (o.map == Map1) {
+ x = RawLoad(o, offset=8);
+ S1;
+ y = RawLoad(o, offset=0);
+
+ } else if (o.map == Map2) {
+ x = RawLoad(o, offset=16);
+ S1;
+ y = RawLoad(o, offset=20);
+
+ } else if (o.map == Map3) {
+ x = RawLoad(o, offset=12);
+ S1;
+ y = RawLoad(o, offset=4);
+
+ } else throw deopt; // Cliff added
+}
+```
+
+Now you get 3 chunks of specialized code where in each case you know the type
+of `o.map` throughout. This process can be repeated at all kinds of levels,
+and assuming the strong knowledge of `o.map` allows more optimizations, can pay
+for itself despite cloning some amount of `S1`. Its certainly common for C2 to
+clone long runs of repeated checks such that in the end there's a fast-path
+where all checks pass "the good way", and bail-out points all long that go to a
+correct but slow path.
+
+
+## Common Case vs Deopt
+
+```
+foo (o) {
+ let x = o.x;
+ bar(); // Might change the "shape" of `o`
+ let y = o.y;
+}
+```
+
+Dadal:
+
+> "If `bar` isn't inlined, then we have to assume that `bar` could change
+> the shape of `o`, and thus a type-check is needed before loading `y`."
+
+In a similar situation C2 will do the same. Of course, for sufficiently hot
+`foo` and modest `bar`, inlining is the answer. This makes me wonder if C2's
+inlininhg budget is substantially higher than V8's.
+
+Dadal:
+> "Oh, and `bar` could be changing the shape of `o` only some times"
+
+In any of these "sometimes Bad Things Happens" situations, C2's go-to answer is
+to register a dependency and deopt if the Bad Thing happens. The set of Bad
+Things that happen between JS and the JVM are different, but it seems to me the
+general theory still holds. Similar (not exact) situation:
+
+```java
+foo( Object o ) {
+ var x = ((Cat)o).x; // Dependency that Cat has no subclasses
+ x.bar(); // Always a `Cat` so call Cat.bar
+}
+```
+
+If later `Cat` is extended with `Siamese` which implements an alternative
+`bar`, this code is deopt'd. The existing calls to `foo` can finish - they are
+correct because they got called with a `Cat` and cannot change that - but no
+new calls are allowed; they will use an alternative more conservative execution
+until a fresh JITd code for `foo` can be made.
+
+
+## Summary
+
+Yeah, the problems between JS and the JVM are different... but not hugely so.
+And the problems shown here are *utterly* unrelated to the Sea-of-Nodes.
+I'd do this stuff in a normal CFG framework just the same.
+
+Cliff
diff --git a/ASimpleReply/ASimpleReplyChain.md b/ASimpleReply/ASimpleReplyChain.md
index 2d53a8ee..eb0bc80e 100644
--- a/ASimpleReply/ASimpleReplyChain.md
+++ b/ASimpleReply/ASimpleReplyChain.md
@@ -17,10 +17,22 @@ https://gist.github.com/DadaIsCrazy/8ce07d409f8f74bad7b36e74b77a3129
### Cliff's Reply(2):
-(first, this reply-chain to gather all the parts)
-
https://github.com/SeaOfNodes/Simple/blob/main/ASimpleReply/ASimpleReply2.md
+### Dadal's Reply(2):
+
+https://studio.youtube.com/channel/UCbutMk6cKrH8q4sJyQHGPVQ/comments/inbox?filter=%5B%5D
+https://github.com/SeaOfNodes/Simple/blob/main/ASimpleReply/DadalReply2.md
+
+
+### Cliff's Reply(3)
+
+https://github.com/SeaOfNodes/Simple/blob/main/ASimpleReply/ASimpleReply3.md
+
+### Dadal's Reply(3)
+
+https://github.com/SeaOfNodes/Simple/blob/main/ASimpleReply/DadalReply3.md
+
[room for more conversations]
diff --git a/ASimpleReply/DadalReply2.md b/ASimpleReply/DadalReply2.md
new file mode 100644
index 00000000..bb30e155
--- /dev/null
+++ b/ASimpleReply/DadalReply2.md
@@ -0,0 +1,58 @@
+
+This is Dadal's reply from U-tube, recreated here (permission pending) so its
+both easier to find and not lost if U-tube changes their mind.
+
+The original link I copied from:
+
+https://studio.youtube.com/channel/UCbutMk6cKrH8q4sJyQHGPVQ/comments/inbox?filter=%5B%5D
+
+The whole replace chain:
+
+https://github.com/SeaOfNodes/Simple/blob/main/ASimpleReply/ASimpleReplyChain.md
+
+
+
+Type checks: to give an example:
+
+```
+foo(o) {
+ let x = o.x;
+ ....
+ let y = o.y;
+}
+```
+
+Assuming polymorphic feedback (which is very frequent in JS), this will become something like (using JS-like syntax, but this happening inside the compiler of course)
+```
+foo (o) {
+ let x;
+ if (o.map == Map1) {
+ x = RawLoad(o, offset=8);
+ } else if (o.map == Map2) {
+ x = RawLoad(o, offset=16);
+ } else if (o.map == Map3) {
+ x = RawLoad(o, offset=12);
+ }
+ ....
+ let y;
+ if (o.map == Map1) {
+ y = RawLoad(o, offset=0);
+ } else if (o.map == Map2) {
+ y = RawLoad(o, offset=20);
+ } else if (o.map == Map3) {
+ y = RawLoad(o, offset=4);
+ }
+}
+```
+
+Here, it's not like any of the type-checks when loading `y` could be removed. Well, in some cases, we do things like double-diamond elimination to remove some checks, but often it's not possible.
+
+For monomorphic cases, subsequent checks will be removed when possible, but objects can change shapes in JavaScript. This means that if you have something like
+```
+foo (o) {
+ let x = o.x;
+ bar();
+ let y = o.y;
+}
+```
+If `bar` isn't inlined, then we have to assume that `bar` could change the shape of `o`, and thus a type-check is needed before loading `y`. Oh, and `bar` could be changing the shape of `o` only some times, so `o.x` could have monomorphic feedback while `o.y` could have polymorphic feedback. Or the other way around :)
diff --git a/ASimpleReply/DadalReply3.md b/ASimpleReply/DadalReply3.md
new file mode 100644
index 00000000..e5e89aa4
--- /dev/null
+++ b/ASimpleReply/DadalReply3.md
@@ -0,0 +1,25 @@
+
+This is Dadal's reply from U-tube, recreated here (permission pending) so its
+both easier to find and not lost if U-tube changes their mind.
+
+The original link I copied from:
+
+https://www.youtube.com/watch?v=iNWAgSAEPAg&lc=Ugwn_YpruALAauwRLqt4AaABAg.AGbLSQ5tlmFAGcQm0uZyZO
+
+The whole replace chain:
+
+https://github.com/SeaOfNodes/Simple/blob/main/ASimpleReply/ASimpleReplyChain.md
+
+
+
+On medium sized functions, everything tends to be inlined in V8, but on larger
+functions it tends to be more complicated. We've tried before to increase the
+inclining budget, but this means reducing compile time, which means delaying
+the time until the optimized code starts being executed, which in turns
+regresses whatever benchmarks we're using to measure how fast V8 is. One
+difference with Java here might be 1- how slow unoptimized JS code is (after
+all, V8 bytecodes are ultra-generic, assuming any types for inputs, and having
+to check a ton of various cases) and 2- how fast-running JS applications are
+compared to Java (not in the sense that "JS is faster" but in the sense that
+"JS applications probably stop running at least one order of magnitude earlier
+than Java ones")