Make AssocTable cache initial capacity configurable (#54)#238
Conversation
AssocTable's lookup cache was created as Dictionary<_,_>(2000). Two AssocTables (action + goto) are constructed on every Tables.Interpret call, so a parser invoked over many small inputs pre-allocated two ~2000-entry dictionaries per parse that never came close to filling — dominating parse-time allocation. Expose ParseSettings.AssocTableCacheInitialCapacity (default 2000, so existing behaviour is unchanged) and use it for the cache's initial capacity. Setting it to 0 grows the cache on demand, allocating only what a parse actually uses. Addresses the allocation half of fsprojects#54 without changing default behaviour.
nojaf
left a comment
There was a problem hiding this comment.
This looks reasonable, could you give an example of how end-users configure this?
| @@ -1,5 +1,6 @@ | |||
| #### 11.3.0 - Unreleased | |||
| * Add Fable support to FsLexYacc.Runtime. | |||
| * Make the AssocTable lookup cache initial capacity configurable to avoid pre-allocating 2000 entries per parse #54 | |||
There was a problem hiding this comment.
https://www.nuget.org/packages/FsLexYacc/11.3.0 does appear to be released, could you make this into 11.4.0
There was a problem hiding this comment.
Good catch — bumped to 11.4.0
Sure
open FSharp.Text.Parsing
[<EntryPoint>]
let main argv =
// We parse many small inputs, so grow the AssocTable cache on demand
// instead of pre-allocating 2000 entries per parse.
ParseSettings.AssocTableCacheInitialCapacity <- 0
// ... run the app / parser ...
0
Typical use is an app that parses many small inputs over its lifetime (editor, linter, a service parsing snippets), where the per-parse 2000-entry pre-allocation dominates. Leaving it unset keeps today's behaviour for grammars that parse a few large inputs. |
|
Makes me wonder if the cli app should have an option to set this as well. |
I will look into it |
|
@dsyme or @sergey-tihon could you update the key in FsLexYacc/.github/workflows/push-main.yml Line 33 in 4d9ac06 |
…jects#54) Add a Tables.Interpret overload that takes an explicit initial capacity for the per-parse AssocTable lookup caches, threading it through the interpreter (AssocTable now receives the capacity as a constructor argument). The existing 3-argument Interpret keeps using ParseSettings.AssocTableCacheInitialCapacity, so this is non-breaking — existing generated parsers are unaffected. This is the runtime support for letting fsyacc bake a per-parser capacity into generated code (follows in a separate commit).
Wire the runtime AssocTable cache-capacity control (issue fsprojects#54) through to the fsyacc command line, since fslex/fsyacc are most commonly driven via CLI from MSBuild rather than by setting the runtime knob in code. - fsyacc.fs: new `--assoc-cache-capacity <int>` arg, forwarded into GeneratorState.assocCacheCapacity. - fsyaccdriver.fs: when the capacity is set, emit the 4-arg Interpret overload; otherwise emit the parameterless overload (unchanged default behaviour). - FsYacc.Core.Tests: codegen tests asserting the generated engine line uses the capacity-carrying overload when the flag is given and the default overload when it is not. Sequenced, since fsyacc's spec compiler uses process-global state. Usage from MSBuild: <OtherFlags>--assoc-cache-capacity 0</OtherFlags>
The AssocTable cache initial capacity (fsprojects#54) is a pure allocation-tuning knob and must never change what a parser produces. Extend the JSON example smoke test to parse each input at the historical default (2000) and at capacities 0, 1, 64 and 100000, asserting identical results and failing with a nonzero exit otherwise. This exercises the runtime interpreter that both the default path and the --assoc-cache-capacity path funnel into (Parser.start reads ParseSettings.AssocTableCacheInitialCapacity), complementing the codegen-only assertions in FsYacc.Core.Tests.
|
CLI option added |
|
We should set up trusted publishing here, I'll work on it |
|
@sergey-tihon thanks, I'm going to merge this and hope for the best. |
|
@sergey-tihon no luck
|
|
@nojaf Sorry, I wasn’t aware of the FsYacc.Core package. The API key has now been updated, and all packages have been successfully published using the workflow.
|
|
Thx Sergey! |

Addresses the allocation half of #54.
Problem
AssocTable's lookup cache is created asDictionary<_,_>(2000). TwoAssocTables (action + goto) are constructed insideTables.Interpreton every parse, so a parser invoked over many small inputs pre-allocates two ~2000-entry dictionaries per parse that never come close to filling — this dominates parse-time allocation.What changed
This makes the initial cache capacity configurable, defaulting to the historical 2000 so existing behaviour is unchanged. Correctness is unaffected — only the cache's initial capacity changes. It is purely opt-in, at three layers:
ParseSettings.AssocTableCacheInitialCapacity(default2000) sets the capacity process-wide, and a newTables.Interpretoverload takes an explicit capacity that overrides the global for a single parser. Setting the capacity to0grows the cache on demand, allocating only what a parse actually uses.--assoc-cache-capacity <int>option makes the generated parser call the capacity-carrying overload. Omitting the flag emits the existing parameterless overload, so generated output is unchanged by default.11.4.0(bumped from11.3.0, which is already released).Public API additions (non-breaking)
Both are additive — the existing 3-argument
Interpretand the2000default are untouched, so current callers and previously generated parsers are unaffected.Usage
From MSBuild, grow the cache on demand instead of pre-allocating 2000 entries:
Or set it process-wide at startup:
ParseSettings.AssocTableCacheInitialCapacity <- 0.Evidence
Measured on a downstream F#/APL analyzer whose grammar is parsed once per statement (so the per-parse fixed cost is paid heavily), opting in with capacity 0:
Dictionary<int,int>was ~95% of allocation, via a GC-AllocationTick profile).Testing
FsYacc.Core.Tests) — asserts fsyacc emits the 4-argumentInterpretoverload when--assoc-cache-capacityis given, and the parameterless overload when it is not.JsonLexAndYaccExample) — parses each sample input at the historical default (2000) and at capacities 0, 1, 64 and 100000, and fails if any result differs. This is the key safety property: the capacity is an allocation-tuning knob and must never change what a parser produces.I kept the default at 2000 so this is non-breaking; happy to instead default to 0 (grow-on-demand) if you'd prefer that as the out-of-the-box behaviour.