Repository navigation
Measure a two-byte string first when it may not fit in the pool - #1339
Conversation
Benchmark:
|
| Scenario | Base req/s | Head req/s | Head / base | Rounds | Noise | Busy | CPU per request |
|---|---|---|---|---|---|---|---|
| http/hello-world | 114720 | 114214 | 0.995x (-0.5%) | 0.99 to 1.00 | ±2.5% | 100% / 100% | 8.71 / 8.75 us (1.005x) |
| http/headers | 98104 | 97502 | 0.993x (-0.7%) | 0.99 to 0.99 | ±1.6% | 100% / 100% | 10.18 / 10.25 us (1.007x) |
| http/json-post | 85126 | 84857 | 0.996x (-0.4%) | 1.00 to 1.00 | ±1.0% | 100% / 100% | 11.74 / 11.77 us (1.004x) |
| http/cached | 129020 | 128512 | 0.996x (-0.4%) | 0.99 to 1.00 | ±0.5% | 100% / 100% | 7.75 / 7.78 us (1.004x) |
| ws/echo-20b | 117499 | 118931 | 1.010x (+1.0%) | 1.01 to 1.02 | ±1.7% | 100% / 100% | 8.50 / 8.40 us (0.990x) |
| ws/echo-4kb | 102356 | 102790 | 1.005x (+0.5%) | 1.00 to 1.01 | ±1.2% | 100% / 100% | 9.76 / 9.72 us (0.995x) |
4 rounds of 3s per scenario, each round loading base, head and a second process of each one, from a copy of its build, after the other, in an order that changes every round. "Head / base" is the median of the per-round ratios of req/s and "Rounds" their range. "Noise" is how far base/base and head/head, the same code on both sides, got from 1 in this same run: that is what the machine did, so a row is marked only when the median moved further than that, at least 2%, and every round moved the same way: 👀 slower, 🏆 faster. "Busy" is the cpu time of the server process over the round and "CPU per request" that time divided by the requests it answered, with the head/base ratio judged against its own same-code band and bold when it cleared it. When busy is well under 100% the load generator set the pace, the req/s are its and the cpu per request is the column to read. Only the ratios are comparable across runs, the absolute figures depend on the runner.
Node v26.10.0, AMD EPYC 7763 64-Core Processor, 4 cores (server on cpu 0, load on 2,3), http_load_test and load_test with 100 connections.
| size_t capacity = pool.size() - pool_offset; | ||
| bool fits = false; | ||
| if ((size_t) string->Length() <= capacity) { | ||
| if ((size_t) string->Length() * (string->IsOneByte() ? 1 : 3) <= capacity) { |
There was a problem hiding this comment.
One byte Latin-1 can cause 2 byte Utf-8
There was a problem hiding this comment.
Yes, latin1 can take 2 bytes per char, so a one-byte string can still fill the pool for nothing, as on master. I left it on purpose: V8 writes one-byte strings to UTF-8 fast, so that waste is small, while a 2x check would also measure first every ascii string between 64 and 128 KB, the +40% I mesured for the 3x check on that range. If you prefer it safe for latin1 too, I can use 2 for one-byte strings.
Adjust comment regarding string byte size handling.
For #1336. After #1337 a string can still write the pool once for nothing: fewer characters than free bytes, but more UTF-8 bytes. This is costly for two-byte strings: up to 3 bytes per unit, and V8 writes them to UTF-8 much slower than ASCII.
Now a two-byte string tries the first write only if
Length() * 3fits in the free pool, otherwise it is measured first andallocgives the pool or malloc. One-byte strings do not change.Trade-off,
app.publish('t', message)with no subscribers, Windows, Node 26, min of 11 rounds:€: from 316 to 168 us€+ 60 KB ascii: from 69 to 79 us (+14%)The 3x check for all strings, as in #1336, costs also +40% to ascii between 43 and 128 KB, so I kept one-byte strings out.
Test:
tests/strings.jshas a two-byte string past the pool and one that fits after the measure.