Add LCS LEN fast path using rolling two-row DP

When LCS is invoked with LEN (and without IDX), the actual subsequence
or match indices are not needed, so allocating the full O(n*m) table is
wasteful. Compute the length using two rolling rows of size
O(min(|a|,|b|)) instead.

This removes the proto-max-bulk-len ceiling that previously rejected
moderately large inputs (e.g. 16K x 16K) and improves wall time for
larger inputs (~1.9x at 8KB x 8KB) thanks to better cache locality and
much smaller transient allocations. Smaller inputs are unaffected.

The rolling computation is extracted into lcsReplyLength() to keep
lcsCommand focused on argument parsing and dispatch.

Signed-off-by: charsyam <charsyam@naver.com>
This commit is contained in:
charsyam 2026-04-12 18:46:27 +09:00
parent 0d85627bf0
commit fc0897bfc1

View file

@ -973,6 +973,54 @@ void strlenCommand(client *c) {
addReplyLongLong(c,stringObjectLen(kv));
}
/* Compute only the LCS length using two rolling rows, and reply to the client.
* Used when LCS is invoked with LEN (and without IDX), since recovering the
* actual subsequence or match indices is not required. Memory usage is
* O(min(|a|,|b|)) instead of O(|a|*|b|). On allocation/limit failures this
* sends an error reply; in all cases the client receives exactly one reply. */
static void lcsReplyLength(client *c, sds a, sds b) {
uint32_t alen = sdslen(a);
uint32_t blen = sdslen(b);
/* Use the shorter string for the inner dimension to minimize memory. */
const char *astr = a;
const char *bstr = b;
if (blen > alen) {
uint32_t tmp_len = alen; alen = blen; blen = tmp_len;
const char *tmp_str = astr; astr = bstr; bstr = tmp_str;
}
size_t row_bytes = ((size_t)blen + 1) * sizeof(uint32_t);
if (row_bytes * 2 > (size_t)server.proto_max_bulk_len) {
addReplyError(c, "Insufficient memory, transient memory for LCS exceeds proto-max-bulk-len");
return;
}
uint32_t *prev = ztrycalloc(row_bytes);
uint32_t *curr = ztrycalloc(row_bytes);
if (!prev || !curr) {
if (prev) zfree(prev);
if (curr) zfree(curr);
addReplyError(c, "Insufficient memory, failed allocating transient memory for LCS");
return;
}
for (uint32_t ii = 1; ii <= alen; ii++) {
curr[0] = 0;
for (uint32_t jj = 1; jj <= blen; jj++) {
if (astr[ii - 1] == bstr[jj - 1]) {
curr[jj] = prev[jj - 1] + 1;
} else {
uint32_t v1 = prev[jj];
uint32_t v2 = curr[jj - 1];
curr[jj] = v1 > v2 ? v1 : v2;
}
}
uint32_t *tmp = prev; prev = curr; curr = tmp;
}
addReplyLongLong(c, prev[blen]);
zfree(prev);
zfree(curr);
}
/* LCS key1 key2 [LEN] [IDX] [MINMATCHLEN <len>] [WITHMATCHLEN] */
void lcsCommand(client *c) {
uint32_t i, j;
@ -1032,6 +1080,13 @@ void lcsCommand(client *c) {
goto cleanup;
}
/* Fast path: when only LEN is requested, compute with rolling DP
* to avoid allocating the full O(n*m) table. */
if (getlen && !getidx) {
lcsReplyLength(c, a, b);
goto cleanup;
}
/* Compute the LCS using the vanilla dynamic programming technique of
* building a table of LCS(x,y) substrings. */
uint32_t alen = sdslen(a);