[php-src] PR #24190: Feature: Fiber stacks on demand on Windows (performance)
| From: | EdmondDantes | Date: | Thu, 08 Oct 2026 09:03:17 +0000 |
| Subject: | [php-src] PR #24190: Feature: Fiber stacks on demand on Windows (performance) | ||
| Groups: | php.git-pulls | ||
| Request: | Send a blank email to git-pulls+get-39248@lists.php.net to get a copy of this message | ||
Pull Request: https://github.com/php/php-src/pull/24190
Author: EdmondDantes
On Windows,
zend_fiber_stack_allocate() commits the whole fiber stack with
VirtualAlloc(MEM_COMMIT). Every live fiber therefore charges its full
fiber.stack_size (2 MiB by default on 64-bit) to the system commit limit (RAM plus page
file), however little of the stack it uses. On Linux mmap() only reserves the address
range, and pages are charged as the stack touches them.
A process with many suspended fibers runs out of commit long before it runs out of memory: 10 000
suspended fibers need about 20 GB of commit, and new Fiber() then fails with
Fiber stack allocate failed: VirtualAlloc failed: [0x000005af] The paging file is too small
for this operation to complete.
### Reproducer
```php
<?php
$fibers = [];
for ($i = 0; $i < 20000; $i++) {
$fiber = new Fiber(static function () {
Fiber::suspend();
});
$fiber->start();
$fibers[] = $fiber;
}
echo "ok\n";
```
Expected: ok, with under 1 GB of commit.
Actual: about 40 GB of commit (20 000 × 2 MiB); with less free commit, the Fiber stack
allocate failed exception above.
### Fix
The stack is now laid out the way Windows lays out a thread stack:
- the whole stack is reserved with MEM_RESERVE;
- only its top 32 KiB is committed, with one PAGE_GUARD page below;
- the bottom page stays reserved and is never committed: it stops the stack.
The kernel commits the rest page by page when the stack reaches the guard page, as it does for
threads. It does so for any stack between the TEB's DeallocationStack and
StackBase, and jump_fcontext() already swaps these TEB fields (and
StackLimit) per context, so no assembly changes:
- zend_fiber_init_context() passes the reservation's full size to
make_fcontext(), so DeallocationStack is the reservation's base, as
for a thread stack.
- zend_fiber_trampoline() sets the TEB StackLimit to the bottom of the
committed part, once per fiber. make_fcontext() stores the bottom of the whole stack
there, and x64 __chkstk only probes pages below StackLimit, so a frame
larger than a page would otherwise skip the guard page. From then on the kernel moves
StackLimit as the stack grows, and jump_fcontext() saves it with the
context.
The context switch does no extra work. Non-Windows code is unchanged.
### Behaviour change
Running out of commit no longer throws when the fiber starts. It ends the process with
STATUS_STACK_OVERFLOW while the stack grows, as it does for a thread. A fiber that
overflows its C stack now also ends with STATUS_STACK_OVERFLOW (0xC00000FD); before, it
ended with STATUS_GUARD_PAGE_VIOLATION (0x80000001), because the old one-shot guard
page lay below DeallocationStack.
### Measurements
Windows 11 x64, i7-11700K, Release_TS build. Commit charge is the process's private bytes.
| | before | after |
|---|---|---|
| Commit per suspended fiber (2 000 / 10 000 fibers) | 2074 / 2074 KiB | 41 / 42 KiB |
| Create, start and drop a fiber, median | 0.465 us | 0.459 us |
| Allocate one stack (C, no PHP) | 1.73 us | 2.18 us |
| Free one stack (C, no PHP) | 7.9 us | 6.9 us |
Allocation makes three VirtualAlloc calls instead of VirtualAlloc and
VirtualProtect; freeing is cheaper, so a full allocate-and-free cycle is slightly
faster. Page faults per fiber are unchanged.
### Tests
Zend/tests/fibers and Zend/tests/stack_limit pass on Windows x64
(Debug_TS) and Linux, with the same results before and after. On Windows the same suites also pass
with the initial commit reduced to 4 KiB, which makes every fiber grow its stack through the guard
page and __chkstk from its first frames.
No new test: commit charge cannot be checked from a phpt, and stack growth and overflow inside
fibers are covered by Zend/tests/stack_limit.
Not tested on x86 or ARM64 Windows. Both use the same TEB fields in jump_fcontext();
x86 _chkstk probes from ESP without reading StackLimit.
### NEWS
```
- Core:
. Fiber stacks on Windows are committed on demand instead of in full.
(Edmond Dantes)
```