Skip to content

size: the micro stack caches no name and fragments no packet - #120

Closed
tinic wants to merge 1 commit into
mainfrom
size/minimal-micro-step1
Closed

tinic wants to merge 1 commit into
mainfrom
size/minimal-micro-step1

Conversation

@tinic

@tinic tinic commented Sep 30, 2026

Copy link
Copy Markdown
Owner

Base 994aa0ed, candidate 76806432, one commit, CHANGELOG.md in it.

The minimal and micro drawers were both over their bsdsocket.library
budget. Both are now under, by taking options the drawer already promises
away, plus the shim work one of them needs to be honest about it.

arm before after budget what
minimal 241,288 238,060 239,000 AMINETXDUO_HOT_O2=OFF (-3,228)
micro 206,856 201,672 205,000 resolver cache off (-3,236) + fragmenter off (-1,892)
default 368,828 368,828 366,000 untouched

Measured one option apart from the same tree at 994aa0ed on the pinned
toolchain (16.2.0b), cmake --preset <arm> -B <dir>, --target release_libraries. Full drawer is still 2,828 over and stays that way here:
it is shared shim code, not a profile feature. anxgenet.device (+96) and
usergroup.library (+172) are over in all three arms before this change as
after.

micro: no answer cache. gethostbyname()/gethostbyaddr() ask the name
server every time rather than once per TTL. AMINETXDUO_DNS_CACHE=OFF leaves
NX_DNS_CACHE_ENABLE undefined, which drops the table and the lookup code from
nx_dns.c; netstack_dns.c guards its nx_dns_cache_initialize() on the same
macro, so nothing else moves.

micro: no fragmenter. AMINETXDUO_IP_FRAGMENTATION=OFF takes the transmit
splitter and the receive reassembler out together (NetX Duo has no
receive-only arm). This one needs shim work, and that is most of the diff:

  • With no fragmenter to hand an oversize packet to, nx_ip_driver_packet_send()
    releases it and returns NX_SUCCESS, so a sender is told the bytes went.
    bsd_nofrag_maxdgram() (src/bsdsocket/transfer.c) caps UDP and raw sends at
    the smallest attached interface MTU less the headers and refuses the rest with
    EMSGSIZE. The egress interface is not this layer's to know, so the smallest
    MTU is exact for the one-interface drawer that takes this and wrong in the safe
    direction for a multi-interface build.
  • nx_ip_fragment_enable() answers NX_NOT_ENABLED rather than failing, so
    src/netstack/netstack.c skips the call and its warning instead of logging a
    fault at every stack start.
  • TCP is unaffected in both directions: a segment's size follows the MTU.

CROSS_CONFIGS gains a nofrag arm, because AMINETXDUO_IP_FRAGMENTATION=OFF
for four interfaces with IPv6 on is the only build that compiles the shim's
smallest-MTU loop and its IPv6 header arithmetic. Gate set: image size,
ram size, option coverage, shipping config, changelog prose, ci arm coverage and
stage coverage all pass; the new arm builds clean.

minimal: the hot -O2 list off. Eight profiled NetX Duo translation units
built at -O2 after the tree's -Os. Turning that off pays the drawer's whole
overshoot on its own. The full drawer keeps the list, since nothing in this tree
has ever measured what it buys and the full drawer is where it was aimed.

The rest of the compiler-flag space was swept the same day and is empty; the
numbers and the reasoning are written into CMakeLists.txt next to the flags.
-Oz and -fomit-frame-pointer are each worth exactly nothing (0 bytes,
byte-identical output), -msmall-code is 800 bytes worse, -fbaserel does not
link, and there is no floating-point code in any image. -mregparm is 11,800
bytes smaller and unusable as a flag -- it moves the convention for every call
including leads out to Exec and SANA-II -- but __attribute__((regparm(N))) is
selective and is a separate, larger piece of work, deliberately not in this PR.

Two gates are red on 994aa0ed and stay red here, both untouched by this
change and both verified against a pristine checkout of the base:
tools/aminet-survey/check-derived.sh (own-tools.tsv no longer matches
src/tools/src/config), and tools/check-stage-freshness.sh (every workflow
behind every stage has zero successes in its last 20 runs, so the local host
stage stops before ctest).

🤖 Generated with Claude Code

Three size decisions for the 2026-09-30 pass, each one option apart from
the same tree at 994aa0e and measured on the pinned toolchain:

  micro    -5,184 bytes of bsdsocket.library (206,856 -> 201,672) and
           2,072 bytes of resident AmiNetStack
             AMINETXDUO_DNS_CACHE=OFF         -3,236 alone
             AMINETXDUO_IP_FRAGMENTATION=OFF  -1,892 alone
             both                             -5,184
  minimal  -3,228 bytes (241,288 -> 238,060): AMINETXDUO_HOT_O2=OFF, which
           pays that drawer's whole overshoot.  The full drawer keeps the
           hot list -- nothing in this tree has ever measured what it buys,
           and the full drawer is where it was aimed.
  micro    is 201,672 against a 205,000 budget, minimal 238,060 against
           239,000.  The full drawer's 2,828-byte overshoot is untouched:
           it is shared shim code, not a profile feature.

