From 88b585469d8506f2f1ae8abcf05b10fc6532f696 Mon Sep 17 00:00:00 2001
From: Youngkwang Lee <youngkwang.lee@navercorp.com>
Date: Fri, 28 Aug 2026 18:53:05 +0900
Subject: [PATCH 2/4] OPTIM: uri_normalizer: compute query parameter names only
 once

query_param_cmp() called iststop(param, '=') on both of its operands on
every single comparison to find where the parameter name ends. qsort()
performs O(n log n) comparisons, so each parameter name was rescanned
about log2(n) times: for a query string with 427 parameters that is
roughly 7400 scans to recover 427 pieces of information.

The collection loop already walks every parameter once, so locate '='
there and remember the name length alongside the parameter. This turns
the O(n log n) rescanning into a single O(n) pass.

Storing the extra length requires a dedicated struct rather than a bare
struct ist. It is laid out as a pointer plus two 32 bit lengths, which
keeps it at 16 bytes on 64-bit platforms, exactly the size of the
struct ist it replaces, so the number of parameters that fit into the
trash chunk is unchanged and the existing ERR_ALLOC threshold is
preserved there. On 32-bit platforms it is 12 bytes instead of 8, which
reduces that number by a third; queries with more parameters than fit
are rejected with ERR_ALLOC as before, just from a lower bound. Both
lengths are bounded by the buffer size, so 32 bits are always
sufficient.

The resulting order is identical: the same names are compared with the
same istdiff(), and the pointer comparison that makes the sort stable
for identical names is kept.

Measured on a 24-core AMD EPYC 7413 with a 20 kB query string
(427 parameters), gcc -O2, requests handled with keep-alive, comparing
the six normalizers before and after this series:

  8 byte path            266.4 us ->  266.0 us   (no measurable change)
  60 parameter query     314.02 us -> 311.17 us  -0.9%
  427 parameter query   1137.52 us -> 1062.53 us -6.6%

Those are whole-request figures, where normalization is only a part of
the cost. Against a build with the six actions removed the same 427
parameter case costs 989.67 us, so what the actions themselves add goes
from 218.3 us down to 142.9 us, about a third less:

  normalization overhead  +218.3 us -> +142.9 us  -34.6%

The gain grows with the number of parameters, as expected, and short
URIs get essentially nothing out of it. The same series built with -Og
gives -6.2% on the large query, so that gain is not an artefact of one
optimisation level.

This is a pure optimization with no functional change, so the risk of
regression is low. It is not needed in stable branches.

Signed-off-by: Youngkwang Lee <youngkwang.lee@navercorp.com>
---
 src/uri_normalizer.c | 38 +++++++++++++++++++++++++++++---------
 1 file changed, 29 insertions(+), 9 deletions(-)

diff --git a/src/uri_normalizer.c b/src/uri_normalizer.c
index c24d95fd5..cb67d56f0 100644
--- a/src/uri_normalizer.c
+++ b/src/uri_normalizer.c
@@ -394,6 +394,21 @@ enum uri_normalizer_err uri_normalizer_path_merge_slashes(const struct ist path,
 	return err;
 }
 
+/* A query parameter with the length of its name (the bytes before the
+ * first '=', or the whole parameter if there is none) computed once while
+ * collecting the parameters. Keeping it here spares query_param_cmp() an
+ * O(n log n) rescan for '='. Both lengths are bounded by the buffer size,
+ * so 32 bits are always sufficient. On 64-bit platforms this is the same
+ * 16 bytes as the struct ist it replaces, so the number of parameters
+ * that fit in the trash buffer is unchanged. On 32-bit platforms it is
+ * 12 bytes instead of 8, which reduces that number by a third.
+ */
+struct query_param {
+	const char *ptr;
+	uint32_t len;
+	uint32_t name_len;
+};
+
 /* Compares two query parameters by name. Query parameters are ordered
  * as with memcmp. Shorter parameter names are ordered lower. Identical
  * parameter names are compared by their pointer to maintain a stable
@@ -401,10 +416,10 @@ enum uri_normalizer_err uri_normalizer_path_merge_slashes(const struct ist path,
  */
 static int query_param_cmp(const void *a, const void *b)
 {
-	const struct ist param_a = *(struct ist*)a;
-	const struct ist param_b = *(struct ist*)b;
-	const struct ist param_a_name = iststop(param_a, '=');
-	const struct ist param_b_name = iststop(param_b, '=');
+	const struct query_param *param_a = a;
+	const struct query_param *param_b = b;
+	const struct ist param_a_name = ist2(param_a->ptr, param_a->name_len);
+	const struct ist param_b_name = ist2(param_b->ptr, param_b->name_len);
 
 	int cmp = istdiff(param_a_name, param_b_name);
 
@@ -412,10 +427,10 @@ static int query_param_cmp(const void *a, const void *b)
 		return cmp;
 
 	/* The contents are identical: Compare the pointer. */
-	if (istptr(param_a) < istptr(param_b))
+	if (param_a->ptr < param_b->ptr)
 		return -1;
 
-	if (istptr(param_a) > istptr(param_b))
+	if (param_a->ptr > param_b->ptr)
 		return 1;
 
 	return 0;
@@ -432,7 +447,7 @@ enum uri_normalizer_err uri_normalizer_query_sort(const struct ist query, const
 	struct ist scanner = query;
 
 	const struct buffer *trash = get_trash_chunk();
-	struct ist *params = (struct ist *)b_orig(trash);
+	struct query_param *params = (struct query_param *)b_orig(trash);
 	const size_t max_param = b_size(trash) / sizeof(*params);
 	size_t param_count = 0;
 
@@ -449,13 +464,18 @@ enum uri_normalizer_err uri_normalizer_query_sort(const struct ist query, const
 
 	while (istlen(scanner) > 0) {
 		const struct ist param = istsplit(&scanner, delim);
+		const char *eq;
 
 		if (param_count + 1 > max_param) {
 			err = URI_NORMALIZER_ERR_ALLOC;
 			goto fail;
 		}
 
-		params[param_count] = param;
+		eq = memchr(istptr(param), '=', istlen(param));
+		params[param_count].ptr = istptr(param);
+		params[param_count].len = istlen(param);
+		params[param_count].name_len = eq ? (uint32_t)(eq - istptr(param))
+		                                  : (uint32_t)istlen(param);
 		param_count++;
 	}
 
@@ -465,7 +485,7 @@ enum uri_normalizer_err uri_normalizer_query_sort(const struct ist query, const
 		if (i > 0)
 			newquery = __istappend(newquery, delim);
 
-		if (istcat(&newquery, params[i], size) < 0) {
+		if (istcat(&newquery, ist2(params[i].ptr, params[i].len), size) < 0) {
 			/* This is impossible, because we checked the size of the destination buffer. */
 			my_unreachable();
 			err = URI_NORMALIZER_ERR_INTERNAL_ERROR;
-- 
2.45.1

