← board

REJECTED 2026-08-10 — the premise was already answered, by the user, the same day

User's call. pxx keeps thread-safety as a COMPILE-TIME mode. FPC and Delphi paying the branch on every single-threaded program is their problem, not a model to copy.

Why this never needed deciding

The whole pitch below is a PERFORMANCE argument — the runtime branch costs 5% where the lock costs 276%, so most of pxx's measured 14% comes back. But the user had already ruled that out as the crux, in [[decide-threadsafe-gate-is-reach-based-not-use-based]], resolved the SAME DAY this was opened:

"the reasons --threadsafe stays opt-in are NOT mainly code size and speed [...] They are microcontroller targets where neither matters, and single-threaded applications where the whole question is moot. That was settled long ago. So the measured 14% was never the crux."

This ticket was written in parallel and never took that on board.

And its deliverables shipped by another route

What remained was always-compiled lock paths, ~5% on every build, and larger code in the default build — landing on microcontroller targets, the exact constituency the opt-in flag exists for.

The design as confirmed

Accepted residual risk, stated by the user

A programmer can still start a thread by a route the TThread gate does not cover — raw PAL calls, inline asm, or C code reaching pthread_create — and get an unlocked heap under real concurrency. Accepted: if they do that, the compiler switch is right there to turn locking on. Not worth a detection pass.

If this ever comes back

The narrow version needs no codegen change at all: a Delphi/FPC corpus that READS IsMultiThread can be served by exposing it as an ordinary variable that is simply True in a --threadsafe build and False otherwise. File that as compat work if a corpus actually needs it; do not reopen the mode question.

IsMultiThread at runtime, instead of a compile-time mode?

What Delphi and FPC actually do — measured, not recalled

The question included "not sure how Delphi solves it, maybe they never optimize for no-threads, maybe they prescan". Neither. FPC 3.2.2 is on this box, so it was measured rather than guessed:

writeln(IsMultiThread);          { FALSE }
t := TW.Create(True);
writeln(IsMultiThread);          { TRUE — set at Create, before Start }

IsMultiThread is a plain longbool in rtl/inc/systemh.inc:694, initially FALSE, set TRUE when a thread is created and never reset. The refcount primitives then branch on it. From FPC's own x86-64 assembler (rtl/x86_64/x86_64.inc:678), comment included:

function declocked(var l : longint) : boolean;assembler; nostackframe;
  asm
     { this check should be done because a lock takes a lot }
     { of time!                                             }
     cmpl       $0,IsMultithread(%rip)

So there is no compile-time detection anywhere in the design. The locking code is always compiled in; the lock is skipped at runtime while the program is single-threaded. Delphi shares this lineage and this mechanism.

What the branch costs, versus the lock

200M iterations, clock_gettime(CLOCK_PROCESS_CPUTIME_ID), gcc -O2:

refcount path time vs floor
always lock incl 1.454 s +276%
cmp flag, single-threaded path taken 0.407 s +5%
plain incl (floor) 0.387 s

The branch is perfectly predicted — the flag never changes in a single-threaded run — so it recovers about 95% of what the lock costs. For comparison, pxx's current all-or-nothing --threadsafe measured +14% on a string/heap-heavy Pascal benchmark; that whole 14% is what this design would mostly reclaim while still being thread-safe.

Why this dissolves the question that prompted it

Every hard part of "auto-detect threading" was a consequence of the mode being a COMPILE-TIME fact:

With a runtime flag none of it applies. Nothing is detected, because nothing needs to be: the program is single-threaded until the moment it creates a thread, and that moment sets the flag.

What it would mean for pxx

Cost and open questions

Recommendation

Land [[feature-a-pxx-threadsafe-conditional-define]] first regardless — it is a one-line compiler change that unblocks TThread in Classes now, and it is not wasted work if this lands later (the ifdef simply becomes unnecessary and comes out).

Then decide this one on its own merits. It is the design Delphi and FPC both converged on, the measurement says the branch is nearly free, and it removes a whole class of questions rather than answering them. The counter-argument is code size on microcontrollers, and the inverted flag answers that — but it is a real piece of Track A work across five targets, not a quick win.