From b904884b94ca48e025a12697ab172711e6c134d8 Mon Sep 17 00:00:00 2001 From: Michiel van Slobbe Date: Sat, 10 Oct 2026 14:50:10 +0200 Subject: [PATCH] Improve performance of string output (#3643) Printing a string wrote each character with a separate call, one fwrite() per character, or one jv_string_append_buf() for tojson and tostring, which made output of string-heavy data slow. Scan ahead for the next byte that needs escaping and write the run of bytes before it with a single call, without changing the output. Printing strings is now significantly faster, most of all for long strings with few characters to escape, and the same applies to tojson and tostring. With --ascii-output, text that is almost all non-ASCII is about the same speed, since nearly every byte is escaped anyway. --- src/jv_print.c | 13 ++++++++++++- tests/jq.test | 9 +++++++++ tests/utf8test | 14 ++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/jv_print.c b/src/jv_print.c index 94924203..568656dc 100644 --- a/src/jv_print.c +++ b/src/jv_print.c @@ -126,7 +126,18 @@ static void jvp_dump_string(jv str, int ascii_only, FILE* F, jv* S, int T) { int c = 0; char buf[32]; put_char('"', F, S, T); - while ((i = jvp_utf8_next((cstart = i), end, &c))) { + while (i < end) { + // Write each run of bytes that need no escaping with a single call. + for (cstart = i; cstart < end; cstart++) { + unsigned char b = *cstart; + if (b < 0x20 || b == '"' || b == '\\' || b == 0x7F || (ascii_only && b >= 0x80)) + break; + } + if (cstart > i) + put_buf(i, cstart - i, F, S, T); + if (cstart == end) + break; + i = jvp_utf8_next(cstart, end, &c); assert(c != -1); int unicode_escape = 0; if (0x20 <= c && c <= 0x7E) { diff --git a/tests/jq.test b/tests/jq.test index 9f2ab525..183950d4 100644 --- a/tests/jq.test +++ b/tests/jq.test @@ -100,6 +100,15 @@ null ["foo", 1, ["a", 1, "b", 2, {"foo":"bar"}]] ["foo",1,["a",1,"b",2,{"foo":"bar"}]] +# Put escapes on both sides of runs of bytes that need no escaping +map(tojson) +["abcdefg\"", "abcdefgh\\", "abcdefghi\u0001j", "abcdefghijklmno\u007f", "abcdefgh\té", "abcdefghijklmnop\n"] +["\"abcdefg\\\"\"","\"abcdefgh\\\\\"","\"abcdefghi\\u0001j\"","\"abcdefghijklmno\\u007f\"","\"abcdefgh\\té\"","\"abcdefghijklmnop\\n\""] + +[range(18) as $i | ("\"", "\\", "\u0001", "\u007f", "é", "😀") as $c | "abcdefghijklmnopq"[:$i] + $c + "abcdefghijklmnopq"[$i:] | (tojson | fromjson) == .] | all +null +true + # # Dictionary construction syntax # diff --git a/tests/utf8test b/tests/utf8test index 37d8043f..e9512210 100755 --- a/tests/utf8test +++ b/tests/utf8test @@ -21,4 +21,18 @@ for l in 1 2 3 4; do fi done +# --ascii-output escapes non-ASCII characters on both sides of runs of +# bytes that need no escaping +$VALGRIND $Q $JQ -cn '[range(18) as $i | ("é", "😀", "\u007f") as $c | + "abcdefghijklmnopq"[:$i] + $c + "abcdefghijklmnopq"[$i:]]' > $d/input +$VALGRIND $Q $JQ -ac . $d/input > $d/out +if LC_ALL=C grep -q '[^ -~]' $d/out; then + echo "--ascii-output printed a non-ASCII byte" + exit 1 +fi +if ! $VALGRIND $Q $JQ -e --slurpfile a $d/input '. == $a[0]' $d/out > /dev/null; then + echo "--ascii-output changed a string" + exit 1 +fi + exit 0