Skip to content

Measure a two-byte string first when it may not fit in the pool - #1339

Merged
uNetworkingAB merged 2 commits into
uNetworking:masterfrom
nigrosimone:nativestring-two-byte
Oct 1, 2026
Merged

uNetworkingAB merged 2 commits into
uNetworking:masterfrom
nigrosimone:nativestring-two-byte

Conversation

@nigrosimone

Copy link
Copy Markdown
Contributor

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() * 3 fits in the free pool, otherwise it is measured first and alloc gives 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:

  • two-byte that does not fit, 50 K chars of €: from 316 to 168 us
  • two-byte that fits after the measure, € + 60 KB ascii: from 69 to 79 us (+14%)
  • ascii and latin1 from 20 B to 1 MB: same

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.js has a two-byte string past the pool and one that fits after the measure.

@github-actions

Copy link
Copy Markdown

Benchmark: 850b7d1 against f4fc521

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.

@nigrosimone
nigrosimone marked this pull request as ready for review September 30, 2026 18:53
Comment thread src/Utilities.h
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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One byte Latin-1 can cause 2 byte Utf-8

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@uNetworkingAB
uNetworkingAB merged commit 32a27de into uNetworking:master Oct 1, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants