Skip to content

FileStream Flush(true) significant performance degradation in .NET 8 when buffer size is bigger than the payload #100229

Closed

Description

Description

The original issue was identified in apache/lucenenet#933; nevertheless, after deeper analysis, the root cause was narrowed down to the FileStream class, pointing towards a framework issue.

Below is the source code that reproduces the issue.

namespace Emy.Benchmark;

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;

[SimpleJob(RuntimeMoniker.Net70, baseline: true)]
[SimpleJob(RuntimeMoniker.Net80)]
[GcServer(true)]
public class FileStreamBenchmarks
{
    private byte[] buffer = null!;

    [GlobalSetup]
    public void Setup()
    {
        if (Directory.Exists("test_dir"))
            Directory.Delete("test_dir", true);
        Directory.CreateDirectory("test_dir");
        
        buffer = PayloadSize.RandomBytes();
    }
    
    [Params(512, 1024, 4096)]
    public int PayloadSize { get; set; }
    
    [Params(1024, 4096, 8192)]
    public int BufferSize { get; set; }

    [Benchmark] 
    public int WriteStream() =>  WriteData(buffer, BufferSize);
    
    private static int WriteData(byte[] buffer, int bufferSize)
    {
        using var stream = 
            new FileStream(
                path: Path.Combine("test_dir", $"test.{buffer.Length}.bin"),
                mode: FileMode.OpenOrCreate,
                access: FileAccess.Write,
                share: FileShare.ReadWrite,
                bufferSize: bufferSize);
        stream.Write(buffer, 0, buffer.Length);
        stream.Flush(flushToDisk: true);
        return buffer.Length;
    }
}

public static class Bytes
{
    public static byte[] RandomBytes(this int self)
    {
        byte[] buffer = new byte[self];
        Random.Shared.NextBytes(buffer);
        return buffer;
    }
}

Project configuration

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
        <PackageReference Include="BenchmarkDotNet.Diagnostics.dotTrace" Version="0.13.12" />
    </ItemGroup>

</Project>

Configuration

  • Which version of .NET is the code running on?
    .NET 8, or .NET 9
  • What OS version, and what distro if applicable?
    macOS
  • What is the architecture (x64, x86, ARM, ARM64)?
    ARM64

Regression?

The issue is reproducible in .NET 8 and .NET 9.

Data

Notice when the buffer size is bigger than the payload, performance degrades ~150x.

BenchmarkDotNet v0.13.12, macOS Sonoma 14.4 (23E214) [Darwin 23.4.0]
Apple M2 Max, 1 CPU, 12 logical and 12 physical cores
.NET SDK 9.0.100-preview.2.24157.14
  [Host]   : .NET 8.0.3 (8.0.324.11423), Arm64 RyuJIT AdvSIMD
  .NET 7.0 : .NET 7.0.5 (7.0.523.17405), Arm64 RyuJIT AdvSIMD
  .NET 8.0 : .NET 8.0.3 (8.0.324.11423), Arm64 RyuJIT AdvSIMD

Server=True  