The resolver keeps no answer cache in micro, so gethostbyname() and
gethostbyaddr() ask the name server every time instead of once per TTL.
That is a size decision with a price on a slow link, and it is the one
drawer that pays it.

Fragmentation off takes the transmit splitter and the receive reassembler
out together, because NetX Duo has no receive-only arm.  It needs shim
work to be honest: with no fragmenter to hand an oversize packet to,
nx_ip_driver_packet_send() releases it and returns NX_SUCCESS, so the
sender is told the bytes went.  bsd_nofrag_maxdgram() (transfer.c) caps
UDP and raw sends at the smallest attached interface MTU less the headers
and refuses the rest with EMSGSIZE, and src/netstack/netstack.c skips
nx_ip_fragment_enable() rather than reporting the NX_NOT_ENABLED it now
answers as a fault at every stack start.  TCP is unaffected in both
directions: a segment's size follows the MTU.

Two options and the micro preset carry it; the guide and the changelog say
what the user loses.  CROSS_CONFIGS gets a nofrag arm because that
combination builds the shim's smallest-MTU loop for four interfaces with
IPv6 on, which no other arm compiles.

The full flag space was swept the same day and is empty; the results,
including why -mregparm is unusable as a flag though the attribute form is
not, are in CMakeLists.txt.  -Oz and -fomit-frame-pointer are each worth
exactly nothing, -msmall-code is 800 bytes worse, and -fbaserel does not
link.  There is no floating-point code in any image.

Not addressed here: anxgenet.device (29,096 against 29,000) and
usergroup.library (10,172 against 10,000) are over their budgets in all
three arms, before this change as after it.

Co-Authored-By: Claude Code <noreply@anthropic.com>
tinic added a commit that referenced this pull request Sep 30, 2026
bsd_stack_ip(), bsd_stack_pool(), bsd_stack_last_opener(), bsd_owner_elect()
and bsd_owner_drop() were `static inline` in bsdsocket_internal.h, so every
translation unit that touches a base carried its own copy: -O2 inlines a
function called once in a TU whatever its size, and bsd_stack_ip() alone has
53 call sites across 16 translation units off LTO.  They are shared code with
one implementation, so they are one implementation, in base.c, called.

Called is not expensive here.  Each takes at most two arguments, and each is
marked regparm(2), so they arrive in registers rather than on the stack.  On
this target the convention is address registers for pointers and data
registers for integers -- the caller emits `movea.l 8(sp),a0` / `jsr
_bsd_stack_ip`, with nothing pushed.

The attribute is part of the function type, so it is on the declarations in
the header and on the definitions in base.c: a caller that does not see it
pushes its arguments into a callee reading registers, which is a miscompile
and not a warning.  It is also a target attribute -- m68k implements it, host
x86-64 gcc and clang do not -- so BSD_REGPARM2 expands to nothing off the
Amiga and the host tier compiles base.c unchanged.

bsdsocket.library, origin/main c3bb63c, m68k-amigaos-gcc 16.2.0b, -flto:

    arm        before     out of line   + regparm(2)
    default    368,896         --         368,608    -288
    minimal    241,320       241,148      241,016    -304
    micro      206,860       206,712      206,608    -252

Roughly 100-130 bytes of that is the attribute itself on these five calls
(micro -104, minimal -132); the rest is one copy instead of one per
translation unit.  This is a size lever of its own and does not touch the
default drawer's overshoot, which is shared shim code.  The branch is on
origin/main alone and does not carry the option changes in #120, so minimal's
baseline here is still 2,320 over its 239,000 budget.

The host tests that compile the callers get base.c: handoff builds and passes
(53 checks, 0 failures), and expunge and expunge_cork fail to link on
origin/main already, for two symbols out of library.c this does not touch --
the undefined references are identical before and after.
@tinic

tinic commented Oct 1, 2026

Copy link
Copy Markdown
Owner Author

Closing as superseded by current main. The verified f2d14df release-candidate archive contains bsdsocket.library at 346,336 bytes (full), 226,404 (minimal), and 194,216 (micro), all below this PR’s respective 366,000 / 239,000 / 205,000 limits without removing DNS caching or fragmentation. This branch is now far behind main with conflicts and failing checks; its tip is preserved for recovery.

@tinic tinic closed this Oct 1, 2026
@tinic
tinic deleted the size/minimal-micro-step1 branch October 1, 2026 04:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant