# Progressing the SWI-Prolog engine

**URL:** <https://swi-prolog.discourse.group/t/progressing-the-swi-prolog-engine/9880>\
**Category:** Request For Comments\
**Created:** [November 2, 2026, 10:34am UTC](https://swi-prolog.discourse.group/t/progressing-the-swi-prolog-engine/9880 "2026-11-02T10:34:24Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![jan](https://yyz2.discourse-cdn.com/free1/user_avatar/swi-prolog.discourse.group/jan/32/4_2.png) [@jan](https://swi-prolog.discourse.group/u/jan)\
**Post date:** [November 2, 2026, 10:34am UTC](https://swi-prolog.discourse.group/t/progressing-the-swi-prolog-engine/9880/1 "2026-11-02T10:34:24Z")

</div>

_Progressing_ seems popular 🙂 This topic is on improvements to the engine and asking people for suggestions and testing.

I’ve pushed a number of commits that introduce **thread local atoms/blobs**. It started with the observation that, while _blobs_ are a neat and safe way to encapsulate references to foreign resources, they tend to stress the global garbage collector and the conservative nature of SWI-Prolog’s AGC (_Atom Garbage Collector_) can cause foreign resources to leak. So, I started discussing with Claude with the idea to get a cheaper thread-local alternative that can be promoted to a shared blob for the case we want to share a resource between blobs. After some rounds, and trials, this resulted in thread-local atoms. The idea is

- The atom array is split in chunks of 64 atoms that are reserved for a specific thread. That alone is an advantage that was discovered while timing multi-threaded tabling. Before, subsequent atoms (_trie_ blobs) had the tendency to get interleaved over multiple threads, causing a lot of contention.
- An atom/blob starts as thread-local. It is part of the shared atom table, but has a flag and using the above chunks we can figure out the owner thread. The thread itself runs synchronous (and thus precise) AGC using similar rules as AGC. It it stays local, only a simple atomic synchronization is needed to discard it (also used by shared AGC).
- If the atom is added to a global resource (e.g, a clause), `PL_register_atom()` makes it global. This merely clears the _local_ flag and hands the atom from the threads local AGC to the global AGC. This mechanism also ensures that an atom that is passed to another thread using a clause, record or message is made global.
- _Unique atoms_ (most of them, i.e., where the atom handle has a one-to-one mapping to the represented value) are made global at the moment a second thread tries to create the atom: it finds the local atom (which is also in the global atom table) and makes it global.

This mechanism now covers streams, clause and record references (clause/3, asserta/2), tries (tables), undo/1 terms and … text atoms for e.g., read/1, `atom_concat/3` and any foreign predicate using `PL_unify_chars()` or one of its variations (as opposed to `PL_new_atom() && PL_unify_atom()`, i.e., `PL_new_atom()` creates a shared atom). This extents into thread\_local/1 predicates, so reading terms and asserting them into a thread local dynamic predicate keeps all the atoms owned by the thread. Thread-local predicates now also use thread-local clause garbage collection.

In addition, the xpce, jpl and Janus packages now use this mechanism to reference foreign objects.

My timing shows mild improvements on single threaded workloads and serious improvements on multi-threaded workloads (up to 4 times better for JPL). Far fewer calls to global AGC and much better reclamation of foreign resources because (a) local AGC is much cheaper and thus can run more frequently and (b) it is _precise_.

All this has few user visible consequences. If you use blobs in your foreign code you can use the blob registration flag `PL_BLOB_LOCAL` and you should read the updated manual sections. C/C++ code needs no changes except that `PL_get_atom()` _can_ give you a local atom. All is fine if you just communicate the `atom_t` to the same thread or store it and use `PL_register_atom()` (as required also before this). You cannot pass this `atom_t` to another thread or engine though. If you need to do that, you must use `PL_register_atom()`, `PL_put_atom()` (or a similar function to store it in a `term_t`) and `PL_unregister_atom()`. Predicates such as current\_atom/1 or current\_blob/2 return only shared atoms/blobs and blobs owned by the calling thread.

All the coding and testing is done by Claude. It found several pre-existing race conditions and other bugs that are fixed as well. Design was cooperative. Claude came with the idea of local atoms but got stuck in the design several times where I could come up with solutions.