| Method      | Job      | Runtime  | PayloadSize | BufferSize | Mean        | Error     | StdDev     | Ratio  | RatioSD |
|------------ |--------- |--------- |------------ |----------- |------------:|----------:|-----------:|-------:|--------:|
| WriteStream | .NET 7.0 | .NET 7.0 | 512         | 1024       |    28.90 μs |  0.330 μs |   0.292 μs |   1.00 |    0.00 |
| WriteStream | .NET 8.0 | .NET 8.0 | 512         | 1024       | 4,416.57 μs | 87.109 μs | 196.619 μs | 154.81 |    5.78 |
|             |          |          |             |            |             |           |            |        |         |
| WriteStream | .NET 7.0 | .NET 7.0 | 512         | 4096       |    27.75 μs |  0.550 μs |   1.184 μs |   1.00 |    0.00 |
| WriteStream | .NET 8.0 | .NET 8.0 | 512         | 4096       | 4,298.04 μs | 85.603 μs | 149.926 μs | 155.93 |   10.91 |
|             |          |          |             |            |             |           |            |        |         |
| WriteStream | .NET 7.0 | .NET 7.0 | 512         | 8192       |    28.50 μs |  0.352 μs |   0.329 μs |   1.00 |    0.00 |
| WriteStream | .NET 8.0 | .NET 8.0 | 512         | 8192       | 4,423.72 μs | 87.476 μs | 225.804 μs | 150.71 |    5.80 |
|             |          |          |             |            |             |           |            |        |         |
| WriteStream | .NET 7.0 | .NET 7.0 | 1024        | 1024       | 4,289.43 μs | 85.522 μs | 142.889 μs |   1.00 |    0.00 |
| WriteStream | .NET 8.0 | .NET 8.0 | 1024        | 1024       | 4,466.22 μs | 87.752 μs | 173.214 μs |   1.05 |    0.05 |
|             |          |          |             |            |             |           |            |        |         |
| WriteStream | .NET 7.0 | .NET 7.0 | 1024        | 4096       |    28.15 μs |  0.550 μs |   0.949 μs |   1.00 |    0.00 |
| WriteStream | .NET 8.0 | .NET 8.0 | 1024        | 4096       | 4,416.73 μs | 84.027 μs | 210.808 μs | 159.07 |    9.01 |
|             |          |          |             |            |             |           |            |        |         |
| WriteStream | .NET 7.0 | .NET 7.0 | 1024        | 8192       |    29.14 μs |  0.257 μs |   0.241 μs |   1.00 |    0.00 |
| WriteStream | .NET 8.0 | .NET 8.0 | 1024        | 8192       | 4,597.65 μs | 86.784 μs | 209.593 μs | 158.45 |    9.97 |
|             |          |          |             |            |             |           |            |        |         |
| WriteStream | .NET 7.0 | .NET 7.0 | 4096        | 1024       | 4,353.85 μs | 85.278 μs | 166.328 μs |   1.00 |    0.00 |
| WriteStream | .NET 8.0 | .NET 8.0 | 4096        | 1024       | 4,204.54 μs | 75.218 μs | 110.254 μs |   0.97 |    0.05 |
|             |          |          |             |            |             |           |            |        |         |
| WriteStream | .NET 7.0 | .NET 7.0 | 4096        | 4096       | 4,257.61 μs | 83.921 μs | 133.108 μs |   1.00 |    0.00 |
| WriteStream | .NET 8.0 | .NET 8.0 | 4096        | 4096       | 4,431.81 μs | 88.581 μs | 198.124 μs |   1.04 |    0.05 |
|             |          |          |             |            |             |           |            |        |         |
| WriteStream | .NET 7.0 | .NET 7.0 | 4096        | 8192       |    28.78 μs |  0.148 μs |   0.138 μs |   1.00 |    0.00 |
| WriteStream | .NET 8.0 | .NET 8.0 | 4096        | 8192       | 4,187.94 μs | 77.722 μs |  68.898 μs | 145.63 |    2.46 |

Also, if we disable the buffering altogether, bufferSize: 0, then the performance between .NET 7 and .NET 8 is on par.

Analysis

After enabling tracing, it seems the problem is in the fsync() call; at least, this is the difference between buffered, non-buffered and buffer size less than the payload stack trace.

Buffered (buffer size larger than the payload) stack trace (slow performance):
Screenshot 2024-03-24 at 6 20 30 PM

Non-buffered stack trace (fast performance):
Screenshot 2024-03-24 at 7 04 19 PM

Buffered (buffer size less than payload) (fast performance):
Screenshot 2024-03-24 at 8 53 02 PM

In summary, the DotNet Framework seems to call the fsync() method when the buffer is bigger than the payload, which leads to severe performance degradation.

@paulirwin, @eladmarg, @jeme

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions