Cut akbasic_Runtime's static footprint from 10.75 MiB to 2.40 MiB #33

Closed
tachikoma wants to merge 3 commits from feature/reduce_memory_usage into main
7 changed files with 76 additions and 18 deletions
Showing only changes of commit 17af2d406c - Show all commits

View File

@@ -211,8 +211,12 @@ typedef struct akbasic_Runtime
* not exist relative to the working directory, which is what makes a `.bas`
* beside its art work from anywhere. Group F's disk verbs will want the
* same, which is why it is on the runtime rather than in the sprite state.
*
* Sized to AKBASIC_MAX_SOURCE_PATH_LENGTH, not AKBASIC_MAX_LINE_LENGTH: a
* directory is a filesystem path, not a line of BASIC, and the two do not
* belong to the same budget.
*/
char sourcepath[AKBASIC_MAX_LINE_LENGTH];
char sourcepath[AKBASIC_MAX_SOURCE_PATH_LENGTH];
/*
* The armed interrupts, and the environment the one currently running was
@@ -365,7 +369,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_set_ui(akbasic_Runtime *obj,
* @param path Path to the program file, or NULL for none.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
* @throws AKBASIC_ERR_BOUNDS When the path is longer than AKBASIC_MAX_LINE_LENGTH.
* @throws AKBASIC_ERR_BOUNDS When the path is longer than AKBASIC_MAX_SOURCE_PATH_LENGTH.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_set_source_path(akbasic_Runtime *obj, const char *path);

View File

@@ -23,9 +23,17 @@
* serves variables, functions and labels. Capacity is a member; the table is
* kept below a 75% load factor by construction because `capacity` counts slots
* and the caller's logical maximum is smaller.
*
* 172 rather than the old 256: no caller passes akbasic_symtab_init() anything
* larger than AKBASIC_MAX_VARIABLES (128), and 172 keeps that under the 75%
* load factor the comment above promises (128 / 0.75 = 170.7).
*
* AKBASIC_SYMTAB_MAX_KEY 24 rather than 64: the longest identifier across
* examples/breakout and examples/megademo -- variables, DIMmed arrays, labels
* and DEF FN names alike -- is 11 characters (TITLESCREEN, CLEARPOWERS).
*/
#define AKBASIC_SYMTAB_MAX_SLOTS 256
#define AKBASIC_SYMTAB_MAX_KEY 64
#define AKBASIC_SYMTAB_MAX_SLOTS 172
#define AKBASIC_SYMTAB_MAX_KEY 24
typedef struct
{

View File

@@ -6,6 +6,7 @@
#ifndef _AKBASIC_TYPES_H_
#define _AKBASIC_TYPES_H_
#include <limits.h>
#include <stdbool.h>
#include <stdint.h>
@@ -27,15 +28,39 @@
#define AKBASIC_MAX_VALUES 64
#define AKBASIC_MAX_VARIABLES 128
/* Whole-runtime pools */
#define AKBASIC_MAX_SOURCE_LINES 9999
#define AKBASIC_MAX_LINE_LENGTH 256
/*
* Whole-runtime pools.
*
* AKBASIC_MAX_SOURCE_LINES, AKBASIC_MAX_LINE_LENGTH, AKBASIC_MAX_ARRAY_VALUES,
* AKBASIC_MAX_ENVIRONMENTS and AKBASIC_MAX_FUNCTIONS were cut from their
* original values against measurements taken off examples/breakout and
* examples/megademo, the two most demanding programs this interpreter runs --
* see the memory-footprint discussion this commit's PR body links. Each is
* sized at roughly 1.5-2x the peak the reference corpus actually reaches, not
* at the peak itself.
*
* AKBASIC_MAX_LINE_LENGTH in particular follows Commodore BASIC's own 80-column
* line limit rather than a measurement, which is why sink_stdio.c now refuses a
* line that fills the buffer with no terminator instead of silently truncating
* it: at 256 bytes that failure mode was theoretical, and at 80 it is not.
*
* AKBASIC_MAX_SOURCE_PATH_LENGTH is its own constant rather than a reuse of
* AKBASIC_MAX_LINE_LENGTH, which is where it used to come from. `sourcepath`
* (runtime.h) holds a directory, not a line of BASIC, and the two ideas do not
* scale together: shrinking the line limit to 80 broke every golden test in
* this checkout, because this repository's own working directory is deeper
* than that. PATH_MAX is the actual bound a filesystem path is subject to, so
* it is the one this borrows.
*/
#define AKBASIC_MAX_SOURCE_LINES 2048
#define AKBASIC_MAX_LINE_LENGTH 80 /* Commodore BASIC's own line limit */
#define AKBASIC_MAX_SOURCE_PATH_LENGTH PATH_MAX
#define AKBASIC_MAX_ARRAY_DEPTH 64 /* dimensions per array */
#define AKBASIC_MAX_ARRAY_ELEMENTS 1024 /* elements in one array */
#define AKBASIC_MAX_ARRAY_VALUES 4096 /* array elements across all variables */
#define AKBASIC_MAX_ARRAY_VALUES 2048 /* array elements across all variables */
#define AKBASIC_MAX_STRING_LENGTH 256 /* see TODO.md 1.2 */
#define AKBASIC_MAX_ENVIRONMENTS 32 /* new: Go allocated these unbounded */
#define AKBASIC_MAX_FUNCTIONS 64 /* new: Go used an unbounded map */
#define AKBASIC_MAX_ENVIRONMENTS 12 /* new: Go allocated these unbounded */
#define AKBASIC_MAX_FUNCTIONS 8 /* new: Go used an unbounded map */
#define AKBASIC_MAX_LABELS 64 /* new: Go used an unbounded map */
/*
* Leaves a DO/LOOP condition may use. Its own small pool rather than a second

View File

@@ -13,12 +13,22 @@
#include <akerror.h>
#include <akbasic/symtab.h>
#include <akbasic/types.h>
#include <akbasic/value.h>
typedef struct
{
char name[AKBASIC_MAX_STRING_LENGTH];
/*
* Sized to AKBASIC_SYMTAB_MAX_KEY, not AKBASIC_MAX_STRING_LENGTH: this name
* only ever gets here by surviving akbasic_symtab_set() first
* (akbasic_environment_create() calls it right after this field is
* populated), and that call refuses anything AKBASIC_SYMTAB_MAX_KEY
* characters or longer with AKBASIC_ERR_BOUNDS. A variable whose name did
* not fit could never exist, so the wider buffer was 232 bytes of headroom
* nothing could ever put a byte into.
*/
char name[AKBASIC_SYMTAB_MAX_KEY];
akbasic_Type valuetype;
akbasic_Value *values; /** The pool, or `inlinevalue` for a scalar */
int valuecount;

View File

@@ -301,7 +301,7 @@ akerr_ErrorContext *akbasic_runtime_set_source_path(akbasic_Runtime *obj, const
}
FAIL_ZERO_RETURN(errctx, (length < sizeof(obj->sourcepath)), AKBASIC_ERR_BOUNDS,
"Program path of %zu characters exceeds the %d character limit",
length, AKBASIC_MAX_LINE_LENGTH - 1);
length, AKBASIC_MAX_SOURCE_PATH_LENGTH - 1);
PASS(errctx, aksl_memcpy(obj->sourcepath, path, length));
obj->sourcepath[length] = '\0';
SUCCEED_RETURN(errctx);

