Cut akbasic_Runtime's static footprint from 10.75 MiB to 2.40 MiB #33
Reference in New Issue
Block a user
Delete Branch "feature/reduce_memory_usage"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
sizeof(akbasic_Runtime)goes from 11,270,976 bytes (10.75 MiB) to2,512,048 bytes (2.40 MiB) — a 4.49x reduction — by tightening seven
AKBASIC_MAX_*/AKBASIC_SYMTAB_MAX_*pool constants and one field's type,sized against measured use rather than guessed. Numbers were taken by
compiling a
sizeof()probe against the real headers and cross-checkedagainst
nm -Son the actual linkedbasicbinary'sRUNTIME/SCRIPTsymbol, not estimated.
Methodology
Nothing in this interpreter
mallocs; every pool is a fixed array bounded bya compile-time constant, so the runtime's memory cost is static and knowable.
Two example programs —
examples/breakout/sprites/breakout.bas(1343 lines)and
examples/megademo/megademo.bas(1573 lines) — are the most demandingBASIC programs in this repository, so they were used as the measuring stick:
static analysis of source structure (line counts, identifier lengths, GOSUB
call-graph depth via a DAG walk) plus dynamic high-water-mark instrumentation
(temporarily patched into
env_acquire()/akbasic_runtime_new_variable(),reverted before committing) run headless under
SDL_VIDEODRIVER=dummyfor both programs.AKBASIC_MAX_ENVIRONMENTSAKBASIC_MAX_FUNCTIONSDEF FNAKBASIC_MAX_ARRAY_VALUESAKBASIC_MAX_SOURCE_LINESAKBASIC_SYMTAB_MAX_SLOTSAKBASIC_SYMTAB_MAX_KEYAKBASIC_MAX_LINE_LENGTHAKBASIC_MAX_VARIABLES(128) is deliberately untouched: breakout alonereaches 121 of 128 concurrent named variables, the least headroom of any pool
measured, and shrinking it would be a real risk rather than a free win.
akbasic_Variable.namemoves fromAKBASIC_MAX_STRING_LENGTH(256) toAKBASIC_SYMTAB_MAX_KEY. This one isn't a measurement, it's a correctnessfix: every variable name is registered with
akbasic_symtab_set()immediately after this field is populated
(
akbasic_environment_create(),src/environment.c), and that call alreadyrefuses anything
AKBASIC_SYMTAB_MAX_KEYcharacters or longer withAKBASIC_ERR_BOUNDS. No variable with a longer name could ever exist, so thewider field was 232 bytes per variable (128 variables) nothing could reach.
Two defects this surfaced, both fixed here
sourcepathwas borrowingAKBASIC_MAX_LINE_LENGTHby accident. It holdsa directory (for resolving relative asset paths like
SPRSAV), not a lineof BASIC — and this checkout's own test paths are 81+ characters deep, which
broke every golden test's ability to even load until this was split into its
own
AKBASIC_MAX_SOURCE_PATH_LENGTH, backed byPATH_MAXthe waylibakerroralready sizes its own path buffers.src/sink_stdio.c'sstdio_readline()didn't checkaksl_fgets()'s owndocumented contract. A full buffer with no trailing newline means the line
was longer than the buffer and the remainder is still in the stream —
aksl_fgets()'s doc comment says so explicitly. Unchecked, the nextreadline()call picks that remainder up as its own statement: a programdoesn't fail to load, it silently becomes a different, wrong program. I
found this by loading
breakout.bas(which has an 87-character line) afterthe
libakstdlib-26port landed and getting a garbled downstream parse errorinstead of a clean bounds error. At the old 256-byte ceiling this was
theoretical. At 80 it is not, so
stdio_readline()now refuses loudlyinstead of truncating silently — this is also what makes the
AKBASIC_MAX_LINE_LENGTHdrop safe rather than just smaller.tests/value_pool.c'stest_pool_is_untouched_by_scopes()was pinned to theold 4×1024=4096 pool arithmetic (four max-size arrays proving nothing leaked
across 200 GOSUB scopes); rewritten to 2×1024=2048 for the same proof against
the new
AKBASIC_MAX_ARRAY_VALUES.Known consequence — read before merging
AKBASIC_MAX_LINE_LENGTH=80follows Commodore BASIC's own column limit, nota measurement, and it breaks real content:
tests/reference/language/functions/mod.bas:4andtests/reference/language/flowcontrol/nestedforloopwaitingforcommand.bas:3are both 82 characters.
MAINTENANCE.mdandCMakeLists.txt:585areexplicit that
tests/reference/is a byte-identical copy of the Goreference's own corpus and is never edited to suit this interpreter — these
two golden cases cannot be fixed by shortening them, and fail outright.
tests/language/cases (this project's own local corpus — see the fulllist in andrew/akbasic#32) and one line in
docs/18-tutorial-breakout-artwork.mdare in the same position but arecontent this project owns.
examples/breakoutandexamples/megademo(not covered by CI) also havelines well over 80 characters and will need rework to load under this
build.
This was a deliberate, explicit call, not an oversight — I raised the
tests/reference/conflict before pushing, laid out the alternatives (raisethe limit to clear the corpus with margin, e.g. 96 or 128; rewrite the
editable content and gate
tests/reference/out of the golden-case loop;or carry a documented exception to the never-edit rule), and was told to ship
80 anyway. Filed as andrew/akbasic#32 with the full failing-test list,
the measured floor (83, with zero margin), and the tradeoffs, rather than
silently worked around here.
Currently failing under this branch:
docs_examples,golden_language_flowcontrol_nestedforloopwaitingforcommand,golden_language_functions_mod, and 12local_*cases — 97/112 passing.Full list and context in #32.
Verification
844ebeef22to46cb4549fe46cb4549feto1e514f679bBuild fix pushed as
bd63e3e.Validation: core build succeeded; ctest passes 109/109 tests. The AKGL-only job was not run locally because nested SDL dependency cloning stalled, but all affected source lines are below the interpreter limit.
bd63e3e9fatoeb93bb7da0Pull request closed