Path.GetFileName performance improvements in .NET 8 versus .NET 7




Date Added (UTC):

07 Apr 2024 @ 21:23

Date Updated (UTC):

07 Apr 2024 @ 21:35


.NET Version(s):

.NET 7 .NET 8

Tag(s):

#.Net8PerfImprovement #FileIO


Added By:
Profile Image

Blog   
Ireland    
.NET Developer and tech lead from Ireland!

Benchmark Results:





Benchmark Code:



using BenchmarkDotNet.Attributes;
using System.IO;
using System;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Environments;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Reports;


[Config(typeof(Config))]
[HideColumns(Column.Job, Column.RatioSD, Column.AllocRatio)]
[MemoryDiagnoser]
public class GetFileNameBenchmark
{
    private string _path = Path.Join(Path.GetTempPath(), "SomeFileName.cs");

    [Benchmark]
    public ReadOnlySpan<char> GetFileName() => Path.GetFileName(_path.AsSpan());

    private class Config : ManualConfig
    {
        public Config()
        {
            AddJob(Job.Default.WithId(".NET 7").WithRuntime(CoreRuntime.Core70).AsBaseline());
            AddJob(Job.Default.WithId(".NET 8").WithRuntime(CoreRuntime.Core80));

            SummaryStyle =
                SummaryStyle.Default.WithRatioStyle(RatioStyle.Percentage);
        }
    }
}

// .NET 7, .NET 8
public ReadOnlySpan<char> GetFileName()
{
    return Path.GetFileName(MemoryExtensions.AsSpan(_path));
}

// .NET 7
.method public hidebysig 
    instance valuetype [System.Runtime]System.ReadOnlySpan`1<char> GetFileName () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 1d 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x207f
    // Code size 17 (0x11)
    .maxstack 8

    // sequence point: (line 30, col 48) to (line 30, col 80) in _
    IL_0000: ldarg.0
    IL_0001: ldfld string GetFileNameBenchmark::_path
    IL_0006: call valuetype [System.Runtime]System.ReadOnlySpan`1<char> [System.Memory]System.MemoryExtensions::AsSpan(string)
    IL_000b: call valuetype [System.Runtime]System.ReadOnlySpan`1<char> [System.Runtime]System.IO.Path::GetFileName(valuetype [System.Runtime]System.ReadOnlySpan`1<char>)
    IL_0010: ret
}

// .NET 8
.method public hidebysig 
    instance valuetype [System.Runtime]System.ReadOnlySpan`1<char> GetFileName () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 1d 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x2050
    // Code size 17 (0x11)
    .maxstack 8

    // sequence point: (line 30, col 48) to (line 30, col 80) in _
    IL_0000: ldarg.0
    IL_0001: ldfld string GetFileNameBenchmark::_path
    IL_0006: call valuetype [System.Runtime]System.ReadOnlySpan`1<char> [System.Memory]System.MemoryExtensions::AsSpan(string)
    IL_000b: call valuetype [System.Runtime]System.ReadOnlySpan`1<char> [System.Runtime]System.IO.Path::GetFileName(valuetype [System.Runtime]System.ReadOnlySpan`1<char>)
    IL_0010: ret
}

// .NET 7 (X64)
GetFileName()
    L0000: push rsi
    L0001: sub rsp, 0x30
    L0005: xor eax, eax
    L0007: mov [rsp+0x20], rax
    L000c: mov rsi, rdx
    L000f: mov rcx, [rcx+8]
    L0013: test rcx, rcx
    L0016: je short L003f
    L0018: lea rdx, [rcx+0xc]
    L001c: mov eax, [rcx+8]
    L001f: mov [rsp+0x20], rdx
    L0024: mov [rsp+0x28], eax
    L0028: mov rcx, rsi
    L002b: lea rdx, [rsp+0x20]
    L0030: call qword ptr [0x7ffbc8d1ba68]
    L0036: mov rax, rsi
    L0039: add rsp, 0x30
    L003d: pop rsi
    L003e: ret
    L003f: xor edx, edx
    L0041: xor eax, eax
    L0043: jmp short L001f
