Огромная разница в производительности с HashSet между .NET Framework 4.8 и .NET 5

У меня проблема, когда приложение работает очень медленно, потому что есть события с тысячами прикрепленных к нему обработчиков, и удаление этих обработчиков происходит очень медленно. Мое приложение работает как процесс .NET Framework 4.8,

Я написал тестовую программу (но с .NET 5), в которой я заменил событие собственной реализацией, в которой я использовал HashSet для хранения обработчиков и сам реализовал добавление / удаление обработчиков события.

Мое тестовое приложение работало отлично, но после применения этого решения к моему реальному приложению производительность все еще была плохой.

Затем я понял, что причина, по-видимому, заключается в разнице во времени выполнения. Следующий пример программы работает быстро (примерно 1 секунда в .NET 5, но работает несколько минут при запуске того же кода в приложении .NET Framework 4.8):

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;

namespace EventPerformaneMeasurement
{
    class Program
    {
        private static void Main()
        {
            var sw = new Stopwatch();
            var mc = new MyClass();
            var list = Enumerable.Range(1, 1_000_000).Select(x => new PropertyChangedEventHandler((_, _) => { Console.WriteLine($"Handler {x} called"); })).ToList();
            
            sw.Restart();
            list.ForEach(handler=>mc.PropertyChangedNew += handler); 
           
            sw.Stop();
            Console.WriteLine(sw.ElapsedMilliseconds);
            
            sw.Restart();
            list.ForEach(handler=>mc.PropertyChangedNew -= handler); 
            
            sw.Stop();
            Console.WriteLine(sw.ElapsedMilliseconds);
        }
    }

    class MyClass
    {
        private HashSet<PropertyChangedEventHandler> _handlers = new();
        
        public event PropertyChangedEventHandler PropertyChangedNew
        {
            add => _handlers.Add(value);
            remove => _handlers.Remove(value);
        }
    }
}

Я создал две скрипки .NET для воспроизведения этого поведения, скрипка .NET потребовала от меня немного изменить синтаксис моего примера кода и уменьшить количество итераций со 1000000 до 15000, но разница в производительности все еще видна:

Я не могу поверить, что существует такая большая разница между .NET 5 и .NET Framework 4.x в отношении производительности HashSet.

Кто-нибудь знает больше об этом эффекте или где я, может быть, застрял? Есть ли способ получить такую ​​же производительность в .NET 4.8 другим способом?


person Dirk Huber    schedule 23.12.2020    source источник
comment
Вы должны начать с того факта, что .NET Core / .NET 5 намного быстрее и легче, чем .NET Framework во многих областях, поскольку большие части были переписаны с нуля. Не уверен, что здесь можно сделать слишком много   -  person Camilo Terevinto    schedule 23.12.2020
comment
Вы запускаете его без подключенного отладчика? (Ctrl + F5 из Visual Studio)   -  person xanatos    schedule 23.12.2020
comment
Я тестировал без отладчика, поэтому отладчик не был подключен. И если вы посмотрите на две скрипки, вы увидите прибл. коэффициент 4000, так что 2 мс против 8 секунд! Я очень удивлен, что этот вариант использования .NET Framework должен быть таким медленным ...   -  person Dirk Huber    schedule 23.12.2020
comment
HashTable критически зависит от хорошей реализации GetHashCode (). Сравните .NETCore в .NETFramework. Обратите внимание на исправление ошибки и менее чем идеальный обходной путь GetType (). GetHashCode () ужасен, поскольку дает каждому объекту-делегату один и тот же хэш-код. Вероятно, это было в их списке необходимых исправлений, изменение реализации .NETFramework слишком рискованно.   -  person Hans Passant    schedule 23.12.2020
comment
Привет, Ганс, спасибо. Ваш ответ неверен. Это не решает мою проблему, но отвечает на мой вопрос, действительно ли такая проблема с производительностью существует в .NET Framework. Если вы поставите свой комментарий в качестве ответа, я отмечу его как правильный ответ.   -  person Dirk Huber    schedule 23.12.2020


Ответы (1)


В этом случае и поскольку я не могу дождаться, пока мы сможем переключиться с нашим продуктом на .NET 5, мне нужно будет провести рефакторинг, чтобы не использовать события / делегаты, и вместо этого использовать Шаблон Oberserver с интерфейсами и добавлением / удалением методов прослушивателя. Такие объекты-наблюдатели не будут иметь таких слабых реализаций GetHashCode, поэтому HashSet будет работать с такими объектами-наблюдателями ...

person Dirk Huber    schedule 23.12.2020