IGNORE's per-TU static akerr_last_ignored costs 37 KB of TLS per translation unit #37

Open
opened 2026-08-05 22:59:51 -04:00 by tachikoma · 2 comments
Collaborator

Measured while bumping akbasic from libakerror 2.0.1 to the current main (ee38eed).

include/akerror.tmpl.h:219 defines the ignored-error snapshot as a file-scope object in the public header:

static AKERR_THREAD_LOCAL akerr_ErrorContext akerr_last_ignored;

Because it is static, every translation unit that includes akerror.h gets its own copy, and because it is AKERR_THREAD_LOCAL every copy is thread-local storage. akerr_ErrorContext is large — AKERR_MAX_ERROR_FNAME_LENGTH defaults to 4096 and there are two such fields — so each copy is substantial.

Measured

sizeof(akerr_ErrorContext) = 37,296 bytes at default options.

akbasic's basic driver, default configuration, Debug:

TLS segment
libakerror 2.0.1 (__akerr_last_ignored, an extern pointer) no TLS segment at all
libakerror main 0x15a020 = 1,417,248 bytes

readelf -sW build/basic shows 38 akerr_last_ignored symbols; 38 x 37,296 = 1,417,248 exactly. The entire TLS segment is this one variable, duplicated once per TU, and it scales with translation-unit count rather than with anything about the program.

That is ~1.35 MiB of per-thread storage in a project that recently spent a release cutting its interpreter's static footprint from 10.75 MiB to 2.40 MiB, and it would be paid again by every thread a host game spawns.

Second, smaller symptom

A static object defined in a header and unused in most TUs warns. The same build emits 84 instances of:

akerror.h:219:46: warning: 'akerr_last_ignored' defined but not used [-Wunused-variable]

one per TU that includes the header without using IGNORE. Consumers building with -Werror cannot include akerror.h.

Why it is written this way

I think the snapshot is deliberate and correct: 4fe7571 ("Release ignored error contexts") made IGNORE copy the context so the pool slot can be released, which fixes a real leak. The issue is only where the copy lives — one per TU rather than one per thread.

Suggested direction

Declare it extern AKERR_THREAD_LOCAL akerr_ErrorContext akerr_last_ignored; in the header and define it once in src/error.c, which restores one copy per thread and silences the warning. If it must stay in the header, AKERR_MAYBE_UNUSED/__attribute__((unused)) fixes the warning but not the footprint.

Tests that would cover it

  • A test asserting the TLS segment of a two-TU program does not grow with the second TU.
  • A compile of a TU that includes akerror.h and never calls IGNORE, under -Wall -Wextra -Werror. tests/negative/ is the existing shape for a compile that must behave a particular way.
Measured while bumping `akbasic` from libakerror 2.0.1 to the current `main` (ee38eed). `include/akerror.tmpl.h:219` defines the ignored-error snapshot as a file-scope object in the **public** header: ```c static AKERR_THREAD_LOCAL akerr_ErrorContext akerr_last_ignored; ``` Because it is `static`, every translation unit that includes `akerror.h` gets its own copy, and because it is `AKERR_THREAD_LOCAL` every copy is thread-local storage. `akerr_ErrorContext` is large — `AKERR_MAX_ERROR_FNAME_LENGTH` defaults to 4096 and there are two such fields — so each copy is substantial. ## Measured `sizeof(akerr_ErrorContext)` = **37,296 bytes** at default options. akbasic's `basic` driver, default configuration, Debug: | | TLS segment | |---|---| | libakerror 2.0.1 (`__akerr_last_ignored`, an `extern` pointer) | no TLS segment at all | | libakerror `main` | `0x15a020` = **1,417,248 bytes** | `readelf -sW build/basic` shows **38** `akerr_last_ignored` symbols; 38 x 37,296 = 1,417,248 exactly. The entire TLS segment is this one variable, duplicated once per TU, and it scales with translation-unit count rather than with anything about the program. That is ~1.35 MiB of per-thread storage in a project that recently spent a release cutting its interpreter's static footprint from 10.75 MiB to 2.40 MiB, and it would be paid again by every thread a host game spawns. ## Second, smaller symptom A `static` object defined in a header and unused in most TUs warns. The same build emits **84** instances of: ``` akerror.h:219:46: warning: 'akerr_last_ignored' defined but not used [-Wunused-variable] ``` one per TU that includes the header without using `IGNORE`. Consumers building with `-Werror` cannot include `akerror.h`. ## Why it is written this way I think the snapshot is deliberate and correct: 4fe7571 ("Release ignored error contexts") made `IGNORE` copy the context so the pool slot can be released, which fixes a real leak. The issue is only *where the copy lives* — one per TU rather than one per thread. ## Suggested direction Declare it `extern AKERR_THREAD_LOCAL akerr_ErrorContext akerr_last_ignored;` in the header and define it once in `src/error.c`, which restores one copy per thread and silences the warning. If it must stay in the header, `AKERR_MAYBE_UNUSED`/`__attribute__((unused))` fixes the warning but not the footprint. ## Tests that would cover it - A test asserting the TLS segment of a two-TU program does not grow with the second TU. - A compile of a TU that includes `akerror.h` and never calls `IGNORE`, under `-Wall -Wextra -Werror`. `tests/negative/` is the existing shape for a compile that must behave a particular way.
andrew added the status::ready label 2026-08-05 23:20:11 -04:00
logikoma was assigned by andrew 2026-08-05 23:20:17 -04:00
Collaborator

Beginning work on issue #37: I will move the ignored-error TLS snapshot to a single library definition, add focused coverage for multi-TU storage and warning behavior, then open a signed pull request.

Beginning work on issue #37: I will move the ignored-error TLS snapshot to a single library definition, add focused coverage for multi-TU storage and warning behavior, then open a signed pull request.
logikoma added status::review and removed status::ready labels 2026-08-05 23:37:10 -04:00
Collaborator

Work complete. Moved akerr_last_ignored from the public header’s static TLS definition to an extern declaration with one definition in src/error.c. Added a two-translation-unit regression test compiled with -Wall -Wextra -Werror; err_ignore and err_ignore_multitu both pass. Pull request: #40

Work complete. Moved akerr_last_ignored from the public header’s static TLS definition to an extern declaration with one definition in src/error.c. Added a two-translation-unit regression test compiled with -Wall -Wextra -Werror; err_ignore and err_ignore_multitu both pass. Pull request: https://source.starfort.tech/andrew/libakerror/pulls/40
Sign in to join this conversation.