s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(
[[ EXCEPTION while printing message '~W'
with arguments [s(s(s(s(s(s(s(s(s(...))))))))),[priority(699),partial(true),max_depth(0)]]:
raised: resource_error(c_stack)
]]
Tested on Linux and MacOS and verified it still compiles on Windows (but the patch has no effect on Windows). For now only protects write/1 and friends.
I think it should work on most modern POSIX based OSes. Not simply crashing is surely a good move
I donāt know what preallocation means. Bottom line is that OSes reserve a memory area for the stack that (typically) grows down on demand until a certain limit or a boundary (the growing heap) is reached. On 64-bit systems the latter seems a bit improbable. It than gets a SEGV exception but it has no stack left to deal with the exception. POSIX provides an alternative stack as an option for this: you allocate a little (default 8K on X64 Linux) area and the system will use that area for handling the segv. You should obviously do as little as possible there as using more than 8k will simply overwrite other parts of the heap So, I use sigsetjmp() to save the state before entering the possibly too deeply nesting function and set a flag to indicate Iāve done so. Now if the signal handler finds this flag it sets the overflow exception and using siglongjmp() to return to the start of the function. Otherwise it calls the default SWI-Prolog crash handler.
Note that memory allocation is rarely a (performance) bottleneck in SWI-Prolog. Most of the work typically happens on the Prolog stacks that are allocated as a big chunk. Only a few potential time critical operations such as sending thread messages, assert/retract dynamic code and tabling operations use small allocations. Eventually tabling should work with bigger regions. Same probably applies to dynamic predicates. If we would allocate the clause for such a predicate on a larger chunk associated with the predicate we avoid them getting fragmented. This improves locality and allows reclaiming the larger chunks. Would work perfectly for dynamic predicates that are populated and later cleaned. For others, it may work out worse Probably message queues should also maintain larger chunks. Here we can expect them to be drained.
In my experience smart pointers work fairly well in single threaded code. SWI-Prolog used reference counting to keep track of erased dynamic clauses in the past. That didnāt affect performance much. When we started doing threads, reference counting quickly caused performance inversion when two threads were using (just execute) the same dynamic code. That is why we moved to garbage collection. Something similar happened with Keriās first implementation of thread-safe hash tables. They used reference counting to reclaim old tables after the table was resized. That didnāt work. Now each thread has a pointer to the hash table on which it performs an operation in its thread structure. After resizing we do a sweep through all thread structures to see whether anyone is using the old key-value store. If not, we discard it. If so, we keep it in a list that is processed on several occasions.
As a result SWI-Prolog barely has any of these bottlenecks these days. It has been tested with up to 128 cores by me (real cores), providing about 85 times speedup. I had access to the machine only briefly, so no time to figure out the bottleneck