Skip to contentSkip to Content
DocsUser GuideMemory Management

Memory Management

SPIRES is a C library, and all memory management is explicit. There is no garbage collector, no reference counting, and no automatic cleanup. Understanding the ownership rules for every allocation is essential to writing correct SPIRES programs.

This page documents the ownership model for every SPIRES type and function that allocates memory.

Ownership Principles

SPIRES follows three consistent rules:

  1. The library allocates, the library frees: Objects created by spires_reservoir_create() must be destroyed by spires_reservoir_destroy(). Never call free() on a reservoir pointer.

  2. Returned arrays are caller-owned: Functions that return double * (such as spires_run() and spires_copy_reservoir_state()) allocate the return buffer with malloc(). The caller owns this memory and must call free() when done.

  3. Input parameters are caller-owned: Pointers passed into SPIRES functions (such as neuron_params, input arrays, and target arrays) remain owned by the caller. SPIRES reads from them but does not free them, retain them, or modify them.

Reservoir Lifecycle

Creation

spires_reservoir *r = NULL; spires_status s = spires_reservoir_create(&cfg, &r);

spires_reservoir_create() allocates all internal memory for the reservoir: weight matrices, neuron states, history buffers (for fractional neurons), and metadata. On success, r points to a fully initialized reservoir. On failure, r is NULL and no memory has been leaked.

The cfg struct is read during creation but not retained. You may modify or discard cfg after spires_reservoir_create() returns.

Destruction

spires_reservoir_destroy(r);

spires_reservoir_destroy() frees all memory associated with the reservoir, including internal weight matrices, state arrays, and history buffers. After this call, r is a dangling pointer. Do not dereference it.

It is safe to pass NULL to spires_reservoir_destroy() — it is a no-op.

Reset

spires_reservoir_reset(r);

spires_reservoir_reset() zeroes out the neuron states and history buffers without freeing or reallocating any memory. The reservoir retains its weight matrices and trained readout weights. This is used to restart the reservoir dynamics from a clean initial state without the cost of destroying and recreating the reservoir.

Functions That Allocate

spires_run()

double *predictions = spires_run(r, input_series, series_length);

Returns a malloc-allocated array of length series_length * num_outputs. The caller must free this array:

/* Use predictions */ for (int i = 0; i < series_length; i++) { printf("%.4f\n", predictions[i]); } /* Free when done */ free(predictions);

If the function fails (e.g., due to invalid arguments or allocation failure), it returns NULL. Always check the return value.

spires_copy_reservoir_state()

double *state = spires_copy_reservoir_state(r);

Returns a malloc-allocated array of length num_neurons containing a snapshot of the current membrane potentials. The caller must free this array:

/* Inspect the state */ for (int i = 0; i < num_neurons; i++) { printf("Neuron %d: %.4f mV\n", i, state[i]); } /* Free when done */ free(state);

Returns NULL on failure.

spires_compute_output()

double *output = spires_compute_output(r);

Returns a malloc-allocated array of length num_outputs containing the current readout output (computed from the current reservoir state and the trained weights). The caller must free this array:

free(output);

Functions That Do Not Allocate

spires_read_reservoir_state()

double buffer[500]; /* caller-provided buffer */ spires_status s = spires_read_reservoir_state(r, buffer, 500);

This function writes the current reservoir state into a caller-provided buffer. No allocation is performed by SPIRES. The caller is responsible for ensuring the buffer is large enough (at least num_neurons elements).

This is the preferred function for performance-critical code that reads the state frequently, as it avoids repeated malloc/free cycles.

spires_step()

spires_status s = spires_step(r, input_vector);

Advances the reservoir by one time step. No memory is allocated. The input_vector is read but not retained.

spires_train_ridge() and spires_train_online()

spires_status s = spires_train_ridge(r, input, target, length, lambda); spires_status s = spires_train_online(r, input, target, length, eta);

These functions allocate temporary internal memory during training (for the state matrix in ridge regression, or for intermediate computations) but free it before returning. The only persistent side effect is the updated readout weights stored inside the reservoir.

The input and target arrays are read but not retained. The caller retains ownership.

The neuron_params Pointer

The neuron_params field in spires_reservoir_config is a void * pointer to a caller-allocated parameter struct:

spires_flif_params params = { .alpha = 0.5, .tau_m = 20.0, /* ... */ }; cfg.neuron_params = &params; spires_reservoir_create(&cfg, &r); /* params can now be modified, reused, or freed */ /* SPIRES has copied the values it needs */

SPIRES reads the pointed-to struct during spires_reservoir_create() and copies the values into its internal data structures. After creation, the caller may do whatever it wants with the original struct — it is not referenced again.

If neuron_params is NULL, SPIRES uses built-in default parameters.

Common Patterns

Basic Lifecycle

spires_reservoir *r = NULL; spires_status s = spires_reservoir_create(&cfg, &r); if (s != SPIRES_OK) { /* handle error */ } /* Use the reservoir */ s = spires_train_ridge(r, input, target, len, lambda); double *pred = spires_run(r, test_input, test_len); /* Clean up */ free(pred); spires_reservoir_destroy(r);

Multiple Runs with Reset

spires_reservoir *r = NULL; spires_reservoir_create(&cfg, &r); for (int trial = 0; trial < 10; trial++) { spires_reservoir_reset(r); /* fresh state, same weights */ double *pred = spires_run(r, test_input, test_len); /* evaluate trial */ free(pred); /* free each prediction buffer */ } spires_reservoir_destroy(r);

Reading State Without Allocation

int N = spires_num_neurons(r); double *buf = malloc(N * sizeof(double)); for (int t = 0; t < time_steps; t++) { spires_step(r, &input[t]); spires_read_reservoir_state(r, buf, N); /* process buf without allocation overhead */ } free(buf); /* free once at the end */

Memory Leak Checklist

When debugging memory issues in a SPIRES program, verify:

  • Every spires_reservoir_create() has a matching spires_reservoir_destroy().
  • Every non-NULL return from spires_run() is passed to free().
  • Every non-NULL return from spires_copy_reservoir_state() is passed to free().
  • Every non-NULL return from spires_compute_output() is passed to free().
  • Error paths destroy the reservoir and free any allocated buffers before returning.

Tools such as Valgrind (valgrind --leak-check=full ./my_program) or AddressSanitizer (gcc -fsanitize=address) can detect leaks at runtime.


← Scoring Metrics | Parallelism →

Last updated on