Использование перечисления в качестве ключа для словаря
Неявная генерация объектов в процессе работы .NET приложения является неоспоримым злом, а в процессе работы .NET приложения, которые по совместительству еще и являются играми реального времени, такое поведение и вообще смерти подобно. Сам по себе вызов сборки мусора к краху приложения конечно ни в коем случае не приведет, но желательно, чтобы их в процессе жизни нашего приложения происходило, как можно меньше. В данной статье я хочу рассмотреть довольно частый прием, который применяется программистами .NET – использование перечисления (enum) в качестве ключа для объекта словаря (Dictionary). Во-первых, применение данного метода удобно, объекты одного типа собраны в один контейнер и доступ к ним осуществляется фактически по имени (значение перечисления), IntelliSince тоже вносит свою немалую лепту в написании кода при таком подходе. Во-вторых, данный подход помогает избежать ошибок, в отличие от применения скажем string в качестве ключа.
Но как всегда найдется ложка с дегтем, нависающая над нашей бочкой с прополисом. Использование перечисления, как ключа для словаря ведет к генерации мусора, в бизнес приложении можно конечно забить на сей факт (что конечно тоже нежелательно), но в играх XNA, где фреймрейт показатель довольно значительный с этим мериться никак нельзя.
Если вкратце, то при использовании в качестве ключа ссылочного типа мы ограничиваемся только вызовом метода GetHashCode и по вычисленному хэшу получаем значение из HashTable, то при использовании в качестве ключа типа значение, для получения хэша нам приходится неявно приводить значение в ссылочный тип, в нашем случае – это object и уже затем вычислять хэш. А этот объект отбрасывается в виде мусора за ненадобностью, при накоплении достаточного количества таких мусорных объектов будет инициирована сборка, которая сама по себе является не легкой операцией. В своем докладе Иван обошел это применением кэширования, он просто закэшировал ссылку на объект текстуры, которую до этого получал из словаря по ключу для каждого объекта звезды.
Теперь давайте более подробно разберемся в сложившейся ситуации, а точнее покопаемся внутри объекта Dictionary.
К моему удивлению reflectior не показал мне объекта Dictionary в пространстве имен System.Collections.Generic, где он по теории обитает, поэтому я взял исходники .NET Framework.
Итак, что у нас происходит, когда мы объявляем и инициализируем словарь?
Во-первых, передается количество элементов словаря, под которое будет сразу же выделена память, но более интересен для нас сейчас – это объект comparer. Если мы нечего не скармливаем в конструктор, то будет создан экземпляр стандартной реализации EqualityComparer.Default; Не буду вдаваться в подробности его реализации, но если пройтись по коду, то станет видно, что в случае с перечислениями он инициализируется реализацией internal class ObjectEqualityComparer: EqualityComparer. Собственно данный класс и подписывает нам приговор при использовании перечисления, а точнее его универсальность, в связке со спецификой реализации перечислений дают просто убийственный эффект в производительности.
Метод GetValue() возвращает нам object, по которому у нас и высчитывается хэш, по окончанию расчетов он будут отброшен, как мусорный объект.
Но к счастью разработчики .NET дали нам возможность передавать в словарь свой класс для сравнения ключей, а природа наделила нас мозгом. Мы можем написать свою реализацию для сравнения типов значений, реализовав интерфейс IEqualityComparer и передав его в конструктор словаря.
За основу для кода я взял исходники Ивана Андреева из его доклада, который упоминался выше.
Простейшая реализация класса для сравнения значений ключей для перечисления текстур:
Если кому-нибуть непонятно, что мы этим добились, ведь у нас все так-же продолжает вызываться GetHashCode, поясняю, реализация методов GetHashCode и Equals у типов значений (наследованные от ValueType), в частности наше перечисление TextureEnum отличается от реализации у объектов ссылочного типа, которые напрямую наследуют от object, в них учитывается именно значение, которое находится в объекте.
Далее мы просто скармливаем наш класс для сравнения в конструктор словаря.