[HN Gopher] V8 Sparkplug - A non-optimizing JavaScript compiler
___________________________________________________________________
V8 Sparkplug - A non-optimizing JavaScript compiler
Author : tosh
Score : 209 points
Date : 2021-05-27 16:19 UTC (6 hours ago)
(HTM) web link (v8.dev)
(TXT) w3m dump (v8.dev)
| SigmundA wrote:
| "The second trick is that Sparkplug doesn't generate any
| intermediate representation (IR) like most compilers do. Instead,
| Sparkplug compiles directly to machine code in a single linear
| pass over the bytecode, emitting code that matches the execution
| of that bytecode."
|
| Isn't bytecode an intermediate representation?
|
| https://en.wikipedia.org/wiki/Intermediate_representation
|
| Am I just being pedantic?
| titzer wrote:
| Even if you do consider bytecode to be an intermediate
| representation, the statement "Sparkplug doesn't _generate_ any
| intermediate representation (IR) " is true. It goes from one
| existing representation to machine code without creating a new
| one in the process.
| SigmundA wrote:
| Right the _javascript_ compiler is Ignition + Sparkplug with
| the bytecode as IR. Sparkplug doesn 't parse or optimize, it
| relies on Ignition for that, so its not really a full
| compiler, its just the backend of a compiler.
|
| https://en.wikipedia.org/wiki/Compiler#Back_end
| masklinn wrote:
| Well except ignition is first and foremost an interpreter,
| so sparkplug is a compiler backend welded onto an
| interpreter frontend.
| SigmundA wrote:
| Ignition optionally interprets but always parses
| javascript, optimizes and compiles bytecode. So IMO
| Ignition is primarily a compiler frontend that has an
| optional interpreter back end built in.
|
| I could see them splitting Ignition into a bytecode
| compiler and separate interpreter and give them separate
| names.
| meheleventyone wrote:
| I guess the difference is that the bytecode is executed by the
| interpreter and an intermediate representation typically isn't
| executable?
| SigmundA wrote:
| Well some processors can execute Java byte code directly, and
| most of the time its jitted, however originally it was just
| interpreted.
| Twirrim wrote:
| Does anyone actually do that other than Transmeta back at
| the turn of the millenium?
| SigmundA wrote:
| Not really anymore but many phones had Jazelle capable
| processors in the mid 2000's:
|
| https://en.wikipedia.org/wiki/Java_processor
| chrisseaton wrote:
| In the compiler community, people usually use IR to mean
| something lower than bytecode, usually used as part of the
| compilation process itself rather than used for storage or when
| not actively compiling. IR is usually not intended for direct
| execution.
|
| (LLVM IR is an unusual exception therefore. Please don't 'well
| actually' me I'm trying to give a useful practical
| explanation.)
| Ar-Curunir wrote:
| I thought IR was just the internal processing format used by
| a compiler, eg: MIR or LLVM IR, and so is at a level usually
| "higher" on the abstraction stack than assembly/bytecode,
| which can be seen as the primary outputs of a compiler
| MikeHolman wrote:
| IR is usually represented as a sort of linked list of
| instructions where you can easily move around instructions,
| add/remove them, and replace higher level instructions with
| lower level instructions as you go through compiler passes.
| At the end, the IR is a linked list of machine
| instructions, which gets written out to a code buffer as
| the last step.
|
| Bytecode is usually a buffer of high level instructions
| that is compact and fast to read (for the machine). Good
| for interpreting and holding the semantics of a program in
| a format that is more efficient than the source code, but
| it is usually not a good format for running compiler
| passes.
| chrisseaton wrote:
| We'd say it is 'lower' not 'higher' as it usually
| decomposes single higher-level compound operations into
| multiple simpler operations, and it makes things that were
| implicit explicit so they can be reasoned about. Usually
| more verbose therefore. Converting into IR is often called
| 'lowering'.
| pizlonator wrote:
| For Sparkplug, bytecode is the input representation. But you're
| right that bytecode is an intermediate representation of V8. So
| to me, whether bytecode is an IR or not depends on whether
| you're talking about V8 as a whole or Sparkplug in particular.
|
| To me, an intermediate representation is what the compiler
| translates the input to other than machine code. Sparkplug goes
| straight from its input (bytecode) to machine code.
|
| So you're not being too pedantic. It's just that V8 has two
| compilers now, one that has an IR of its own (turbofan), and
| one that doesn't (sparkplug). Lack of IR is one of the things
| (maybe the most important thing) that makes Sparkplug special.
| If you define IR in a way that means that bytecode is an IR
| then I don't know how to easily articulate what it is that
| makes Sparkplug special.
| johntb86 wrote:
| It seems like Sparkplug per se isn't generating the IR. Instead
| it's the interpreter doing that.
| IshKebab wrote:
| Yes but Sparkplug doesn't generate any bytecode.
| zachrip wrote:
| Am I the only one struggling to read the excalidraw'ings?
| pistachiopro wrote:
| Seems like the page background and text invert for OS dark
| mode, but the drawings don't.
| fwip wrote:
| Thanks, changing my OS to light mode made the diagrams
| readable.
| Leszek wrote:
| Yeah, sorry, dark mode support needed CSS changes, so if you
| have cached CSS then it looks wrong. A hard refresh should fix
| it.
| IshKebab wrote:
| This sounds like it should be so fast I'm surprised they even
| kept the interpreter. Is there some delay setting JIT up or
| something?
| titzer wrote:
| Memory consumption and cold code. Machine code is larger, and
| it takes time to generate that only pays off if you execute the
| code many times.
| inglor wrote:
| If anyone is interested here is the SparkPlug design document I
| have bookmarked from a few months back:
| https://docs.google.com/document/d/13c-xXmFOMcpUQNqo66XWQt3u...
| inglor wrote:
| One interesting point from there (regarding their old
| FullCodeGen original JIT compiler):
|
| > Isn't this just FullCodeGen?
|
| > Kind of! It's FullCodeGen for modern V8, and that's not such
| a bad thing. This brings the performance benefits of FCG back
| to V8, but
| tamlin wrote:
| Very cool, that's a decent performance bump.
|
| It'd be nice to have some simple benchmarks that directly compare
| ignition, sparkplug and turbofan (and full-codegen too). And also
| time spent compiling.
|
| It looks somewhat similar to context threading as I remember it:
|
| http://www.cs.toronto.edu/~matz/pubs/demkea_context.pdf
|
| but it's been a while...
|
| Ha-ha, the stacks grow downwards button is priceless :-D
| kragen wrote:
| It's disappointing that they don't include any performance
| results from Sparkplug itself, only from V8 using Sparkplug some
| unspecified and unknowable percentage of the time. While it's
| true that Octane doesn't provide optimal performance targets for
| engineering V8, it _does_ provide a legible approximation of the
| relative speeds of different operations.
|
| From what's in the post, we can't tell if the interpreter is 128x
| slower than optimized code while Sparkplug code is only 32x
| slower, or the interpreter is 32x slower and Sparkplug code is
| 16x slower, or the interpreter is 32x slower and Sparkplug code
| is 4x slower (which is my guess as to the reality of the
| situation.) I'm more interested in this is-it-4x-or-32x question
| than the possibility that microbenchmarks might misrepresent a
| 32x slowdown as being a 48x slowdown or a 24x slowdown.
|
| https://docs.google.com/document/d/13c-xXmFOMcpUQNqo66XWQt3u...
| does offer more performance numbers, but they're for "an early
| prototype", and more importantly they don't break out the
| compilation speed vs. the execution speed.
|
| The description in the post makes it sound like Sparkplug output
| is pretty similar to direct-threaded Forth code, just a sequence
| of subroutine calls to other methods and implementations of
| primitive operations, interspersed with the occasional open-coded
| primitive (=== maybe) and control flow. And typically that means
| an overhead of around 4x, although maybe it's more with JS's
| aggressive implicit coercions and whatnot. It sure would have
| been nice to see some disassembly listings, too.
| Leszek wrote:
| Author here: we do have various numbers collected here, with
| varying levels of "I'm sure enough about these numbers to share
| them in a public blog post".
|
| Compile time is on roughly the same order of magnitude as
| Ignition compilation (just AST to bytecode, so excluding
| parsing), and roughly two to three orders of magnitude faster
| that TurboFan. The relative performance to the interpreter, as
| the sibling comments point out, varies _wildly_ by workload,
| but around 4x is probably a decent approximation for something
| not entirely dominated by property loads.
| kragen wrote:
| Thank you, that's great! How fast are property loads?
| Leszek wrote:
| Those vary widely too, depending on what (and more
| importantly, how many) object shapes they see. They'll be
| roughly the same cost in Ignition and Sparkplug though,
| since they effectively call the same handlers for those
| loads (and way faster in TurboFan, since that one then can
| generate optimised code tailored to that type feedback)
| kragen wrote:
| Oh, so code that's entirely dominated by property loads
| will be much slower in Sparkplug than in TurboFan, by a
| factor of more than that 4x, perhaps closer to the 32x I
| was SWAGging for Ignition's overall relative slowdown? I
| guess at this point I ought to be running microbenchmarks
| instead of asking you to do it for me... ;)
| sfink wrote:
| I doubt guesstimating based on interpreter loop overhead is
| going to get you that far, because Sparkplug looks like it has
| ICs. I would expect their impact to dominate on many, many
| workloads.
| kragen wrote:
| ICs? Integrated circuits?
| sfink wrote:
| Sorry, Inline Caches.
|
| From what I understand, there are basically 3 things that
| provide the bulk of performance gains in compilers: inline
| caches, register allocation, and inlining. The latter two
| don't really apply to interpreters or baseline compilers.
|
| Compilers do _many_ optimizations beyond those, but
| generally only for incremental improvements.
| kragen wrote:
| Oh, sorry, that should have been obvious. Are you
| suggesting that the performance improvement from inline
| caches (whether PIC or by-the-book monomorphic Deutsch &
| Schiffman) wouldn't show up in microbenchmarks, or would
| be misleadingly effective in microbenchmarks? IIRC Octane
| has a benchmark or two specifically for this...
|
| I think constant folding, specialization, and loop
| hoisting (especially of bounds checks) commonly produce
| more than merely incremental improvements. If you don't
| have some kind of inlining you don't get those (usually)
| but inlining by itself is usually only a minor
| improvement.
| sfink wrote:
| You're right, inlining by itself doesn't do much. I think
| inlining + register allocation is the big one, but you
| might be right about the other optimizations that are
| enabled (or at least massively boosted) by inlining.
|
| ICs steal a lot of specialization's thunder.
| (Alternatively, since the point of ICs _is_
| specialization, you could say it 's the other way
| around!)
| kragen wrote:
| Well, just as inlining is necessary for constant
| propagation or loop hoisting in a lot of cases,
| specialization is necessary for inlining in a lot of
| cases. Despite the nominative similarity, inline caches
| don't enable inlining in the same way specialization
| does, because they just enact the transfer of control
| more quickly; the code they transfer control to is just
| the standard generic version of the code, not an inline
| copy that can be optimized further according to the
| arguments at that callsite.
| IainIreland wrote:
| As an educated guess: Sparkplug is very similar in design to
| SpiderMonkey's baseline compiler. I ran a quick test a few
| weeks ago to see what the performance delta was between SM's
| baseline compiler and baseline interpreter, and going from the
| interpreter to the compiler was about a 2x speedup. Based on
| that, I'd expect that you're looking at a ~2-4x speedup from
| the interpreter to Sparkplug.
|
| Edited to add: Which is a very good result! Congrats to the V8
| team for landing this!
| mastrsushi wrote:
| Let us all take a moment to acknowledge the author's choice in
| deliberately titling the post "non-optimizing"
| eevilspock wrote:
| What tool are they using to generate those "hand drawn"
| illustrations? i like them.
| sambe wrote:
| A sibling poster suggested it is https://excalidraw.com/
| nielsbot wrote:
| Here's one https://roughjs.com
| rkagerer wrote:
| _A CPU is effectively an interpreter itself, albeit one for
| machine code; seen this way, Sparkplug is a "transpiler" from
| Ignition bytecode to CPU bytecode, moving your functions from
| running in an "emulator" to running "native"._
| quotemstr wrote:
| Yep. Formally in CS, there's no difference between a "compiler"
| and a "transpiler". They're both names for the process of
| program translation. All compilation is "transpilation", even
| when you're compiling things like C++.
| hajile wrote:
| To me, in general, compiling seems to imply native output
| where optimizations are done while transpiling seems to
| target another high-level language where semantics are kept
| as close to the original as possible.
| TazeTSchnitzel wrote:
| Compiling between high-level languages is a similarly
| difficult endeavour to compiling to low-level ones!
| kaba0 wrote:
| There is no real distinction, it is all just bad usage of
| the word. There are only compilers. It may compile to a
| high level language, but "transpiling" adds zero
| information to the sentence.
| ksec wrote:
| So they went from two tiers to three tiers, similar to what
| WenKit JSC is doing which I think is four tiers.
|
| I wonder how will it compare to JSC.
| inglor wrote:
| Technically four tiers: Ignition, Sparkplug, TurboProp,
| TurboFan. The design document goes into more detail
| https://docs.google.com/document/d/13c-xXmFOMcpUQNqo66XWQt3u...
| as well as the "The Case For Four Tiers" document (Google
| Internal mostly ATM
| https://docs.google.com/document/d/1VNUPPb2JQh0yg8SR2Y2SAk7h...
| )
| sfink wrote:
| Yeah, it's kind of funny. A couple years back, V8 had a
| baseline interpreter and optimizing compiler, then added a
| baseline compiler. Spidermonkey had a baseline compiler and
| optimizing compiler, then added a baseline interpreter.
|
| My guess is that all of the main JS engines will eventually
| evolve into crabs.
| masklinn wrote:
| > A couple years back, V8 had a baseline interpreter and
| optimizing compiler, then added a baseline compiler.
|
| Initially v8 only had a baseline compiler and an optimising
| jit (in fact that was very much the originality of v8 at
| release).
|
| A few years back they switched to an interpreter +
| optimising jit (ignition / turbofan), then they added the
| mid-tier turboprop which is turbofan with the most
| expensive optimisations dropped.
| gmaster1440 wrote:
| Neat, would love to understand how this would affect Node.js
| performance. Tooling like esbuild[0] exists for the very reason
| that compiled languages are significantly faster, so I wonder how
| viable of an alternative (in terms of performance) is it to build
| an esbuild in `node --sparkplug`.
|
| Or take the existing Webpack CLI which is written in Node, what
| kind of improvements can we expect there, if any?
|
| [0] https://esbuild.github.io/faq/#why-is-esbuild-fast
| wonnage wrote:
| This would mostly benefit performance the first time some code
| is run, before v8 has a chance to gather runtime data and
| optimize it. Since all JS execution goes through this phase,
| this should make everything a little faster, but it will
| probably benefit short-lived processes (e.g, a website script)
| more than something like a bundler.
| klodolph wrote:
| It's not really a good blanket statement that compiled
| languages are significantly faster. There are a lot of problems
| getting tools written in JavaScript to run fast, and the fact
| that JavaScript is "not compiled" is a bit farther down the
| list. There are other pressing issues, like the fact that code
| written in Go, like esbuild, is free to run multiple threads
| and freely share data structures between threads.
|
| The distinction between compiled and interpreted languages is
| also a bit fuzzy to begin with.
| gmaster1440 wrote:
| Yes, as the link I shared mentions, there's concurrency via
| actual threads, and proper memory sharing--by no means
| implying compilation is all there is, but it does seem at the
| very least to be a contributing factor, and understanding the
| change to bottom-line performance would still be welcome.
| azinman2 wrote:
| Wonder why the M1 is so much worse on certain benchmarks. I wish
| they'd gone into detail there as it's not uniform across their
| tests.
| drew-y wrote:
| Those benchmarks measure performance _increase_ rather than
| performance overall. M1 isn 't necessarily doing "worse" as
| much as it just didn't see as much of an improvement as the
| others.
| Leszek wrote:
| Author here: this is indeed just relative score change, the
| M1's absolute values are of course impressively high. My
| personal, unofficial suspicion is that this has something to do
| with the size of the M1's reorder buffer being so large that it
| can just predict and speculate straight through all the
| interpreter overheads; but that's just a guess.
| Lorin wrote:
| The blog illustrations don't display properly in 'dark mode' site
| theme.
| k__ wrote:
| I like, how the "improvements" for Reddit are negative, lol
| sergiomattei wrote:
| I'm usually very pro-SPA, but Reddit messed up theirs so bad,
| it's plain unusable.
|
| Most interactions using a 2018 iPad Pro w/ Safari take
| _seconds_ to execute. This device can handle anything I throw
| at it except reddit.com.
|
| The new web app is a plain regression.
| masklinn wrote:
| TBF the entire concept of an SPA for Reddit is insane to
| begin with. There are no complex split-second interactions or
| anything. It's a case study in what does not benefit in any
| way from spas.
|
| I have pretty much the same thoughts about discourse, which I
| loathe.
| phpnode wrote:
| I feel like they're pretty strongly incentivised to not
| improve it. They want to push people to the app, and they
| have enough traction and community lock-in that they're not
| really worried about losing traffic
| a_wild_dandan wrote:
| The Reddit redesign is far and away the most sluggish,
| unresponsive major site I've ever used. It brings my fancy
| $3,000 laptop to its knees.
|
| The sad thing is, the front end stack they use (React/Redux) is
| great. But it's an abysmal example of the tech. I turned on re-
| renders in the React devtools, and this is what it looks like:
| https://i.imgur.com/Sm1U24M.gifv
|
| Makes me sad.
| fastball wrote:
| Oh no that is bad.
| Leszek wrote:
| Author here: ah well, you win some, you lose some. This is just
| v1 that's shipping with M91, we've already made improvements
| since then and there's plenty more low hanging fruit to pick,
| heuristics to tweak, etc.
| MaxBarraclough wrote:
| > With our current two-compiler model, we can't tier up to
| optimised code much faster; we can (and are) working on making
| the optimisation faster, but at some point you can only get
| faster by removing optimisation passes [...]
|
| > Enter Sparkplug: our new non-optimising JavaScript compiler
| we're releasing with V8 v9.1, which nestles between the Ignition
| interpreter and the TurboFan optimising compiler.
|
| This surprised me. Didn't V8 have a 'fast JIT' alongside an
| optimizing JIT as early as 2013? It was called _full-codegen_.
| Did they remove it?
|
| https://wingolog.org/archives/2013/04/18/inside-full-codegen...
| chrisseaton wrote:
| > This surprised me. Didn't V8 have a 'fast JIT' alongside an
| optimizing JIT as early as 2013? It was called full-codegen.
|
| Did you see the image at the bottom of the design document?
| That explains it.
| justicezyx wrote:
| I guess it's not obvious that answers with information are
| mostly adjective or preposition words are poor answer to
| engineering questions, because engineers are looking for
| (relatively) precise answers.
|
| For example: * What is bottom? Did it means one of the
| pictures in the bottom page. Or the absolute bottom.
|
| * What if the picture was not at the bottom, instead, it was
| followed by large chunk of texts?
|
| * What deisng doc? Was it mentioned in the parent post, or
| was it a "well known" design doc?
|
| * Design doc for which topic? Turbofan/Craneshaft/etc? (This
| is nitpicking, but still happen often because one might found
| this as the first sight, not following the thread. Again,
| reading answers for engineers are also a skill, quite
| different from reading, for example, science fiction, another
| day another topic)
|
| * What does the figure explain? What if the figure shows
| something that not match my concept of the question?
|
| In the end, answering engineering question, should be, and
| have to be, stating accurate facts. If and how that actually
| answers the question, is more depending on the readers and
| questioner's continued communication.
|
| Edit: To make sure I was not leaving wrong impression, those
| questions I listed above was encountered everyday for the
| answers I received. It's not nitpicking. You can read this
| thread for an example how answers without concrete
| information being inappropriate engineering answers.
| coldtea wrote:
| Note how we didn't learn anything (precise or not) about
| the answer to the question from this comment, but we did
| learn your preferences about communication styles.
| justicezyx wrote:
| Now the parent was gone, it seems I am random ranting...
|
| BTW, I view this as not about communication style, but
| communication efficiency.
| wonnage wrote:
| I'm not sure how you'd expect people to find that since it's
| not linked from the article.
| ptx wrote:
| Where?
| chrisseaton wrote:
| https://docs.google.com/document/d/13c-xXmFOMcpUQNqo66XWQt3
| u...
| MaxBarraclough wrote:
| Thanks. For those following, see specifically _FAQ >
| Isn't this just FullCodeGen?_
| IainIreland wrote:
| In the document here[1], there's a picture of Gandalf the
| White at the bottom with the caption "Sparkplug: I am
| FullCodeGen. Or rather, FullCodeGen, as it should have
| been".
|
| [1]:https://docs.google.com/document/d/13c-xXmFOMcpUQNqo66X
| WQt3u...
| Leszek wrote:
| Author here: we did, and as other comments say we removed it in
| favour of Ignition, with the intent of at some point in the
| future bringing back some form of baseline compiler.
|
| The big difference is that Sparkplug compiles from bytecode,
| not from source, and thus the bytecode stays the source of
| truth for the program. Back in the FCG days, the optimising
| compiler had to re-parse the source code to AST, and compile
| from there - even worse, to be able to deoptimise back to FCG,
| it had to kind of "replay" FCG compilation to get the deopted
| stack frame right.
|
| We did actually have a configuration (never released) that had
| Ignition, FCG, _and_ TurboFan, and boy was it a mess...
| MaxBarraclough wrote:
| > We did actually have a configuration (never released) that
| had Ignition, FCG, _and_ TurboFan, and boy was it a mess...
|
| Do you mean it had technical debt that was never resolved, or
| do you mean 3 tiers is always too many?
|
| I believe WebKit's JavaScript engine currently has 4 tiers,
| [0] although that includes an interpreter as the first tier.
|
| [0] https://arstechnica.com/information-
| technology/2014/05/apple...
| kaba0 wrote:
| Could you please compare it to OpenJDK's JIT compilers? I'm
| unfortunately not familiar with V8 - but do I get it right
| that they have sort of converged and just got more similar?
| masklinn wrote:
| > Did they remove it?
|
| They removed it a while back after they introduced the ignition
| interpreter: v8 used to only have compilers (full-codegen and
| crankshaft, the latter being the jit optimising compiler, full-
| codegen was _not a jit_ ).
|
| One particularity of the pipeline was that since there was no
| interpreter both compilers worked from the source, which
| crankshaft had to keep around and re-parse.
|
| In 2013 they started working on a new optimising jit
| (turbofan), but for a while it didn't really improve things
| (though the failure modes were often better than crankshaft's).
|
| Then some engineers realised they could use part of turbofan
| for an interpreter, which would allow load sharing (amongst
| other things turbofan would jit ignition's bytecode instead of
| having to re-parse from source), that brought in the
| improvements they were looking for, but then full-codegen was
| kinda sitting in the middle awkwardly with no integration, its
| low-end eaten by ignition and with no real high end. So it was
| dropped alongside crankshaft in v8 5.9.
|
| Now they're reintroducing a non-optimising compiler, but one
| that's integrated with the ignition-turbofan pipeline, and
| actually a jit.
| londons_explore wrote:
| * Figure out which bits of javascript consume the most CPU time
| globally.
|
| * Rewrite those bits of javascript in C++, and patch them in
| whenever they are seen byte-for-byte in the wild.
|
| * Have both a fuzzer and a webcrawler verify the two
| implementations never differ.
|
| Obviously it's vital the C++ and javascript implementations
| behave identically - so probably best to have a bunch of checks
| that the input is in the exact form expected, and fall back to
| the javascript version if the C++ version can't be guaranteed to
| behave the same.
|
| The C++ versions of say the globally most popular 25 MB of
| javascript source code could be shipped with Chrome, or as a
| seperate download-on-first-use component.
| nayuki wrote:
| This feels like a repeat of the C1 and C2 compiler tiers in the
| Java HotSpot virtual machine.
| https://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary...
| titzer wrote:
| It isn't. C1 and C2 are two competing optimizing compilers in
| HotSpot, originally developed by different teams. There is a
| big difference in compilation time vs code quality between
| those, though that narrowed as C1 evolved from almost a
| template-compiler to an SSA-CFG-based compiler. C2 was and is a
| graph-based, heavily optimizing compiler with a graph coloring
| register allocation. It's only been in the past 5-10 years or
| so that both compilers have been used in concert for tiering.
| Prior to that, they were selected by the "-client" or "-server"
| options, once at startup.
|
| Sparkplug is designed specifically as a baseline compiler that
| does not have an IR, in order for maximum compile speed. While
| the idea of two compilers with different compile time/speed
| tradeoffs is similar, the constants involved are pretty
| different.
|
| TurboFan is similar to C2, but Sparkplug is not similar to C1.
| They don't compete, but cooperate.
| pizlonator wrote:
| This is an impressive result! Congratulations are in order. :-)
| ndesaulniers wrote:
| "I think the stack grows downward" was a nice touch, lol.
| Rapzid wrote:
| I love how it doesn't go into any details and just leaves
| people to wonder why there is a button casually
| challenging/catering to their assumptions haha.
| Leszek wrote:
| Just wait until you look at the source code which decides
| which way round to show you the stacks at first...
| ndesaulniers wrote:
| Oh, I already enjoy
|
| #define __ basm_.
___________________________________________________________________
(page generated 2021-05-27 23:00 UTC)