You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Directory.EnumerateFiles regressed in .NET 8 #110754
My team observed a regression for the performance of Directory.EnumerateFiles in .NET 8. We store a large number of database redo logs in a folder, named with a prefix and a log generation number (e.g., E10ABCD1234.log). Our goal is to determine the maximum log generation number efficiently.
To achieve this, we have developed a fast search algorithm that looks for the highest generation log files in a hierarchical manner, starting from E10F*******.log to E100*******.log and so on, until the last digit. We use Directory.EnumerateFiles(directory, filter) to detect matching files.
We compared the performance of our search algorithm against a direct enumeration of all files to get the maximum generation number.
The direct enumeration takes approximately 0.5 seconds in both .Net Framework & .Net 8.
Our search algorithm is significantly faster with a baseline of 4 milliseconds for 400,000 log files in .Net Framework, but it degrades a lot in .Net 8. Below are the detailed performance metrics.
Runtime
# of Log
Elapsed Time
Enum Times
.NET Framework 4.8.9282.0
1000
00:00:00.0040757
99
.NET Framework 4.8.9282.0
10000
00:00:00.0051796
115
.NET Framework 4.8.9282.0
100000
00:00:00.0039643
101
.NET Framework 4.8.9282.0
400000
00:00:00.0042406
101
.NET 8.0.11
1000
00:00:00.0341829
99
.NET 8.0.11
10000
00:00:00.3642297
115
.NET 8.0.11
100000
00:00:02.7944484
101
.NET 8.0.11
400000
00:00:11.4074592
101
Previously in .Net framework it uses FindFirstFile which takes the search filter to find out the first / next file.
This API changed in .Net Core/6/8 implementation,
Directory.EnumerateFiles enumerates all files and see if current one match the prefix. So, this means FindHighestGenerationLogFileFastV2 scans the whole directory for many times.
@Tjianke can you please share how can I repro this regression? .NET 9 seems to perform even better than .NET Framework 4.8.1 based on what I tried.
Log generation:
// write 400k log files with the following format E10ABCD1234.log, where ABCD is a 2 hex pair number and 1234 is a 4 digit decimal numberinthexPair=0xF000;// Start from F000 for the hex pair (ABCD)intdecimalNumber=0;// Start from 0 for the decimal number (1234)for(inti=0;i<400_000;i++){stringhexPairFormatted=hexPair.ToString("X4");// Format hex as 4 digitsstringdecimalNumberFormatted=decimalNumber.ToString("D4");// Format decimal as 4 digitsstringfileName=$"E10{hexPairFormatted}{decimalNumberFormatted}.log";File.Create(fileName).Dispose();Console.WriteLine("Created file: "+fileName);decimalNumber++;if(decimalNumber>9999){decimalNumber=0;hexPair++;}}
@jozkee IIUC the regression is when you use a search pattern other than just *, like E10FF*.log. .NET Framework would pass the search pattern to Windows, which would pass it to the file system driver; but .NET Core enumerates all files in the directory and compares their names to the search pattern itself, so there is more copying of data from kernel space to user space and perhaps even more disk I/O.
@KalleOlaviNiemitalo I see, thanks. The more the searchPattern narrows it down, the larger the regression looks.
In .NET Core, the interop overhead remains the same between * and e.g. E10F0000000.log, while in .NET FW, it decreases drastically because the filter is passed to the Windows API.
Even increasing the buffer size so it reduces the number of calls to NtQueryDirectoryFile doesn't reduce that much the time spent in it, so I suspect the large chunk of time doesn't come from many jumps between managed and native but from filling the buffer.
Activity
dotnet-policy-service commentedon Dec 16, 2024
Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.
stephentoub commentedon Dec 16, 2024
cc: @JeremyKuhne
KalleOlaviNiemitalo commentedon Dec 17, 2024
There is previous discussion in #31214.
jeffhandley commentedon Jan 16, 2025
Assigned to @jozkee to finish triaging this.
jozkee commentedon Jan 17, 2025
May be worth exploring #31214 (comment).
jozkee commentedon Feb 25, 2025
@Tjianke can you please share how can I repro this regression? .NET 9 seems to perform even better than .NET Framework 4.8.1 based on what I tried.
Log generation:
Benchmark:
Results:
9.0:
4.8.1:
KalleOlaviNiemitalo commentedon Feb 25, 2025
@jozkee IIUC the regression is when you use a search pattern other than just
*, likeE10FF*.log. .NET Framework would pass the search pattern to Windows, which would pass it to the file system driver; but .NET Core enumerates all files in the directory and compares their names to the search pattern itself, so there is more copying of data from kernel space to user space and perhaps even more disk I/O.jozkee commentedon Mar 12, 2025
@KalleOlaviNiemitalo I see, thanks. The more the
searchPatternnarrows it down, the larger the regression looks.In .NET Core, the interop overhead remains the same between
*and e.g.E10F0000000.log, while in .NET FW, it decreases drastically because the filter is passed to the Windows API.Even increasing the buffer size so it reduces the number of calls to
NtQueryDirectoryFiledoesn't reduce that much the time spent in it, so I suspect the large chunk of time doesn't come from many jumps between managed and native but from filling the buffer.jeffhandley commentedon Mar 3, 2026
This should be addressed by #122947 and #123695, included in .NET 11 Preview 1.