View File

@@ -74,6 +74,19 @@ static akerr_ErrorContext *stdio_readline(akbasic_TextSink *self, char *dest, si
if ( *eof ) {
SUCCEED_RETURN(errctx);
}
/*
* aksl_fgets(3)'s own contract: a full buffer with no trailing newline is
* how a caller spots a line longer than the buffer, because the rest of it
* is still sitting unread in the stream. Refusing here is what makes that
* true -- without it, the unread remainder is picked up by the *next*
* readline() as if it were its own statement, which does not fail, it just
* runs the wrong program. AKBASIC_MAX_LINE_LENGTH is small enough now that
* this is not a hypothetical: examples/breakout's own longest line used to
* clear the old 256-byte ceiling by more than half.
*/
FAIL_NONZERO_RETURN(errctx, (used == len - 1 && dest[used - 1] != '\n' && dest[used - 1] != '\r'),
AKBASIC_ERR_BOUNDS,
"Source line exceeds the %zu character limit", len - 1);
/*
* Strip the line terminator. The scanner treats \r and \n as end-of-line
* anyway, but leaving them on would make a stored source line differ from

View File

@@ -97,9 +97,9 @@ static void test_scoped_loop_counter_costs_nothing(void)
/**
* @brief The pool is genuinely untouched, not merely large enough.
*
* Two hundred scope entries and then the pool's *entire* width -- four arrays,
* Two hundred scope entries and then the pool's *entire* width -- two arrays,
* because AKBASIC_MAX_ARRAY_ELEMENTS caps any one of them at 1024 while the pool
* holds 4096. One leaked slot and the fourth `DIM` has nowhere to go, which
* holds 2048. One leaked slot and the second `DIM` has nowhere to go, which
* makes this the sharpest of the three and by far the cheapest: the two above
* prove a program finishes, this proves nothing at all was spent. The long ones
* stay because they are the shape the defect was found in.
@@ -111,10 +111,8 @@ static void test_pool_is_untouched_by_scopes(void)
"30 NEXT T#\n"
"40 DIM A#(1024)\n"
"50 DIM B#(1024)\n"
"60 DIM C#(1024)\n"
"70 DIM D#(1024)\n"
"80 D#(1023) = 7\n"
"90 PRINT D#(1023)\n"
"70 B#(1023) = 7\n"
"90 PRINT B#(1023)\n"
"100 END\n"
"110 LABEL SUBA\n"
"120 LOC# = 1\n"