Runtime: .NET Core performance few times slower compared to .NET Framework for Read & Write to File using BinaryFormatter with Mutex

Created on 29 Jun 2018  路  22Comments  路  Source: dotnet/runtime

Hardware specification
Windows Server 2016 Standard
.NET Core 2.1
4 Cores
12 GB RAM

I noticed that the performance for .NET Core is much worst than .NET Framework when i want to achieve thread synchronization while read and write to a file. The following code snippet demonstrate what I want to achieve. Which is at any one time only 1 thread can read the file, change the value and write it back to the file.

Full console based code as requested
```C#
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading;
using System.Threading.Tasks;

namespace MutexTest
{
class Program
{
static void Main(string[] args)
{
var fileName = @"D:PersonalTestFile.txt";
if (!File.Exists(fileName))
{
using (var fs = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite))
{
var formatter = new BinaryFormatter();
formatter.Serialize(fs, new TestClass() { Value1 = "Somevalue", Value2 = 1, Value3 = 1, Value4 = 1, Value5 = 1, Value6 = 1, Value7 = 1 });
}
}

        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();

        Parallel.For(0, 10000, a =>
        {
            TestMethod();
        });
        stopwatch.Stop();

        Console.WriteLine(stopwatch.Elapsed.TotalMilliseconds);
    }

    private static void TestMethod()
    {
        var mutex = default(Mutex);
        var isExistingMutex = false;

        try
        {
            isExistingMutex = Mutex.TryOpenExisting(@"Global\somemutexname", out mutex);
        }
        catch (IOException)
        {
            // expected
        }

        if (!isExistingMutex)
        {
            var createdNew = false;

            mutex = new Mutex(false, @"Global\somemutexname", out createdNew);
        }

        try
        {
            mutex.WaitOne();
        }
        catch (AbandonedMutexException)
        {
            // expected
        }

        var result = default(TestClass);

        using (var fs = new FileStream(@"D:\Personal\TestFile.txt", FileMode.OpenOrCreate, FileAccess.Read, FileShare.ReadWrite))
        {
            var formatter = new BinaryFormatter();
            result = (TestClass)formatter.Deserialize(fs);
        }

        result.Value2 = new Random().Next();

        using (var fs = new FileStream(@"D:\Personal\TestFile.txt", FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite))
        {
            var formatter = new BinaryFormatter();
            formatter.Serialize(fs, result);
        }

        mutex.ReleaseMutex();
        mutex.Dispose();
    }
}

[Serializable]
public class TestClass
{
    public string Value1 { get; set; }
    public int Value2 { get; set; }
    public long Value3 { get; set; }
    public long Value4 { get; set; }
    public long Value5 { get; set; }
    public long Value6 { get; set; }
    public byte Value7 { get; set; }
}

}
```

The same code is used in .NET Core and .NET Framework. This is the result i get for .NET Core and .NET Framework.
Console - Tested using Stopwatch class. Parallel Loop 2,000.
[Framework] 2358 milliseconds (average)
[Core] 7710 milliseconds (average)

Kestrel (Web API) vs .NET Framework (Web API) - Tested using Visual Studio Load Test, run for 1 minute.
[Framework] 2128 request per second (average)
[Core] 774 request per second (average)

Based on the request, can see that .NET Framework outperform .NET Core. So I would like to know is this the expected result in .NET Core? or I have to write it differently compared to .NET Framework? or is it due to Serialization?

area-System.Threading bug tenet-performance

Most helpful comment

No, what I tried to say is that your change is not connected to the regression. It's probably a regression of Mutex or Mutex + BinaryFormatter code.

All 22 comments

Can you please share the full console-based repro that we can run? There's obviously some code missing.

cc @Anipik who has touched these code paths.

http://sscce.org/ please 馃槂

I moved this class from src to shared folder. But there wasn't any logic changes except removing the length constraint on mutex names
https://github.com/dotnet/coreclr/commit/65fc6e978fc21a05878b59ffc016613d9e34bcf6
https://github.com/dotnet/coreclr/commit/422b0e81797d5428812c6ef825e22565e2dc63f3

@Anipik I'm talking about this change: https://github.com/dotnet/corefx/pull/25590

@stephentoub @kasper3
Sorry for the late reply.

The following are the full console based code.

```C#
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading;
using System.Threading.Tasks;

namespace MutexTest
{
class Program
{
static void Main(string[] args)
{
var fileName = @"D:PersonalTestFile.txt";
if (!File.Exists(fileName))
{
using (var fs = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite))
{
var formatter = new BinaryFormatter();
formatter.Serialize(fs, new TestClass() { Value1 = "Somevalue", Value2 = 1, Value3 = 1, Value4 = 1, Value5 = 1, Value6 = 1, Value7 = 1 });
}
}

        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();

        Parallel.For(0, 10000, a =>
        {
            TestMethod();
        });
        stopwatch.Stop();

        Console.WriteLine(stopwatch.Elapsed.TotalMilliseconds);
    }

    private static void TestMethod()
    {
        var mutex = default(Mutex);
        var isExistingMutex = false;

        try
        {
            isExistingMutex = Mutex.TryOpenExisting(@"Global\somemutexname", out mutex);
        }
        catch (IOException)
        {
            // expected
        }

        if (!isExistingMutex)
        {
            var createdNew = false;

            mutex = new Mutex(false, @"Global\somemutexname", out createdNew);
        }

        try
        {
            mutex.WaitOne();
        }
        catch (AbandonedMutexException)
        {
            // expected
        }

        var result = default(TestClass);

        using (var fs = new FileStream(@"D:\Personal\TestFile.txt", FileMode.OpenOrCreate, FileAccess.Read, FileShare.ReadWrite))
        {
            var formatter = new BinaryFormatter();
            result = (TestClass)formatter.Deserialize(fs);
        }

        result.Value2 = new Random().Next();

        using (var fs = new FileStream(@"D:\Personal\TestFile.txt", FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite))
        {
            var formatter = new BinaryFormatter();
            formatter.Serialize(fs, result);
        }

        mutex.ReleaseMutex();
        mutex.Dispose();
    }
}

[Serializable]
public class TestClass
{
    public string Value1 { get; set; }
    public int Value2 { get; set; }
    public long Value3 { get; set; }
    public long Value4 { get; set; }
    public long Value5 { get; set; }
    public long Value6 { get; set; }
    public byte Value7 { get; set; }
}

}
```

Test 1 - Parallel Loop 10,000
Hardware specification
Windows Server 2016 Standard
4 Cores 4 Threads
12 GB RAM

.NET Framework 4.7.1
10,945 milliseconds
10,958 milliseconds
11,203 milliseconds

.NET Core 2.1
35,781 milliseconds
35,596 milliseconds
35,604 milliseconds

Test 2 - Parallel Loop 10,000
Hardware specification
Windows 10 Professional
4 Cores 8 Threads
32GB RAM

.NET Framework 4.6.1
10,520 milliseconds
10,805 milliseconds
10,539 milliseconds

.NET Core 2.0
38,013 milliseconds
38,103 milliseconds
38,249 milliseconds

.NET Core 2.1
37,432 milliseconds
37,901 milliseconds
39,078 milliseconds

Hi guys, any update on this?

@Anipik the regression is not related to https://github.com/dotnet/corefx/commit/190132d89e47d2623372485b207f4371e05529e1 as it is the same with netcoreapp2.0. Seems like the perf regression was in from the start.

Perfview from master which strongly indicates that the regression happens around Mutex:

image

Can you resolve the coreclr/corelib symbols?

@ViktorHofer was there then some issue with how I collected the benchmark data back then ?

No, what I tried to say is that your change is not connected to the regression. It's probably a regression of Mutex or Mutex + BinaryFormatter code.

The netcoreapp version is using about half as much cpu as netfx in the same amount of the time, that explains the throughput difference. This is related to the contention on the mutex. Below is what I captured in the runs:

netcoreapp:
image
image

netfx:
image
image

the performance of the project running under .net core is terrible, mainly due to the application not utilizing threading very well at all.

The application code is the same on .NET Framework - and you're hypothesizing it's related to Mutex? Do you have profiles that support that?

If you can narrow this down it would be interesting to us (.NET Core is pretty much always faster), but for clarity, I suggest opening a new issue.

OK, thanks @manofstick . We usually use PerfView which has a diff feature that may help.

After analysis I found out that I'm an idiot :-) All my problems were just related to the flavour of GC (i.e I hadn't put server GC on), and how to configure it in the brave new world of .net core (and ensuring that those changes flush through)... Deleted old messages to avoid people worrying about nothing...

Here is a very simple example that .net core is much slower than the old .net!

private List DoCalcInParallel(int numberCount)
{
var numbers = Enumerable.Range(3, numberCount - 3);
var parallelQuery = numbers.AsParallel()
.WithExecutionMode(ParallelExecutionMode.ForceParallelism)
.WithMergeOptions(ParallelMergeOptions.NotBuffered)
.Where(n => Enumerable.Range(2, (int)Math.Sqrt(n))
.All(i => n % i != 0))
.OrderBy(i => i);
return parallelQuery.ToList();
}

var stopwatch = Stopwatch.StartNew();
var primeNumbers = this.DoCalcInParallel(20000000);
stopwatch.Stop();

On my machine (release build) with .NET the code runs in 6.2 seconds with .NET Core in 7.6 seconds!

@paradoxon101 can you please open a new issue?

Moving to Future as BinaryFormatter is deprecated in .NET Core. @viktorhofer @adamsitnik feel free to make the case for 3.0.

@Anipik @ViktorHofer This issue hasn't had activity in 2 years. Is there anything for us to do here? It looks like the threading issue was asked and answered in https://github.com/dotnet/runtime/issues/29198, so I don't think this issue is actionable any longer by us.

Closing due to no activity and last comment ^^

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Timovzl picture Timovzl  路  3Comments

matty-hall picture matty-hall  路  3Comments

sahithreddyk picture sahithreddyk  路  3Comments

EgorBo picture EgorBo  路  3Comments

jzabroski picture jzabroski  路  3Comments