1. 06 Apr, 2020 2 commits
  2. 03 Apr, 2020 1 commit
  3. 02 Apr, 2020 1 commit
    • Eric Myhre's avatar
      Couldn't resist adding a few more test cases. · de61bf1c
      Eric Myhre authored
      I was particularly worried about trailing optionals.  Happily,
      they appear to work fine.
      
      Representation iterators and smooth stopping included.
      
      Starting to wonder how best to organize these tests to continue.
      333 lines already?  Questionable scalability.
      de61bf1c
  4. 01 Apr, 2020 15 commits
    • Eric Myhre's avatar
      Merge branch 'codegen-how-maybe' into codegen-oppdatering · e422966c
      Eric Myhre authored
      Seems to be on track after all.
      e422966c
    • Eric Myhre's avatar
      All nullable and optional cases tested & passing. · ddcf6b4d
      Eric Myhre authored
      I started doing double-duty in tests to keep the volume down:
      the block for nullables tests both 'nullable' and 'nullable optional';
      and the optionals similarly.  Will make failures require more careful
      reading to discern, but probably a fine trade.
      ddcf6b4d
    • Eric Myhre's avatar
      Test null nullable field; works. · bcf7b05c
      Eric Myhre authored
      bcf7b05c
    • Eric Myhre's avatar
      Tests on gen output; some fixes; working. · 072098e6
      Eric Myhre authored
      I'm almost a little staggered.  We've got structures with maybes
      both using pointers and not, and everything seems fine.
      
      A few fixes were necessary, but they were mostly "d'oh" grade.
      (The finishCallback logic is a bit hairy, but appears correct now.)
      
      More cases for more variations in presence coming up next.
      072098e6
    • Eric Myhre's avatar
      More smoke test types: exercise maybeptr=true. · 6dfdc19c
      Eric Myhre authored
      Sanity check level: gen result compiles.
      
      Next: time to target tests at all this jazz that actually exercise
      and run the generated code.
      6dfdc19c
    • Eric Myhre's avatar
      1a6143a4
    • Eric Myhre's avatar
      Support maybe implemented using ptr. · 255bbc57
      Eric Myhre authored
      Easier than previous commit message seemed to think.
      
      Since we already have finishCallback functions in play in any scene
      where maybes can also be in play, we can just make those responsible
      for copying out a pointer from the child assembler to the parent value.
      That makes the child assembler able to create a new value late in
      its lifecycle and still expect it can land in a proper home.
      
      Tests needed badly; this is *oodles* of edge cases.
      Unrelated fix needed first.  (Might make that elsewhere and rebase
      this to include that, to reduce the carnage ever so slightly.)
      255bbc57
    • Eric Myhre's avatar
      Fix many issues with maybes; adjunct config for if maybe is implemented using... · 125a2a7f
      Eric Myhre authored
      Fix many issues with maybes; adjunct config for if maybe is implemented using a pointer; attempt the finishCallback system for reusable child assemblers.
      
      This... almost appears to be on a dang nice track.
      
      Except one fairly critical thing.  It doesn't work correctly if your
      maybe IS implemented as containing a pointer.
      
      Because that pointer starts life as nil in the parent structure.
      
      And that's hard to fix.
      
      We can't have a pointer to a pointer in the assembler;
      that'd cause the type bifructation we've been trying to avoid.
      (We can give up and do that of course; it would be correct.
      Just... more code and larger final assembly size.
      And if we did this, a lot of this diff would be rewritten.)
      
      We could have the parent structure allocate a blank of the field,
      and put a pointer to it in the maybe and also in the child assembler.
      Except... this is a wasted allocation if it turns out to be null.
      
      Rock and hard place detected in a paired configuration.
      Might have to back out of this approach entirely.  Not sure.
      125a2a7f
    • Eric Myhre's avatar
      Late night thoughts on reducing gen type count. · 896d0e57
      Eric Myhre authored
      We'll see how this works out.
      896d0e57
    • Eric Myhre's avatar
      ErrInvalidKey type, and use it. · 27873cb0
      Eric Myhre authored
      27873cb0
    • Eric Myhre's avatar
      Checkpoint of codegen'd struct assemblers. · fd94a062
      Eric Myhre authored
      Lots of tasty bitshuffling in this commit.
      
      Remaining: creating, embedding, and returning child value assemblers.
      This might involve a full type per field: doing so would be direct,
      have pretty excellent verbosity in debugging symbols,
      and pointers back to parent suffer no interface indirection;
      overall, it's probably going to have the fastest results.
      However, it would also mean: we can't save resident memory size if we
      have more than one field in a struct that has the same type;
      and we we can't avoid increasing binary size if the same types are used
      as fields in several places.
      Going to pause to think about this for a while.
      fd94a062
    • Eric Myhre's avatar
      Fix basicnode maps to not wedge on repeated keys; and DRY maps and lists AssignNode validation. · f3be0c3b
      Eric Myhre authored
      The HACKME_builderBehaviors.md file in the repo root says we should be
      able to procede from these repeated keys conditions.
      
      The AssignNode systems now use Finish methods more reasonably.
      In the absense of actual validation logic, I don't know if this really
      structurally mattered, but it feels better now.
      
      There's one further comment in basicnode.Map and basicnode.List's
      AssignNode method about semantics when it's used after *some* data has
      already been added; this should probably be addressed at some point.
      I'm pushing it off the "today" plate though, since it's not a critical
      bug that violates immutability or some such.
      
      All three of these issues were noticed while building the related
      semantics in codegen for structs.
      f3be0c3b
    • Eric Myhre's avatar
      Nicely composable NodeBuilderGenerator, +mixins. · 7ff42bd3
      Eric Myhre authored
      This cleans up a lot of stuff and reduces the amount of boilerplate
      content that's just generating monomorphizing error method stubs.
      
      The nodeGenerator mixins now also use the node mixins.  I don't know
      why I failed to do that from the start; I certainly meant to.
      It results in shorter generated code, and the compiler turns it into
      identical assembly.
      7ff42bd3
    • Eric Myhre's avatar
      Correct iterators for representatons of structs with absent optional values. · 96d63add
      Eric Myhre authored
      (That line kind of says a lot about why this project is tricky, doesn't it.)
      96d63add
    • Eric Myhre's avatar
      Checkpoint of struct gen progress. · 80b0ba29
      Eric Myhre authored
      Compiles and runs; but the results don't.
      Some small issues; some big issues.  Just needed a checkpoint.
      
      It's feeling like it's time to deal with breaking down the assembler
      generators in the same way we did for all the node components, which
      is going to be kind of a big shakeup.
      80b0ba29
  5. 29 Mar, 2020 4 commits
    • Eric Myhre's avatar
      Beginning of struct gen; much ado about Maybes. · 59c295db
      Eric Myhre authored
      I *think* the notes on Maybes are now oscillating increasingly closely
      around a consistent centroid.  But getting here has been a one heck of
      a noodle-scratcher.
      59c295db
    • Eric Myhre's avatar
      Fix excessing pointers in maybes. · cbef5c3e
      Eric Myhre authored
      cbef5c3e
    • Eric Myhre's avatar
      14e6653c
    • Eric Myhre's avatar
      Begin reboot of codegen; also, some research. · 3f138f7f
      Eric Myhre authored
      In research: I found that there are more low-cost ways to switch which
      methods are available to call on a value than I thought.  Also, these
      techniques work even for methods of the same name.  This is going to
      improve some code in NodeAssemblers significantly -- there are several
      situations where this will let us reuse existing pieces of memory
      instead of needing to allocate new ones; even the basicnode package
      now already needs updates to improve this.  It's also going to make
      returning representation nodes from our typed nodes *significantly*
      easier and and lower in performance costs.  (Before finding methodsets
      are in fact so feasible, I was afraid this was going to require our
      typed nodes to embed yet another small struct with a pointer back to
      themselves so we can have amortized availability of value that contains
      the representation's logic for the Node interface... which while it
      certainly would've worked, would've definitely made me sigh deeply.)
      Quite exciting for several reasons; only wish I'd noticed this earlier.
      
      Also in research: I found a novel way to make it (I believe) impossible
      to create zero values of a type, whilst also making a symbol available
      for it in other packages, so that we can do type assertions, etc,
      with that symbol.  This is neat.  We're gonna use this to make sure
      that types in your schema package can never be created without passing
      through any validation logic that the user applies.
      
      In codegen: lots of files disappear.  I'm doing a tabula rasa workflow.
      (A bunch of the old files stick around in my working directory, and
      are being... "inspirational"... but everything is getting whitelisted
      before any of it ports over to the new commits.  This is an effective
      way to force myself to do things like naming consistency re-checks
      across the board.  And there's *very* little that's getting zero change
      since the changes to pointer strategy and assembler interface are so
      sweeping, so... there's very little reason *not* to tabula rasa.)
      
      Strings are reimplemented already.  *With* representations.
      
      Most of the codegen interfaces stay roughly the same so far.
      I've exported more things this time around.
      
      Lots of "mixins" based on lessons learned in the prior joust.
      (Also a bunch of those kind-based rejections look *much* nicer now,
      since we also made those standard across the other node packages.)
      
      Some parts of the symbol munging still up in the air a bit.
      I think I'm going to go for getting all the infrastructure in place
      for allowing symbol-rename adjunct configuration this time.
      (I doubt I'll wire it all the way up to real usable configuration yet,
      but it'll be nice to get as many of the interventions as possible into
      topologically the right places to minimize future effort required.)
      
      There's a HACKME_wip.md file which contains some other notes on
      priorities/goals/lessoned-learned-now-being-applied in this rewrite
      which may contain some information about what's changing at a higher
      level than trying to track the diffs.  (But, caveat: I'm not really
      writing it for an audience; more my own tracking.  So, it comes with
      no guarantee it will make sense or be useful.)
      3f138f7f
  6. 27 Mar, 2020 6 commits
  7. 26 Mar, 2020 2 commits
    • Eric Myhre's avatar
      Rename 's/AssembleDirectly/AssembleEntry/g'. · de201f2d
      Eric Myhre authored
      de201f2d
    • Eric Myhre's avatar
      Finish AssignNode handling for recursives. · 9de3d223
      Eric Myhre authored
      Update to the hackme document to match.
      (Turns out a decent amount of early speculations about how handy it
      would be to nil out the 'w' pointer ended up a bit misguided.)
      
      This spawns another one of those things that's dang hard to test:
      half of these functions are for handling the case that something of
      the right *kind* but a different *style*/implementation is given...
      which we can't test except by ramming two different node
      implementations together.  Even later, that's going to make things
      rather Interesting, because the test package dependencies will
      include one concrete node package and then be that much less usable
      by that package itself (ugh); getting correct coverage attribution
      to the odd package out there will be hard; and for now, it means we
      just plain have no tests for those branches.  Making tests to check
      compatibility and correctness of these branches once we get more
      codegen integration up will be fun, though.
      9de3d223
  8. 11 Mar, 2020 6 commits
  9. 02 Mar, 2020 1 commit
    • Eric Myhre's avatar
      Promote NodeAssembler/NodeStyle solution to core. · 4eb8c55c
      Eric Myhre authored
      This is a *lot* of changes.  It's the most significant change to date,
      both in semantics and in character count, since the start of this repo.
      It changes the most central interfaces, and significantly so.
      
      But all tests pass.  And all benchmarks are *improved*.
      
      The Node interface (the reading side) is mostly unchanged -- a lot of
      consuming code will still compile and work just fine without changes --
      but any other Node implementations out there might need some updating.
      
      The NodeBuilder interface (the writing side) is *extremely* changed --
      any implementations out there will *definitely* need change -- and most
      consumers will too.  It's unavoidable with a semantic fix this big.
      The performance improvements should make it worth your while, though.
      
      If you want more background on how and why we got here, you've got
      quite a few commits on the "research-admissions" branches to catch up
      on reading.  But here's a rundown of the changes:
      
      (Get a glass of water or something calming before reading...)
      
      === NodeAssembler introduced! ===
      
      NodeAssembler is a new interface that describes most of the work of
      creating and filling data into a new Node.
      
      The NodeBuilder interface is still around, but changed in role.
      A NodeBuilder is now always also a NodeAssembler; additionally, it can
      return the final Node to you.
      
      A NodeAssembler, unlike NodeBuilder, can **not** return a Node to you.
      In this way, a NodeBuilder represents the ability to allocate memory.
      A NodeAssembler often *does not*: it's just *filling in* memory.
      
      This design overall is much more friendly to efficient operations:
      in this model, we do allocations in bulk when a NodeBuilder is used,
      and then NodeAssemblers are used thereafter to fill it in -- this
      mental model is very friendly to amortizing memory allocations.
      Previously, the NodeBuilder interface made such a pattern of use
      somewhere between difficult and outright impossible, because it was
      modeled around building small values, then creating a bigger value and
      inserting the smaller ones into it.
      
      This is the key change that cascaded into producing the entire other
      set of changes which land in this commit.
      
      The NodeBuilder methods for getting "child builders" are also gone
      as a result of these changes.  The result feels a lot smoother.
      (You can still ask for the NodeStyle for children of a recursive kind!
      But you'll find that even though it's possible, it's rarely necessary.)
      
      We see some direct improvements from this interface change already.
      We'll see even more in the future: creating values when using codegen'd
      implementations of Node was hugely encumbered by the old NodeBuilder
      model; NodeAssembler *radically* raises the possible ceiling for
      performance of codegen Node implementations.
      
      === NodeStyle introduced ===
      
      NodeStyle is a new interface type that is used to carry information
      about concrete node implementations.
      
      You can always use a NodeStyle to get a NodeBuilder.
      
      NodeStyle may also have additional features on it which can be detected
      by interface checks.  (This isn't heavily used yet, but we imagine it
      might become handy in the future.)
      
      NodeStyle replaces NodeBuilder in many function arguments,
      because often what we wanted was to communicate a selection of Node
      implementation strategy, but not actually the start of construction;
      the NodeStyle interface now allows us to *say that*.
      
      NodeStyle typically cost nothing to pass around, whereas a NodeBuilder
      generally requires an allocation to create and initialize.  This means
      we can use NodeStyle more freely in many contexts.
      
      === node package paths changed ===
      
      Node implementations are now in packages under the "node/*" directory.
      Previously, they were under an "impl/*" directory.
      
      The "impl/free" package is replaced by the the "node/basic" package!
      The package name was "ipldfree"; it's now "basicnode".
      
      === basicnode is an improved runtime/anycontent Node implementation ===
      
      The `basicnode` package works much the same as the `ipldfree` package
      used to -- you can store any kind of data in it, and it just does as
      best it can to represent and handle that, and it works without any
      kind of type info nor needs of compile-time special support, etc --
      while being just quietly *better at it*.
      
      The resident memory size of most things has gone down.
      (We're not using "fat unions" in the implementation anymore.)
      
      The cost of iterating maps has gone down *dramatically*.
      Iteration previously suffered from O(n) allocations due to
      expensive `runtime.conv*` calls when yielding keys.
      Iteration is now O(1) (!!) because we redesigned `basicnode` internals
      to use "internal pointers" more heavily, and this avoids the costs
      from `runtime.conv*`.
      (We could've done this separately from the NodeAssembler change,
      admittedly.  But both are the product of research into how impactful
      clever use of "internal pointers" can be, and lots of code in the
      neighborhood had to be rewritten for the NodeAssembler interface,
      so, these diffs arrive as one.)
      
      Error messages are more informative.
      
      Many small operations should get a few nanoseconds faster.
      (The implementation uses more concrete types and fewer switch
      statements.  The difference probably isn't the most noticeable part of
      all these changes, but it's there.)
      
      --- basicnode constructor helpers do all return pointers ---
      
      All the "New*" helper functions in the basicnode package return
      interfaces which are filled by a pointer now.
      This is change from how they worked previously when they were first
      implemented in the "rsrch" package.
      
      The experience of integrating basicnode with the tests in the traversal
      package made it clear that having a mixture of pointer and non-pointer
      values flying around will be irritating in practice.  And since it is
      the case that when returning values from inside a larger structure,
      we *must* end up returning a pointer, pointers are thus what we
      standardize on.
      
      (There was even some writeup in the HACKME file about how we *might*
      encounter issues on this, and need to change to pointers-everywhere --
      the "pointer-vs-value inhabitant consistency" heading.  Yep: we did.
      And since this detail is now resolved, that doc section is dropped.)
      
      This doesn't really make any difference to performance.
      The old way would cause an alloc in those method via 'conv*' methods;
      the new way just makes it more explicit and go through a different
      runtime method at the bottom, but it's still the same number of
      allocations for essentially the same reasons.  (I do wonder if at some
      future point, the golang compiler might get cleverer about eliding
      'conv*' calls, and then this change we make here might be unfortunate;
      but that's certainly not true today, nor in the future at any proximity
      that I can foresee.)
      
      === iterator getters return nil for wrong-kind ===
      
      The Node.MapIterator and Node.ListIterator methods now return nil
      if you call them on non-maps or non-lists.
      
      Previously, they would return an iterator, but using it would just
      constantly error.
      
      I don't think anyone was honestly really checking those error thunks,
      and they made a lot of boilerplate white noise in the implementations,
      and the error is still entirely avoidable by checking the node kind
      up-front (and this is strictly preferable anyway, since it's faster
      than getting an error thunk, poking it to get the error, etc)...
      so, in total, there seem like very few reasons these were useful:
      the idea is thus dropped.
      
      Docs in the Node interface reflect this.
      
      === node/mixins makes new Node implementations easier ===
      
      The mixins package isn't usable directly, but if you're going to make
      a new Node implementation, it should save you a lot of typing...
      and also, boost consistency of basic error handling.
      
      Codegen will look forward to using this.  (Codegen already had much of
      these semantics internally, and so this package is sort of lifting that
      back out to be more generally usable.  By making it live out here as
      exported symbols in the core library, we should also reduce the sheer
      character count of codegen output.)
      
      === 'typed.Node' is now 'schema.TypedNode' ===
      
      A bunch of interfaces that were under the "impl/typed" path moved to
      be in the "schema" package instead.  This probably makes sense to you
      if you look at them and needs no further explanation.
      
      (The reason it comes in this diff, though, is that it was forced:
      adding better tests to the traversal package highlighted a bunch of
      cyclic dependency issues that came from 'typed.Node' being in a
      package that had concrete use of 'basicnode'.)
      
      === codecs ===
      
      The 'encoding' package is now named 'codec'.
      
      This name is shorter; it's more in line with vocabulary we use
      elsewhere in the IPLD project (whereas 'encoding' was more of a nod
      to the naming found in the golang standard library); and in my personal
      opinion it does better at describing the both directions of the process
      (whereas 'encoding' sounds like only the to-linear-bytes direction).
      
      I just like it better.
      
      === unmarshal functions no longer return node ===
      
      Unmarshal functions accept an NodeAssembler parameter (rather than
      a NodeBuilder, as before, nor a NodeStyle, which might also make sense
      in the new family of interfaces).
      
      This means they no longer need to return a Node, either -- the caller
      can decide where the unmarshalled data lands.  If the caller is using
      a NodeBuilder, it means they can call Build on that to get the value.
      (If it's a codegen NodeBuilder with More Information, the caller can
      use any specialized functions to get the more informative pointers
      without need for casting!)
      
      Broadly speaking, this means users of unmarshal functions have more
      control over how memory allocation comes into play.
      
      We may want to add more helper functions to the various codec packages
      which take a NodeStyle argument and do return a Node.  That's not in
      this diff, though.  (Need to decide what pattern of naming these
      various APIs would deserve, among other things.)
      
      === the fluent package ===
      
      The fluent package changed significantly.
      
      The readonly/Node side of it is dropped.  It didn't seem to get a ton
      of exercise in practice; the 'traversal' package (and in the future,
      perhaps also a 'cursor' package) addresses a lot of the same needs,
      and what remains is also covered well these days by the 'must' package;
      and the performance cost of fluent node wrappers as well as the
      composability obstruction of them... is just too much to be worth it.
      
      The few things that used fluent.Node for reading data now mostly use
      the 'must' package instead (and look better for it, imo).
      
      It's possible that some sort of fluent.Node will be rebuilt someday,
      but it's not entirely clear to me what it should look like, and indeed
      whether or not it's a good idea to have in the repo at all if the
      performance of it is counterindicated in a majority of situations...
      so, it's not part of today's update.
      
      The writing/NodeBuilder/NodeAssembler fluent wrappers are continued.
      It's similar to before (panics promptly on errors, and has a lot of
      closures)... but also reflects all of the changes made in the migration
      towards NodeAssembler: it doesn't return intermediate nodes, and
      there's much less kerfuffle with getting child builders.
      Overall, the fluent builders are now even more streamlined than before;
      the closures need even fewer parameters; great success!
      
      The fluent.NodeAssembler interface retains the "Create" terminology
      around maps and lists, even though in the core interfaces,
      the ipld.NodeAssembler interface now says "Begin" for maps and lists.
      This is because the fluent.NodeAssembler approach, with its use of
      closures, really does do the whole operation in one swoop.
      
      (It's amusing to note that this change includes finally nuking some
      fairly old "REVIEW" comment blocks from the old fluent package which
      regarded the "knb" value and other such sadness around typed recursion.
      Indeed, we've finally reviewed that: and the answer was indeed to do
      something drastically different to make those recursions dance well.)
      
      === selectors ===
      
      Selectors essentially didn't change as part of this diff.  Neat.
      
      (They should get a lot faster when applied, because our node
      implementations hit a lot less interface boxing in common operations!
      But the selector code itself didn't need to change to get the gains.)
      
      The 'selector/builder' helper package *did* change a bit.
      The changes are mostly invisible to the user.
      I do have some questions about the performance of the result; I've got
      a sneaking suspicion there's now a bunch of improvements that might be
      easier to get to now than they would've been previously.  But, this is
      not my quest today.  Perhaps it will deserve some review in the future.
      
      The 'selector/builder' package should be noted as having some
      interesting error handling strategies.  Namely, it doesn't.
      Any panics raised by the fluent package will just keep rising; there's
      no place where they're converted to regular error value returns.
      I'm not sure this is a good interface, but it's the way it was before
      I started passing through, so that's the way it stays after this patch.
      
      ExploreFieldsSpecBuilder.Delete disappears.  I hope no one misses it.
      I don't think anyone will.  I suspect it was there only because the
      ipld.MapBuilder interface had such a method and it seemed like a
      reasonable conservative choice at the time to proxy it; now that the
      method proxied is gone, though, so too shall go this.
      
      === traversal ===
      
      Traversal is mostly the same, but a few pieces of config have new names.
      
      `traversal.Config.LinkNodeBuilderChooser` is now
      `traversal.Config.LinkTargetNodeStyleChooser`.
      Still a mouthful; slightly more accurate; and reflects that it now
      works in terms of NodeStyle, which gives us a little more finesse in
      reasoning about where NodeBuilders are actually created, and thus
      better control and insight into where allocations happen.
      
      `traversal.NodeBuilderChooser` is now
      `traversal.LinkTargetNodeStyleChooser` for the same reasons.
      
      The actual type of the `LinkTargetNodeStyleChooser` now requires
      returning a `NodeStyle`, in case all the naming hasn't made it obvious.
      
      === disappearing node packages ===
      
      A couple of packages under 'impl/*' are just dropped.
      
      This is no real loss.  The packages dropped were Node implementations
      that simply weren't done.  Deleting them is an increase in honesty.
      
      This doesn't mean something with the same intentions as those packages
      won't come back; it's just not today.
      
      --- runtime typed node wrapper disappeared ---
      
      This one will come back.  It was just too much of a pain to carry
      along in this diff.  Since it was also a fairly unfinished
      proof-of-concept with no downstream users, it's easier to drop and
      later reincarnate it than it is to carry it along now.
      
      === linking ===
      
      Link.Load now takes a `NodeAssembler` parameter instead of a
      `NodeBuilder`, and no longer returns a `Node`!
      
      This should result in callers having a little more control over where
      allocations may occur, letting them potentially reuse builders, etc.
      
      This change should also make sense considering how codec.Unmarshal
      now similarly takes a NodeAssembler argument and does not return
      a Node value since its understood that the caller has some way to
      access or gather the effects, and it's none of our business.
      
      Something about the Link interface still feels a bit contorted.
      Having to give the Load method a Loader that takes half the same
      arguments all over again is definitely odd.  And it's tempting to take
      a peek at fixing this, since the method is getting a signature change.
      It's unclear what exactly to do about this, though, and probably
      a consequential design decision space... so it shall not be reopened
      today during this other large refactor.  Maybe soon.  Maybe.
      
      === the dag-json codec ===
      
      The dag-json codec got harder to implement.  Rrgh.
      
      Since we can't tell if something is going to become a Link until
      *several tokens in*, dag-json is always a bit annoying to deal with.
      Previously, however, dag-json could still start optimistically building
      a map node, and then just... quietly drop it if we turn out to be
      dealing with a link instead.  *That's no longer possible*: the process
      of using NodeAssembler doesn't have a general purpose mechanism for
      backtracking.
      
      So.  Now the dag-json codec has to do even more custom work to buffer
      tokens until it knows what to do with them.  Yey.
      
      The upside is: of course, the result is actually faster, and does fewer
      memory allocations, since it gathers enough information to decide what
      it's doing before it begins to do it.
      (This is a lovely example of the disciplined design of NodeAssembler's
      interface forcing other code to be better behaved and disciplined!)
      
      === traversal is faster ===
      
      The `BenchmarkSpec_Walk_MapNStrMap3StrInt/n=32` test has about doubled
      in speed on the new `basicnode` implementation in comparison to the old
      `ipldfree.Node` implementation.
      
      This is derived primarily from the drop in costs of iteration on
      `basicnode` compared to the old `ipldfree.Node` implementation.
      
      Some back-of-the-envelope math on the allocation still left around
      suggest it could double in speed again.  The next thing to target
      would be allocations of paths, followed by iterator allocations.
      Both are a tad trickier, though (see a recently merge-ignore'd
      commit for notes on iterators; and paths... paths will be a doozy
      because the path forward almost certainly involves path values
      becoming invalid if retained beyond a scope, which is... unsafe),
      so certainly need their own efforts and separate commits.
      
      === marshalling is faster ===
      
      Marshalling is much faster on the new `basicnode` implementation in
      comparison to the old `ipldfree.Node` implementation.
      Same reasons as traversal.
      
      Some fixes to marshalling which previously caused unnecessary
      allocations of token objects during recursions have also been made.
      These improve speed a bit (though it's not nearly as noticeable as the
      boost provided by the Node implementation improvements to iteration).
      
      === size hints showed up all over the place ===
      
      The appearance of size hint arguments to assembly of maps and lists
      is of course inevitable from the new NodeAssembler interface.
      
      It's particularly interesting to see how many of them showed up in
      the selector and selectorbuilder packages as constants.
      
      And super especially interesting how many of them are very small
      constants.  44 zeros.  86 ones.  25 twos.  9 threes.  2 fours.
      (Counted via variations of `grep -r 'Map(.*4, func' | wc -l`.)
      It's quite a distribution, neh?  We should probably consider some
      more optimizations specifically targeted to small maps.
      (This is an unscientific sample, and shifted by what we chose to
      focus on in testing, etc etc, but the general point stands.)
      
      `-1` is used to indicate "no idea" for size.  There's a small fix
      to the basicnode implementations to allow this.  A zero would work
      just as well in practice, but using a negative number as a hint to
      the human seems potentially useful.  It's a shame we can't make the
      argument optional; oh well.
      
      === codegen ===
      
      The codegen packages still all compile... but do nonsensical things,
      for the moment: they've not been updated to emit NodeAssembler.
      
      Since the output of codegen still isn't well rigged to test harnesses,
      this breakage is silent.
      
      The codegen packages will probably undergo a fairly tabula-rasa sweep
      in the near future.  There's been a lot of lessons learned since the
      start of the code currently there.  Updating to emit the NodeAssembler
      interface will be such a large endeavor it probably represents a good
      point to just do a fresh pass on the whole thing all at once.
      
      --------
      
      ... and that's all!
      
      Fun reading, eh?
      
      Please do forgive the refactors necessary for all this.  Truly, the
      performance improvements should make it all worth your while.
      4eb8c55c
  10. 29 Feb, 2020 2 commits
    • Eric Myhre's avatar
      Merge-ignore branch 'amortizing-iterators'. · eb71617f
      Eric Myhre authored
      See comments in that commit.  It's a good idea, but needs some further
      refinement in order to really shine.
      eb71617f
    • Eric Myhre's avatar
      Attempt to amortize getting of an iterator. · 7c595d69
      Eric Myhre authored
      It's possible, but this is a bit less trivial than it sounds...
      so I think this is one of those diffs I'm going to *save*, but
      via a "merge-ignore", and come back to the topic again later.
      
      Here's where we were at before this diff:
      
      ```
      BenchmarkSpec_Marshal_MapNStrMap3StrInt/n=0-8    3882726               308 ns/op             448 B/op          5 allocs/op
      BenchmarkSpec_Marshal_MapNStrMap3StrInt/n=1-8    1273840               961 ns/op             464 B/op          6 allocs/op
      BenchmarkSpec_Marshal_MapNStrMap3StrInt/n=2-8     690142              1785 ns/op             624 B/op          8 allocs/op
      BenchmarkSpec_Marshal_MapNStrMap3StrInt/n=4-8     376281              3197 ns/op             944 B/op         11 allocs/op
      BenchmarkSpec_Marshal_MapNStrMap3StrInt/n=8-8     197530              6084 ns/op            1584 B/op         16 allocs/op
      BenchmarkSpec_Marshal_MapNStrMap3StrInt/n=16-8    102693             11534 ns/op            2864 B/op         25 allocs/op
      BenchmarkSpec_Marshal_MapNStrMap3StrInt/n=32-8     54110             22601 ns/op            5424 B/op         42 allocs/op
      ```
      
      (This is still a very sizable improvement over the node implementations on master;
      about double the speed and a quarter of the allocs.  But still, could be improved?)
      
      With this diff adding iterator amortization (and a bench loop with timer pauses in it):
      
      ```
      BenchmarkSpec_Marshal_MapNStrMap3StrInt/n=0-8            1580808               778 ns/op             432 B/op          4 allocs/op
      BenchmarkSpec_Marshal_MapNStrMap3StrInt/n=1-8             744278              1574 ns/op             432 B/op          4 allocs/op
      BenchmarkSpec_Marshal_MapNStrMap3StrInt/n=2-8             468837              2529 ns/op             576 B/op          5 allocs/op
      BenchmarkSpec_Marshal_MapNStrMap3StrInt/n=4-8             277303              4171 ns/op             864 B/op          6 allocs/op
      BenchmarkSpec_Marshal_MapNStrMap3StrInt/n=8-8             172590              7187 ns/op            1440 B/op          7 allocs/op
      BenchmarkSpec_Marshal_MapNStrMap3StrInt/n=16-8             93064             12494 ns/op            2592 B/op          8 allocs/op
      BenchmarkSpec_Marshal_MapNStrMap3StrInt/n=32-8             52755             22671 ns/op            4896 B/op          9 allocs/op
      ```
      
      So... this is kind of an unexpected result.  We got the allocs to go down,
      as intended.  But speed didn't go up.  What gives?
      
      A priori, I've been running with the ballpark number of "88ns per alloc"
      (derived from some other observations on these packages today), which...
      would lead me to compute an expected gain in speed of about `88*(42-9)`
      given how many fewer allocations we see here... which in turn would lead
      to an expectation of about 12.8% speed improvement.
      Which... we don't see at all.  The speed is more or less unchanged.
      Hrm.
      
      I suspect that the reason for this is that having StartTimer/StopTimer
      calls in the middle of your `b.N` loop just is not possible to do without
      having serious skewing effects on results, and if we could clear away
      the skew, we'd probably see the expected improvement.
      
      But still, in light of this unclarity, I'm going to merge-ignore this.
      We can come back to this optimization later.
      After this reflection, I feel ~12% isn't worth chasing today,
      especially if it's a complicated 12%, and especially since we already
      have other much larger effects that are still in progress of getting
      properly landed and into use on the master branch.
      
      Another issue that deserves due consideration is how this change would
      interact with concurrent use of datastructures.  Since nodes are
      immutable, we *should* usually be able to expect that they can be read
      across threads without issue.  This patch would break that: getting
      two different iterators could be a race condition.  Fixing this is easy
      (I'd use https://golang.org/pkg/sync/atomic/#CompareAndSwapUint32),
      but raises the scope just a bit, and files this further as "not today".
      
      Iterators might also deserve a "Close" method in order to make this
      kind of optimization more accessible to the user; there's no reason it
      has to be once-only.  (This would also just so happen to fix the reason
      we needed StartTimer/StopTimer in the b.N loop, making measurement of
      the improvement easier.)
      
      Lots of things to consider when tackling this properly in the future!
      7c595d69