From 24bd0733028be8ced94df1c997fbef3f2c69c040 Mon Sep 17 00:00:00 2001 From: Richard Webb Date: Sun, 26 Apr 2026 11:42:11 +0100 Subject: [PATCH] perf(Core): Reduce zero length array allocations Running the Visual Studio memory analyzer against the benchmarks project reveals that the current code is allocating a massive number of zero length arrays (over 3 million in that one benchmark). Many of these are from the Array functions in FSharp.Core always allocating a new empty array for an empty input. There are enough of these that avoiding some calls to Array.map with zero length inputs give a large reduction in memory allocations. Benchmarks with the current code: | Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | |--------------- |---------:|--------:|--------:|-----------:|----------:|----------:|----------:| | LintParsedFile | 878.0 ms | 4.17 ms | 3.70 ms | 25000.0000 | 8000.0000 | 1000.0000 | 396.83 MB | Benchmarks with this change: | Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | |--------------- |---------:|---------:|---------:|-----------:|----------:|----------:|----------:| | LintParsedFile | 865.0 ms | 12.17 ms | 10.79 ms | 20000.0000 | 7000.0000 | 1000.0000 | 321.69 MB | --- src/FSharpLint.Core/Framework/Rules.fs | 6 +++--- src/FSharpLint.Core/Framework/Utilities.fs | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/FSharpLint.Core/Framework/Rules.fs b/src/FSharpLint.Core/Framework/Rules.fs index d53629800..74dfb5f70 100644 --- a/src/FSharpLint.Core/Framework/Rules.fs +++ b/src/FSharpLint.Core/Framework/Rules.fs @@ -97,12 +97,12 @@ let toWarning (identifier:string) (ruleName:string) (filePath:string) (lines:str let runAstNodeRule (rule:RuleMetadata) (config:AstNodeRuleParams) = rule.RuleConfig.Runner config - |> Array.map (toWarning rule.Identifier rule.Name config.FilePath config.Lines) + |> Array.mapIfNotEmpty (toWarning rule.Identifier rule.Name config.FilePath config.Lines) let runLineRuleWithContext (rule:RuleMetadata>) (context:'Context) (config:LineRuleParams) = rule.RuleConfig.Runner context config - |> Array.map (toWarning rule.Identifier rule.Name config.FilePath config.Lines) + |> Array.mapIfNotEmpty (toWarning rule.Identifier rule.Name config.FilePath config.Lines) let runLineRule (rule:RuleMetadata) (config:LineRuleParams) = rule.RuleConfig.Runner config - |> Array.map (toWarning rule.Identifier rule.Name config.FilePath config.Lines) + |> Array.mapIfNotEmpty (toWarning rule.Identifier rule.Name config.FilePath config.Lines) diff --git a/src/FSharpLint.Core/Framework/Utilities.fs b/src/FSharpLint.Core/Framework/Utilities.fs index c709e1404..8cc98d00c 100644 --- a/src/FSharpLint.Core/Framework/Utilities.fs +++ b/src/FSharpLint.Core/Framework/Utilities.fs @@ -23,6 +23,14 @@ module Dictionary = dict.Add(key, value) +module Array = + + let inline mapIfNotEmpty ([] mapping: 'Source -> 'Dest) (array: 'Source array) = + if Array.isEmpty array then + Array.empty + else + Array.map mapping array + module ExpressionUtilities = open System