Skip to content

Commit 1b63da6

Browse files
committed
perf(lua): string.buffer rewrite of hexlike encode/decode
The main encode/decode loops already use string.buffer; the hexlike paths were still on the old table-accumulator + table.concat pattern. Convert both: - hexlike_encode: stream into one buffer via :put with a precomputed HEX2[256] LUT instead of per-byte string.format("%02X") into a hex_parts table + nested table.concat. Output is byte-identical to the old encoder. - hexlike_decode: FFI reserve/commit byte-write path (like main decode) instead of per-byte string.char into a table + final concat. Hex pairs combine two HEXVAL nibble lookups instead of substring + :match + tonumber(h1..h2,16). Measured (hyperfine, fixed 160KB mixed input; wall incl. ~5ms LuaJIT startup): encode 10.7ms -> 6.2ms (1.73x wall; ~5x work-only) decode 20.8ms -> 9.2ms (2.26x wall; ~3.8x work-only) All H1-H20 hexlike tests pass; full 256-byte and empty-input roundtrips byte-exact. Flagged via dotfiles inbox note (structured-events subsystem leans on PB encode).
1 parent 27db9eb commit 1b63da6

1 file changed

Lines changed: 50 additions & 24 deletions

File tree

bin/printable-binary

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,18 @@ for b = 97, 122 do hexlike_passthrough[b] = true end -- a-z
256256
-- Οχ prefix: Greek Omicron (U+039F) + Greek Chi (U+03C7) — NOT ASCII "0x"
257257
local OX = "\xCE\x9F\xCF\x87"
258258

259+
-- Precomputed hexlike lookup tables (built once at load) so the per-byte hot
260+
-- loops touch a table instead of string.format / tonumber+match per byte.
261+
-- HEX2[b] -> the two-char uppercase hex string for byte b (encode side).
262+
-- HEXVAL[c] -> the nibble value (0-15) for an ASCII hex-digit byte c, else nil
263+
-- (decode side; lets us combine two nibbles with no concat/regex).
264+
local HEX2 = {}
265+
for b = 0, 255 do HEX2[b] = string.format("%02X", b) end
266+
local HEXVAL = {}
267+
for c = 0, 9 do HEXVAL[48 + c] = c end -- '0'-'9'
268+
for c = 0, 5 do HEXVAL[65 + c] = 10 + c end -- 'A'-'F'
269+
for c = 0, 5 do HEXVAL[97 + c] = 10 + c end -- 'a'-'f'
270+
259271
-- Detect Οχ hex sequences in input (for cross-format warnings)
260272
function PrintableBinary.detect_hexlike(input_string)
261273
if type(input_string) ~= "string" or #input_string == 0 then
@@ -278,42 +290,43 @@ function PrintableBinary.hexlike_encode(binary_data, options)
278290
for k, v in pairs(hexlike_passthrough) do pt[k] = v end
279291
if spaces_mode then pt[32] = true end
280292

