perf: add early exit in valueSlice for empty ranges#259
perf: add early exit in valueSlice for empty ranges#259Ayoub-Mabrouk wants to merge 1 commit intojshttp:masterfrom
Conversation
|
I don't think this provides any meaningful change in performance. I included the benchmarks below but it's inconclusive. We'd need to do a more specific benchmark to isolate the changes. At least intuitively, this adds an extra check to every parse when the majority won't be empty. It seems like a de-optimization for the main use-case, and optimizing for an edge case. That said, it can probably be improved but it should be included with benchmarks that show an improvement in the majority of cases. The things you describe as improving are fairly rare, only empty cookie values are likely encountered in the real world and those are going to be massively outnumbered but the amount of cookies that include values. Main: PR: |
|
I tried other variations on this idea and haven't come up with any performance improvement. For example, slicing an empty string vs |
|
I tried taking this change a bit more seriously and wrote up a benchmark but it's inconclusive too: import { describe, bench, expect } from "vitest";
const CASES = [
{
text: "test=example",
start: 5,
end: 12,
expected: "example",
},
{
text: "test=",
start: 5,
end: 5,
expected: "",
},
{
text: "test= example ",
start: 5,
end: 14,
expected: "example",
},
{
text: "test=example ",
start: 5,
end: 13,
expected: "example",
},
{
text: "test= example",
start: 5,
end: 13,
expected: "example",
},
];
function valueSliceDoThenWhileEmptyString(str: string, min: number, max: number) {
let start = min;
let end = max;
do {
const code = str.charCodeAt(start);
if (code !== 0x20 /* */ && code !== 0x09 /* \t */) break;
} while (++start < end);
while (end > start) {
const code = str.charCodeAt(end - 1);
if (code !== 0x20 /* */ && code !== 0x09 /* \t */) break;
end--;
}
return start === end ? "" : str.slice(start, end);
}
function valueSliceDoThenWhile(str: string, min: number, max: number) {
let start = min;
let end = max;
do {
const code = str.charCodeAt(start);
if (code !== 0x20 /* */ && code !== 0x09 /* \t */) break;
} while (++start < end);
while (end > start) {
const code = str.charCodeAt(end - 1);
if (code !== 0x20 /* */ && code !== 0x09 /* \t */) break;
end--;
}
return str.slice(start, end);
}
function valueSliceTwoWhile(str: string, min: number, max: number) {
let start = min;
let end = max;
while (start < end) {
const code = str.charCodeAt(start);
if (code !== 0x20 /* */ && code !== 0x09 /* \t */) break;
start++;
}
while (end > start) {
const code = str.charCodeAt(end - 1);
if (code !== 0x20 /* */ && code !== 0x09 /* \t */) break;
end--;
}
return str.slice(start, end);
}
function valueSliceTwoDoWhile(str: string, min: number, max: number) {
let start = min;
let end = max;
do {
const code = str.charCodeAt(start);
if (code !== 0x20 /* */ && code !== 0x09 /* \t */) break;
} while (++start < end);
do {
const code = str.charCodeAt(end - 1);
if (code !== 0x20 /* */ && code !== 0x09 /* \t */) break;
} while (--end > start);
return str.slice(start, end);
}
function valueSliceDoThenWhileExitEarly(str: string, min: number, max: number) {
if (min === max) return "";
let start = min;
let end = max;
do {
const code = str.charCodeAt(start);
if (code !== 0x20 /* */ && code !== 0x09 /* \t */) break;
} while (++start < end);
while (end > start) {
const code = str.charCodeAt(end - 1);
if (code !== 0x20 /* */ && code !== 0x09 /* \t */) break;
end--;
}
return str.slice(start, end);
}
function valueSliceThenTrim(str: string, min: number, max: number) {
return str.slice(min, max).trim();
}
describe.each(CASES)("valueSlice $text", ({ text, start, end, expected }) => {
bench("do...while then while", () => {
expect(valueSliceDoThenWhile(text, start, end)).toBe(expected);
});
bench("two while", () => {
expect(valueSliceTwoWhile(text, start, end)).toBe(expected);
});
bench("two do...while", () => {
expect(valueSliceTwoDoWhile(text, start, end)).toBe(expected);
});
bench("do...while then while (exit early)", () => {
expect(valueSliceDoThenWhileExitEarly(text, start, end)).toBe(expected);
});
bench("do...while then while (empty string check)", () => {
expect(valueSliceDoThenWhileEmptyString(text, start, end)).toBe(expected);
});
bench("slice then trim", () => {
expect(valueSliceThenTrim(text, start, end)).toBe(expected);
});
});I think the only takeaway from this is that |
|
I opened a PR with everything I did to review this one here: #261. This took me a lot of time and I don't think I'll be doing it again for a small tweak like this. In the future, it'd be greatly appreciated if you have a performance optimization to include some level of testing and benchmarking to prove that it hasn't impacted existing code and has actually improved things, as well as other possibilities you've tested (basically what I did above and in the PR). Once this code combines with everything else and real world tests I think it ends up being basically non-existent still, though it does look like maybe a 10% bump is achieved in the empty string case while the negative impact to other methods was less than I expected. |
add early return when min >= max to avoid unnecessary processing for empty string ranges.
This handles edge cases like empty cookie values, empty string inputs, empty attribute values, and consecutive semicolons.
This optimization avoids character code checks and loop iterations when the slice range is empty or invalid.