Краткое введение
System.Collections.Generric.List <T>— это универсальный класс коллекций в .NET, который может хранить любые типы данных благодаря своему удобству и богатому API, широко используемому в нашей повседневной жизни и считающийся самым используемым классом коллекции.
В написании кода нам часто приходится проходить через <T>коллекцию списков, чтобы получить элементы для бизнес-обработки. Обычно внутри множества мало элементов, и перемещение очень быстрое. Но для обработки больших данных, статистики, вычислений в реальном времени и т.д<T>., как быстро пройтись по списку из десятков тысяч или сотен тысяч данных? Вот чем я хочу поделиться с вами сегодня.
Режим перемещения
Давайте рассмотрим производительность различных методов обхождения и построим следующий ориентир производительности, используя обход коллекции разного порядка, чтобы увидеть производительность разных методов. Фрагмент кода выглядит так:
Используйте утверждение foreach
Foreach — самый распространённый способ прохождения коллекций, это синтаксическая сахарная реализация итераторного паттерна, а также используется как ориентир для этого времени.
Поскольку оператор foreach является синтаксическим сахаром, компилятор в конечном итоге вызывает GetEnumerator() и MoveNext() с циклом while для реализации функциональности. Скомпилированный код выглядит так:
Реализация метода MoveNext() гарантирует, что в итерации не будет других потоков, изменяющих коллекцию, и если произойдёт изменение, появится исключение InvalidOperationException, будет проведена проверка переполнения для проверки легитимности текущего индекса, а также необходимо назначить соответствующий элемент перечислителю. Текущий атрибут,Так что на самом деле её производительность не самая лучшая, фрагмент кода выглядит так:
Давайте посмотрим, как он работает при разных размерах наборов, и результаты выглядят так:
Видно, что при разных размерах требуется линейное соотношение роста длительного процесса, даже если он проходит 100 Вт данных без логики обработки, это занимает не менее 1 секунды.
Используйте метод ForEach для списка
Ещё один распространённый способ — использовать List<T>. Метод ForEach(), который позволяет передавать <T>делегата Действия, который вызывает делегата Действия по мере прохождения элемента<T>.
Это <T>внутренний метод реализации List, позволяющий напрямую получать доступ к частным массивам и избегать проверок переполнения. Теоретически это должно быть быстро; Но в нашем сценарии есть только один пустой метод, который может плохо работать при полностью встроенном вызове метода foreach. Ниже приведён исходный код метода ForEach, который показывает, что у него нет проверки переполнения, но при этом сохраняется проверка параллельного номера версии.
Кроме того, поскольку необходимо передавать делегат методу ForEach, в коде вызова он проверяет, пуст ли объект делегата в классе генерации закрытия каждый раз, а если нет — новый Action<T>(), как показано ниже:
Давайте посмотрим, как оно сравнивается с ключевым словом foreach с точки зрения производительности. Следующее изображение показывает результаты бенчмарка:
Судя по результатам теста, это на 40% медленнее, чем при прямом использовании ключевого слова foreach, кажется, что если это не обязательно, то лучше использовать напрямую foreach, так что есть ли более быстрый способ?
для обхода петли
Возвращаясь к нашему старому способу — использовать ключевое слово «for» для перемещения коллекции. Это должен быть самый эффективный метод обхода на данный момент, так как он не требует какого-то дублирующего кода, как предыдущие (хотя индексатор также проверяется для предотвращения переполнений), и, очевидно, он не проверяет номер версии, поэтому в многопоточной среде коллекция меняется, и при использовании для не будет исключений. Тестовый код выглядит так:
Посмотрим, что получится.
Похоже, именно так мы и ожидаем.Прямое использование петли for на 60% быстрее, чем foreach, набор, который раньше проходил за 1 секунду, теперь занимает всего 400 миллисекунд. Так есть ли более быстрый способ?
Использовать коллекцииМаршал
После .NET5 сообщество dotnet реализовало класс CollectionsMarshal для повышения эффективности операций по сбору. Этот класс реализует, как получить доступ к нативным массивам типов коллекций (если вы видели мой [. .NET Performance Optimization - You Should Set the Initial Size for Collection Types, вы знаете, что основная реализация многих структур данных — это массивы. То есть он может пропускать все виды обнаружений и напрямую обращаться к исходному массиву, который должен быть самым быстрым. Код выглядит так:
Видно, что код, сгенерированный компилятором, очень эффективен.
Прямой доступ к базовому массиву очень опасен, нужно знать, что делаешь с каждой строкой кода, и иметь достаточно тестирования. Результаты эталона следующие:
ОйИспользование CollectionsMarshal на 79% быстрее, чем использование foreach, но именно это должно быть причиной оптимизации JIT, нет большой разницы между использованием foreach и для цикла ключевых слов Span.
сводка
Сегодня я говорил с вами о том, как быстро проходить коллекцию List, и в большинстве случаев рекомендуется использовать ключевое слово foreach, которое включает и проверку переполнения, и многопоточное управление номером версий, что может облегчить написание правильного кода.
Если вам нужны высокая производительность и большие объемы данных, рекомендуется использовать для и CollectionsMarshal.AsSpan напрямую для обхода коллекции.
Ссылка на исходный код этой статьи:
Вход по гиперссылке виден.
Оригинальная ссылка:Вход по гиперссылке виден.
|