281-
local result = {}
293+
-- One growable buffer instead of an accumulator table + final table.concat.
294+
-- Hex output is ~3x the input at most (2 hex chars + delimiters per byte),
295+
-- so size the buffer generously to avoid mid-loop growth.
296+
local sbuf = buffer.new(#binary_data * 3)
282297
local i = 1
283298
local len = #binary_data
299+
local wrote = false -- tracks "have we emitted anything yet" (was #result > 0)
284300

285301
while i <= len do
286302
local byte = string.byte(binary_data, i)
287303

288304
if pt[byte] then
289-
-- Passthrough run
305+
-- Passthrough run: copy the whole run as one slice
290306
local run_start = i
291307
while i <= len and pt[string.byte(binary_data, i)] do
292308
i = i + 1
293309
end
294-
result[#result + 1] = string.sub(binary_data, run_start, i - 1)
310+
sbuf:put(string.sub(binary_data, run_start, i - 1))
311+
wrote = true
295312
else
296-
-- Non-passthrough run: collect hex
297-
local hex_parts = {}
313+
-- Non-passthrough run: emit Οχ then per-byte hex via the precomputed LUT
314+
-- Delimiter space before Οχ (unless at start of output)
315+
if wrote then sbuf:put(" ") end
316+
sbuf:put(OX)
298317
while i <= len and not pt[string.byte(binary_data, i)] do
299-
hex_parts[#hex_parts + 1] = string.format("%02X", string.byte(binary_data, i))
318+
sbuf:put(HEX2[string.byte(binary_data, i)])
300319
i = i + 1
301320
end
302-
303-
-- Delimiter space before Οχ (unless at start of output)
304-
if #result > 0 then
305-
result[#result + 1] = " "
306-
end
307-
result[#result + 1] = OX
308-
result[#result + 1] = table.concat(hex_parts)
321+
wrote = true
309322
-- Delimiter space after hex run (unless at end of output)
310323
if i <= len then
311-
result[#result + 1] = " "
324+
sbuf:put(" ")
312325
end
313326
end
314327
end
315328

316-
return table.concat(result)
329+
return sbuf:tostring()
317330
end
318331

319332
-- Decode hexlike format back to binary
@@ -323,11 +336,21 @@ function PrintableBinary.hexlike_decode(printable_string, options)
323336
end
324337

325338
options = options or {}
326-
local result = {}
327-
local i = 1
328339
local len = #printable_string
329340
local found_hex = false
330341

342+
-- Decode shrinks (each hex pair -> 1 byte, passthrough char -> 1 byte, Οχ and
343+
-- spaces drop out), so output never exceeds the input length. Reserve once and
344+
-- write bytes straight into the buffer via FFI — no per-byte string.char alloc,
345+
-- no accumulator table, no final concat. The hex pairs combine two nibble
346+
-- lookups (HEXVAL) instead of substring + match + tonumber(h1..h2,16).
347+
local out_cap = len
348+
if out_cap < 1 then out_cap = 1 end
349+
local dbuf = buffer.new(out_cap)
350+
local dptr = dbuf:reserve(out_cap)
351+
local dn = 0
352+
local i = 1
353+
331354
while i <= len do
332355
-- Check for Οχ (possibly preceded by delimiter space)
333356
local at_ox = false
@@ -345,27 +368,30 @@ function PrintableBinary.hexlike_decode(printable_string, options)
345368
i = i + 4 -- skip past Οχ
346369
-- Read uppercase hex pairs
347370
while i + 1 <= len do
348-
local h1 = printable_string:sub(i, i)
349-
local h2 = printable_string:sub(i + 1, i + 1)
350-
if h1:match("[0-9A-Fa-f]") and h2:match("[0-9A-Fa-f]") then
351-
result[#result + 1] = string.char(tonumber(h1 .. h2, 16))
371+
local n1 = HEXVAL[string.byte(printable_string, i)]
372+
local n2 = HEXVAL[string.byte(printable_string, i + 1)]
373+
if n1 and n2 then
374+
dptr[dn] = n1 * 16 + n2
375+
dn = dn + 1
352376
i = i + 2
353377
else
354378
break
355379
end
356380
end
357381
-- Consume trailing delimiter space (if present)
358-
if i <= len and printable_string:sub(i, i) == " " then
382+
if i <= len and string.byte(printable_string, i) == 32 then
359383
i = i + 1
360384
end
361385
else
362386
-- Passthrough byte
363-
result[#result + 1] = printable_string:sub(i, i)
387+
dptr[dn] = string.byte(printable_string, i)
388+
dn = dn + 1
364389
i = i + 1
365390
end
366391
end
367392

368-
return table.concat(result), found_hex
393+
dbuf:commit(dn)
394+
return dbuf:tostring(), found_hex
369395
end
370396

371397
-- Encode binary data to printable UTF-8

0 commit comments

Comments
 (0)