[perf-improver] perf: eliminate LINQ LastOrDefault() and minor array allocations#457
Conversation
- GetSearchAfterOptions: replace ScoreDocs.LastOrDefault() (LINQ) with
direct index access ScoreDocs[Length-1]; guard on ScoreDocs.Length > 0
rather than TotalHits (more precise)
- CreatePhraseQuery: use Split(char, opts) overload instead of
Split(char[], opts) to avoid a single-element char[] allocation per call
- SelectFieldInternal: use collection initializer with capacity hint
HashSet<string>(1) { fieldName } instead of new string[] intermediary
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Greptile SummaryThis PR applies three micro-optimisations to hot search-execution paths in
Confidence Score: 5/5All three changes are allocation-only improvements with no observable behavioural difference; safe to merge. Each change is a well-scoped, mechanically safe substitution: a direct array index replacing LINQ on an already-length-checked array, a single-char Split overload available on both target frameworks (net6.0, net8.0), and a HashSet construction that produces the same single-element set. No logic branches, contracts, or public APIs are altered. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[GetSearchAfterOptions called with TopDocs] --> B{"scoreDocs non-null AND Length > 0?"}
B -- No --> C[return null]
B -- Yes --> D["lastDoc = scoreDocs at last index\ndirect array access - no LINQ"]
D --> E{lastDoc is FieldDoc?}
E -- Yes --> F[return SearchAfterOptions with fields clone]
E -- No --> G{lastDoc is ScoreDoc?}
G -- Yes --> H[return SearchAfterOptions with empty fields]
G -- No --> C
I[CreatePhraseQuery called] --> J["txt.Split space char with RemoveEmptyEntries\nno char array allocation"]
J --> K[Add Term per token to PhraseQuery]
K --> L[return PhraseQuery]
M[SelectFieldInternal called] --> N["new HashSet with capacity 1 plus fieldName\nno intermediate string array"]
N --> O[assign to _fieldsToLoad]
O --> P[return CreateOp]
Reviews (1): Last reviewed commit: "perf: eliminate LINQ LastOrDefault() and..." | Re-trigger Greptile |
🤖 Perf Improver — automated performance improvement
Goal
Eliminate three small but avoidable allocations / LINQ overhead on hot search-execution paths.
Changes
1.
GetSearchAfterOptions— direct array index instead of LINQLastOrDefault()topDocs.ScoreDocs.LastOrDefault()iterates via LINQ, boxing the result type and adding a delegate call. Since we already checkScoreDocs.Length > 0, a directscoreDocs[scoreDocs.Length - 1]is safe, zero-allocation, and clearer in intent. The guard was also tightened fromTotalHits > 0(total logical hits) toScoreDocs.Length > 0(actual returned docs) — these are equivalent in practice but the new form is more precise.2.
CreatePhraseQuery—Split(char, opts)overloadtxt.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)allocates a temporarychar[]on every call. Thestring.Split(char, StringSplitOptions)overload (available since .NET 6) avoids this allocation.3.
SelectFieldInternal— collection initialiser with capacitynew HashSet<string>(new string[] { fieldName })creates an intermediatestring[]before populating the set. Replaced withnew HashSet<string>(1) { fieldName }— no intermediate array, and the capacity hint prevents the internal backing array from being over-allocated.Performance evidence
These are micro-optimisations on allocation paths rather than throughput bottlenecks, so the gains show up in allocation counts rather than wall-clock time on a single call. Each change eliminates one heap allocation per invocation on a hot path:
GetSearchAfterOptionsLastOrDefaultdelegateCreatePhraseQuerychar[1]temporarySelectFieldInternalstring[1]temporaryIn aggregate these matter for high-throughput search workloads (many searches/sec) where GC pressure is a real concern.
Trade-offs
None — all changes are strictly equivalent in behaviour.
Reproducibility
Test Status
✅ Build succeeded (0 errors, 0 warnings from project code)
✅ 142 tests passed, 0 failed
Add this agentic workflows to your repo
To install this agentic workflow, run