Skip to content

Commit 44fa661

Browse files
enghitaloclaude
andcommitted
vanilla: armor hot-path byte writes against the V << regression
Single-element array push (`arr << x`) is 4-7x slower on post-0.5.1 V (vlang/v#27468) while bulk push_many, allocation and indexed writes are unaffected. The two hot single-element `<<` sites are now bulk writes: - wi() built integer digits with `out << tmp[i]` per digit; it now itoa's back-to-front into the [20]u8 scratch and flushes with one push_many. - write_json_response() pushed the item separator `,` and closing `}` one byte at a time; the closing `}` is now fused with the separator into a single '},' / '}' push_many. Output is byte-identical (verified across counts 0..4096 and edge-value integers). This makes the JSON hot path fast on both the 0.5.1 release and current master, independent of the upstream codegen regression. Both epoll and io_uring backends. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent abf558e commit 44fa661

2 files changed

Lines changed: 26 additions & 26 deletions

File tree

frameworks/vanilla-epoll/main.v

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -84,24 +84,25 @@ fn ws(mut out []u8, s string) {
8484

8585
// wi appends the decimal digits of a non-negative integer to `out`, no
8686
// allocation (itoa into a stack scratch, emitted most-significant-first).
87+
// The digits are written into the scratch back-to-front and flushed with a
88+
// single `push_many` — single-element `<<` is several times slower than a bulk
89+
// copy on post-0.5.1 V (vlang/v#27468), and this path runs for every number.
8790
@[direct_array_access]
8891
fn wi(mut out []u8, n i64) {
92+
mut tmp := [20]u8{}
8993
if n == 0 {
90-
out << u8(`0`)
94+
tmp[0] = u8(`0`)
95+
unsafe { out.push_many(&tmp[0], 1) }
9196
return
9297
}
9398
mut x := n
94-
mut tmp := [20]u8{}
95-
mut i := 0
99+
mut i := 20
96100
for x > 0 {
101+
i--
97102
tmp[i] = u8(`0`) + u8(x % 10)
98103
x /= 10
99-
i++
100-
}
101-
for i > 0 {
102-
i--
103-
out << tmp[i]
104104
}
105+
unsafe { out.push_many(&tmp[i], 20 - i) }
105106
}
106107

107108
// write_resp appends a complete HTTP/1.1 response (status line + headers + body)
@@ -315,16 +316,15 @@ fn (sh &Shared) write_json_response(mut out []u8, count int, m i64) {
315316
wi(mut out, i64(clen))
316317
ws(mut out, '\r\nConnection: keep-alive\r\n\r\n{"items":[')
317318
for i in 0 .. count {
318-
if i > 0 {
319-
out << `,`
320-
}
321319
ws(mut out, sh.prefixes[i])
322320
wi(mut out, sh.dataset[i].price * sh.dataset[i].quantity * m)
323-
out << `}`
321+
// fuse each object's closing `}` with the item separator `,` into one
322+
// bulk write — single-element `<<` is the slow path on post-0.5.1 V.
323+
ws(mut out, if i < count - 1 { '},' } else { '}' })
324324
}
325325
ws(mut out, '],"count":')
326326
wi(mut out, i64(count))
327-
out << `}`
327+
ws(mut out, '}')
328328
}
329329

330330
// write_json_gzip is the json-comp path. The gzipped response for a given

frameworks/vanilla-io_uring/main.v

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -84,24 +84,25 @@ fn ws(mut out []u8, s string) {
8484

8585
// wi appends the decimal digits of a non-negative integer to `out`, no
8686
// allocation (itoa into a stack scratch, emitted most-significant-first).
87+
// The digits are written into the scratch back-to-front and flushed with a
88+
// single `push_many` — single-element `<<` is several times slower than a bulk
89+
// copy on post-0.5.1 V (vlang/v#27468), and this path runs for every number.
8790
@[direct_array_access]
8891
fn wi(mut out []u8, n i64) {
92+
mut tmp := [20]u8{}
8993
if n == 0 {
90-
out << u8(`0`)
94+
tmp[0] = u8(`0`)
95+
unsafe { out.push_many(&tmp[0], 1) }
9196
return
9297
}
9398
mut x := n
94-
mut tmp := [20]u8{}
95-
mut i := 0
99+
mut i := 20
96100
for x > 0 {
101+
i--
97102
tmp[i] = u8(`0`) + u8(x % 10)
98103
x /= 10
99-
i++
100-
}
101-
for i > 0 {
102-
i--
103-
out << tmp[i]
104104
}
105+
unsafe { out.push_many(&tmp[i], 20 - i) }
105106
}
106107

107108
// write_resp appends a complete HTTP/1.1 response (status line + headers + body)
@@ -315,16 +316,15 @@ fn (sh &Shared) write_json_response(mut out []u8, count int, m i64) {
315316
wi(mut out, i64(clen))
316317
ws(mut out, '\r\nConnection: keep-alive\r\n\r\n{"items":[')
317318
for i in 0 .. count {
318-
if i > 0 {
319-
out << `,`
320-
}
321319
ws(mut out, sh.prefixes[i])
322320
wi(mut out, sh.dataset[i].price * sh.dataset[i].quantity * m)
323-
out << `}`
321+
// fuse each object's closing `}` with the item separator `,` into one
322+
// bulk write — single-element `<<` is the slow path on post-0.5.1 V.
323+
ws(mut out, if i < count - 1 { '},' } else { '}' })
324324
}
325325
ws(mut out, '],"count":')
326326
wi(mut out, i64(count))
327-
out << `}`
327+
ws(mut out, '}')
328328
}
329329

330330
// write_json_gzip is the json-comp path. The gzipped response for a given

0 commit comments

Comments
 (0)