Software engineer and team lead with 15 years of experience. I have worked in product and outsourcing teams, as a developer, tech lead and team lead, on systems where load, latency and cost matter.
Blog · Habr · [email protected]
- High-load backends: microservice design, splitting monoliths, search, caching, traffic routing and failover.
- Data and observability: metrics pipelines, tracing, alerting, analytics on ClickHouse, decisions made from data rather than guesses.
- Performance: profiling down to CPU instructions and memory, from application code to native extensions and the language runtime.
- Leading teams: architecture, planning, code review, hiring, mentoring, development standards.
- One mutex for every row: mysqlnd statistics in thread-based PHP
- 12 web stacks on vendor defaults, PostgreSQL and MySQL: what a request costs and what the drivers change
- json_encode() in php-src, autumn 2026: where the time goes and what is on the table
- php-src's benchmark CI, read as a history — 7,024 master commits of callgrind results, plotted.
php-src — performance of the PHP interpreter: string functions, json_encode(), htmlspecialchars(), CSV, the compiler, and threaded (ZTS) builds. Every change comes with measurements on Intel and AMD with several compilers, a byte-for-byte comparison with master, and the cases where it gets slower. I also review other contributors' performance PRs with the same measurements.
php-src-bench — the A/B benchmark tool behind that work, with a public report for every change: what was compared, full tables, how the output was verified, commands to reproduce.
web-request-cost — CPU and memory per request of 12 web stacks (PHP-FPM, FrankenPHP, RoadRunner, Swoole, Go, Node.js, .NET, Java and others) on PostgreSQL and MySQL, with every raw record.
| PR | Change | Measured |
|---|---|---|
| mysqlnd: relaxed atomics instead of a mutex for statistics in ZTS builds (issue #24447) | FrankenPHP + MySQL, 16 threads: 10–47% less CPU per request, depending on the cores the threads run on; statistics cost 12–91% before, 0.6–1.4% after (report) | |
json_encode(): decode valid UTF-8 inline |
non-ASCII text 9–32% fewer cycles, 6 compiler/CPU configs; with GCC 13/14 on Intel some ASCII inputs up to 20% more (report) | |
php_mblen() without mbrlen() for ASCII |
fgetcsv() 4,210 → 500 ns per row (report) |
|
#[\Deprecated] without constructing the attribute (issue #24191) |
5,016 → 2,778 instructions per deprecated call (report) | |
[$a, $b] = [$b, $a] without building the array |
922 → 523 instructions per swap (report) | |
ASCII fast path in htmlspecialchars() |
41–89% fewer cycles where something has to be encoded (English text, HTML, Markdown, JavaScript) (report) | |
html_entity_decode(): stop at the first non-name byte |
a string of & 3.5× faster; ¶m= URLs in text up to 7% slower |
Numbers for #24198 and #24180 are instructions on an Intel i7-13700H, #24448 is on an AMD Ryzen 9 5950X; the reports have the full tables, machines and compilers. Also reported: the benchmark CI built Symfony Demo without ext-intl, so 18% of the measured request was a polyfill (#24183).
Performance PRs by other contributors, measured on current master with GCC 13.3, GCC 14.2, clang 18.1 and clang 20.1 (Intel i7-13700H, the flags of the official Docker image), with the output compared to master. What the measurements showed:
| PR | Change | Result |
|---|---|---|
mb_substr() on valid UTF-8 |
10–98% fewer cycles in nine languages, depending on bytes per character and position; the first version returned different results for UTF-8-Mobile encodings, fixed in the PR with the suggested change | |
var_export() without temporary strings |
15–68% fewer cycles on small and medium values; on 32-bit builds the escaped length overflowed and var_export() crashed; long strings got slower; a one-pass variant was faster than master in every case measured (report) |
|
SSE2/NEON bin2hex()/hex2bin() |
hex2bin() returned wrong bytes on big-endian AArch64, two-line fix; short inputs 1–4% more cycles (report) |
|
hash_pbkdf2() with precomputed HMAC states |
43–53% fewer cycles at 1,000 iterations (35% for whirlpool), 0–5% more at 1 iteration; the hash contexts that hold the key should be wiped before freeing | |
| charset lookup of the HTML functions, UTF-8 first | short UTF-8 strings 18–27% fewer cycles on top of #23957; with clang 18 "MacRoman" +35% cycles (report) |
|
array_push() of several values in bulk |
9–38% fewer cycles for three or more values; three mutations passed the tests, two short tests suggested that catch them | |
array_shift() with one memmove() |
draining a queue of 1,000 ints 82–84% fewer cycles | |
array_diff() with a bitset for ints |
ints 71–88% fewer cycles; arrays of strings pay +1–7% for the check | |
unserialize() back-reference lookup |
14–31% fewer cycles on payloads with many references | |
SSE2/NEON path in json_encode() |
mostly-ASCII strings of 256 bytes and more 1.3–5× faster; Cyrillic and CJK text 4–19% slower with GCC |