// .NET 8 (X64)
GetFileName()
    L0000: push rbx
    L0001: sub rsp, 0x30
    L0005: xor eax, eax
    L0007: mov [rsp+0x20], rax
    L000c: mov rbx, rdx
    L000f: mov rcx, [rcx+8]
    L0013: test rcx, rcx
    L0016: jne short L001e
    L0018: xor edx, edx
    L001a: xor eax, eax
    L001c: jmp short L0025
    L001e: lea rdx, [rcx+0xc]
    L0022: mov eax, [rcx+8]
    L0025: mov [rsp+0x20], rdx
    L002a: mov [rsp+0x28], eax
    L002e: mov rcx, rbx
    L0031: lea rdx, [rsp+0x20]
    L0036: call qword ptr [0x7ffc8a11c0c0]
    L003c: mov rax, rbx
    L003f: add rsp, 0x30
    L0043: pop rbx
    L0044: ret


Benchmark Description:


[PR](https://github.com/dotnet/runtime/pull/75318)

The provided benchmark code is designed to measure the performance of retrieving the file name from a file path using the `Path.GetFileName` method in .NET, specifically comparing its performance across different .NET versions. The benchmark is set up using BenchmarkDotNet, a powerful .NET library for benchmarking code performance. Let's break down the setup and the rationale behind the benchmark method. ### General Setup - **BenchmarkDotNet Attributes**: The code uses several BenchmarkDotNet attributes to configure the benchmark. `[Config(typeof(Config))]` specifies the configuration class to use. `[HideColumns]` hides specific columns in the output report for clarity. `[MemoryDiagnoser]` enables memory diagnostics to measure memory allocations. - **.NET Versions**: The benchmark is configured to run against two different .NET versions, .NET 7 and .NET 8, allowing for a direct comparison of performance across these versions. This is achieved through the `Config` class, which specifies two jobs with different runtimes (`CoreRuntime.Core70` and `CoreRuntime.Core80`). - **Configuration Class (`Config`)**: This class extends `ManualConfig` from BenchmarkDotNet and is used to customize the benchmark's configuration. It adds two jobs for .NET 7 and .NET 8, sets one as the baseline for comparison, and customizes the summary style to display ratios in percentage. ### Benchmark Method: `GetFileName` - **Purpose**: The `GetFileName` method benchmarks the performance of extracting the file name from a file path using the `Path.GetFileName` method. This operation is common in file handling and processing scenarios, making its performance critical for applications that deal with file systems extensively. - **Performance Aspect**: This benchmark specifically measures the time it takes to execute `Path.GetFileName` on a `ReadOnlySpan<char>` representing a file path. The use of `ReadOnlySpan<char>` is noteworthy as it represents a more modern approach to handling text in .NET, designed for performance by avoiding unnecessary allocations. - **Why It's Important**: Understanding the performance of file path manipulation methods is crucial for optimizing file I/O operations. Faster execution times can lead to significant performance improvements in applications that frequently access the file system. Additionally, comparing performance across .NET versions helps developers make informed decisions about upgrading their applications to take advantage of performance improvements in newer versions. - **Expected Results/Insights**: By running this benchmark, you should expect to see how the performance of `Path.GetFileName` varies between .NET 7 and .NET 8. Ideally, newer .NET versions would offer improved performance due to optimizations in the runtime and libraries. The memory diagnostics enabled by `[MemoryDiagnoser]` will also provide insights into any memory allocations that occur during the execution of the method, which is important for understanding the overall efficiency of the operation. In summary, this benchmark is designed to provide a focused comparison of the performance of a common file path manipulation operation across different .NET versions, offering valuable insights for developers looking to optimize file handling in their applications.


Benchmark Comments: