hadashiA / VYaml

The extra fast, low memory footprint YAML library for C#, focued on .NET and Unity.
MIT License
295 stars 16 forks source link

Severe performance degradation with usage of `ConcurrentQueue<Scalar>` when multi-thread #105

Open UlyssesWu opened 2 months ago

UlyssesWu commented 2 months ago

https://github.com/hadashiA/VYaml/blob/81abc25d9f5d8250028cf09d8a68a59a3d2cd649/VYaml/Internal/Scalar.cs#L11-L24

I have a tool to deserialize all assets from my unity project. It's basically calling YamlSerializer.DeserializeMultipleDocumentsAsync combined with Parallel.ForEach. When I upgraded to the latest VYaml (v0.26) I have observed very obvious performance lost. The process time increased from 1min to 4min.

It seems that the recently imported ConcurrentQueue<Scalar>.TryDequeue(...) for ScalarPool.Rent() caused the problem. image

It's spending too much time on SpinWait. I guess the usage of ConcurrentQueue is thread safe but not multi-thread friendly. 🤔 image

And, this is the time cost for the same input, before the ConcurrentQueue was introduced: image

hadashiA commented 2 months ago

Thank you for your report. I will consider this when I get around to it..

However, if you can, I would be grateful if you could provide me with Benchmark code or similar that can reproduce your problem. Thanks.

UlyssesWu commented 2 months ago

Code: (for demo I just read the same file for many times; for actual usage I'll read each asset in project)

using System.Diagnostics;
using VYaml.Serialization;

namespace VYamlBench
{
    internal class Program
    {
        static async Task Main(string[] args)
        {
            var sw = Stopwatch.StartNew();
            await Test();
            sw.Stop();
            Console.WriteLine($"Elapsed: {sw.ElapsedMilliseconds}ms");
        }

        static async Task Test()
        {
            var path = "Level.prefab";
            await Parallel.ForAsync(0, 1000, async (_, token) =>
            {
                await using var fs = new FileStream(path,
                    new FileStreamOptions()
                        { Access = FileAccess.Read, Options = FileOptions.Asynchronous | FileOptions.SequentialScan });

                try
                {
                    await YamlSerializer.DeserializeMultipleDocumentsAsync<dynamic>(fs, YamlSerializerOptions.Standard);
                }
                catch (Exception)
                {
                    Console.WriteLine($"Parsing error");
                }
            });
        }
    }
}

YAML Sample: (borrowed from an open-source unity project) https://github.com/imengyu/Ballance/blob/main/Assets/Game/Levels/%E9%AD%94%E8%84%93%E7%A9%BA%E9%97%B4%E7%AB%99/Level.prefab

With v0.26.0: Elapsed: 42569ms With v0.14.1: Elapsed: 33275ms

hadashiA commented 1 month ago

108 may have improved things a little.

Here are the results of a run in my environment with the loop count set from 1000 to 100.

UlyssesWu commented 1 month ago

It's improved, but still worse than 0.14.1.

Test 0.14.1 0.26.0 with PR
VYamlBench (1000 loops) (ms) 30803 43672 39051
My Actual Usage (min) 2:07 4:00+ 3:17