Nano Hash - криптовалюты, майнинг, программирование

Почему общий список С# реализован как массив только для добавления?

Универсальный список C# System.Collections.Generic.List<T> реализован с использованием расширяемого массива в качестве резервного хранилища, аналогично реализации, основанной на большем количестве списков массивов. Ясно, что это дает огромные преимущества при выполнении случайного (индексированного) доступа, например. списки, реализованные как связанные списки.

Однако мне интересно, почему не было принято решение реализовать его как круговой массив. Такая реализация будет иметь такую ​​же производительность O(1) для индексированного произвольного доступа и добавления в конец списка. Но это даст дополнительные преимущества, такие как разрешение операций O(1) перед добавлением (т. е. вставка нового элемента в начало списка) и в среднем вдвое сокращение времени, необходимого для случайных вставок и удалений.

Обзор некоторых ответов на данный момент

Как указал @Hachet, для того, чтобы реализация циклического массива имела производительность индексирования, аналогичную производительности System.Collections.Generic.List<T>, необходимо, чтобы она всегда росла до емкости, равной степени 2 (так что дешевая операция модуля можно выполнить). Это будет означать, что невозможно точно определить его исходную емкость, указанную пользователем, как это возможно в настоящее время при построении экземпляра списка. Так что это явный компромиссный вопрос.

И, как показали некоторые быстрые и грязные тесты производительности, индексирование может быть примерно в 2-5 раз медленнее для реализации циклического массива.

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

Это дубликат с дополнительной информацией об ответе

Этот вопрос действительно связан с тем, , почему типичные реализации списка массивов не являются двойными. закончился?, который я не обнаружил перед тем, как опубликовать свой вопрос. Я полагаю, что на него не был получен удовлетворительный ответ:

Я не проводил никаких тестов, но мне просто показалось, что будут другие узкие места (например, нелокальные загрузки/хранения), которые значительно превзойдут это. Я, вероятно, приму это, если не услышу чего-то более убедительного, спасибо. — Мехрдад

Ответы на этот вопрос предоставляют дополнительную информацию о том, как можно сделать индексацию циклического списка достаточно эффективной, включая пример кода и некоторые количественные числа, которые делают решения о компромиссе более ясными. Как таковые, они предоставляют информацию, дополняющую ту, которая присутствует в другом вопросе. Поэтому я согласен с тем, что цель этого вопроса была во многом такой же, и я согласен, что его следует считать дубликатом. Однако было бы обидно, если бы новая информация, сопровождающая эту, была бы утеряна.

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


  • Хорошо, что здесь есть такие люди, как Эрик Липперт и Джон Скит. 31.08.2013
  • Не знаете, как вы планируете, чтобы и добавление, и добавление не требовали перераспределения списка (т. е. последовательности добавление-предварение-добавление)? Уже есть Queue, LinkeList (и, возможно, Stack), которые, вероятно, больше подходят для случаев использования, когда нужен доступ к обоим концам списка. 31.08.2013
  • @AlexeiLevenkov вы переходите к концу выделенного массива (т.е. вы поддерживаете индекс начала и окончания или индекс начала и счета). При добавлении вы уменьшаете голову и увеличиваете количество, при добавлении вы только увеличиваете количество. 31.08.2013

Ответы:


1

Ваш вопрос заставил меня задуматься, какова будет разница во времени выполнения между List‹> и круговой версией. Итак, я собрал быстрый скелет для того, который использует размеры, равные степени двойки (что позволяет избежать операций по модулю), то есть реализацию в лучшем случае. Я не упомянул о росте, потому что просто хотел сравнить разницу во времени свойств индексатора. Это было не так уж плохо; roundList[x] был примерно в два раза медленнее, чем list[x], и оба довольно быстры. Я также не отлаживал это, потому что у меня было ограниченное время. Этому также, вероятно, не хватает некоторого кода проверки, который делает List‹>, что сделало бы круговой список сравнительно немного медленнее, если бы он также имел его.

В общем, я бы сказал, что такое поведение просто отвлекает от основного назначения List‹>, которое на самом деле используется очень редко. Таким образом, вы снижаете производительность для очень многих применений, чтобы получить выгоду от очень небольшого числа применений. Я думаю, что они приняли правильное решение, не внося его в List‹>.

using System;

public class CircularList<T> {
    private int start, end, count, mask;
    private T[] items;
    public CircularList() : this(8) { }

    public CircularList(int capacity) {
        int size = IsPowerOf2(capacity) ? capacity : PowerOf2Ceiling(capacity);
        this.items = new T[size];
        this.start = this.end = this.count = 0;
        this.mask = size - 1;
    }

    public void Add(T item) {
        if (this.count == 0) {
            this.items[0] = item;
            this.start = this.end = 0;
        } else {
            this.items[++this.end] = item;
        }
        this.count++;
    }

    public void Prepend(T item) {
        if (this.count == 0) {
            this.items[0] = item;
            this.start = this.end = 0;
        } else {
            this.start--;
            if (this.start < 0) this.start = this.items.Length - 1;
            this.items[this.start] = item;
        }
        this.count++;
    }

    public T this[int index] {
        get {
            if ((index < 0) || (index >= this.count)) throw new ArgumentOutOfRangeException();
            return this.items[(index + this.start) & this.mask]; // (index + start) % length
        }
        set {
            if ((index < 0) || (index >= this.count)) throw new ArgumentOutOfRangeException();
            this.items[(index + this.start) & this.mask] = value; // (index + start) % length
        }
    }

    private bool IsPowerOf2(int value) {
        return (value > 0) && ((value & (value - 1)) == 0);
    }
    private int PowerOf2Ceiling(int value) {
        if (value < 0) return 1;
        switch (value) {
            case 0:
            case 1: return 1;
        }
        value--;
        value |= value >> 1;
        value |= value >> 2;
        value |= value >> 4;
        value |= value >> 8;
        return unchecked((value | (value >> 16)) + 1);
    }
}
30.08.2013
  • Спасибо. Да, очевидно, что текущая реализация списка является оптимальным выбором для сценариев записи только для добавления и сценариев с ограниченным числом операций записи и большим числом операций чтения. И это, безусловно, распространенные сценарии. Так что, возможно, именно поэтому решение было принято таким образом. 31.08.2013
  • О, и я бы сказал, что проверка, которую он выполняет, очень похожа (см. dotnetframework.org/default.aspx/4@0/4@0/ DEVDIV_TFS/Dev10/). Похоже, вы можете улучшить производительность проверки в приведенном выше примере кода, используя if ((uint)index >= (uint)this.count) 31.08.2013
  • Я только что провел некоторое тестирование производительности с реализацией циклического списка, которую я также сделал (подход аналогичен вашему коду здесь), и в моей системе с .NET 4.5 производительность циклического списка почти такая же высокая, как у списка. Использование 100 миллионов целых чисел: Add = 0,6608 против 0,735 секунды (коэффициент 1,11), для последовательного индексирования: 0,3732 против 0,3811 секунды (коэффициент 1,02). В этом случае это не похоже на большую жертву из-за гораздо более быстрых вставок и случайных вставок. 31.08.2013
  • Что ж, это, безусловно, имело значение: D Добавленная производительность по-прежнему выглядит нормально (но опять же, для последовательных вставок она в основном делает то же самое, что и список): 5,732 нс/оп против 7,434 нс/оп (коэффициент 1,3). Однако последовательная индексация имеет большое значение: 0,666 против 3,261 нс/оп (коэффициент 4,9). 31.08.2013

  • 2

    Действительно, круговой массив может быть реализован со временем доступа O(1). Однако я не верю, что целью индексатора List<T> было O(1). Нотация Big O отслеживает производительность в зависимости от размера коллекции, с которой она работает. Разработчики List<T>, помимо стремления к O(1), вероятно, были одержимы другими вещами, такими как количество инструкций и производительность в узких циклах. Он должен быть как можно ближе к производительности массива в тех же сценариях, чтобы быть в целом полезным. Доступ к элементу массива — очень простая операция

    • Добавить индекс, умноженный на размер, в начало массива
    • почтение

    Индексирование в циклическом массиве, хотя и по-прежнему O (1), включает по крайней мере одну операцию ветвления. Он должен проверить, должен ли индекс массива обернуться или нет на основе желаемого индекса. Это означает, что плотные массивы над циклами с известными границами будут иметь внутри себя логику ветвления. Это было бы значительным шагом вниз в путях кода с быстрыми узкими циклами над необработанным массивом.

    ИЗМЕНИТЬ

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

    Кроме того, еще одна проблема будет заключаться в том, является ли операция prepend приоритетной операцией. Если prepend не считался приоритетом, то зачем принимать какие-либо потери производительности в сценарии, который был (индексирование, безусловно, было приоритетом)? Я предполагаю, что индексация, перечисление и добавление в конец массива были сценариями с наивысшим приоритетом. Такие операции, как prepend, вероятно, считались редкими.

    30.08.2013
  • Индексация элементов в циклическом массиве может быть достигнута без ветвления путем выполнения операции модуля (предпочтительно реализованной без выполнения деления). В некоторых случаях вам потребуется 2 операции memmove, но объем перемещения будет значительно меньше. 31.08.2013
  • @Alex - Доступ к списку‹› очень быстрый, почти такой же быстрый, как обычный доступ к массиву. Добавление модуля к этому будет значительным ударом по производительности, если вы не можете гарантировать размеры списка, которые являются степенью двойки. 31.08.2013
  • @hatchet согласился, когда вы растете, вы всегда должны расти до степени 2. 31.08.2013
  • Но в настоящее время для List‹› такого ограничения нет. Вы можете сделать его начальный размер именно таким, каким хотите. 31.08.2013
  • @hatchet правда, это был бы компромисс, который может привести к решению реализовать его таким, какой он есть. С другой стороны, как только ему потребуется выйти за пределы того, что вы указали в качестве начальной емкости, он будет использовать собственное определение идеальной емкости. 31.08.2013
  • Новые материалы

    Кластеризация: более глубокий взгляд
    Кластеризация — это метод обучения без учителя, в котором мы пытаемся найти группы в наборе данных на основе некоторых известных или неизвестных свойств, которые могут существовать. Независимо от..

    Как написать эффективное резюме
    Предложения по дизайну и макету, чтобы представить себя профессионально Вам не позвонили на собеседование после того, как вы несколько раз подали заявку на работу своей мечты? У вас может..

    Частный метод Python: улучшение инкапсуляции и безопасности
    Введение Python — универсальный и мощный язык программирования, известный своей простотой и удобством использования. Одной из ключевых особенностей, отличающих Python от других языков, является..

    Как я автоматизирую тестирование с помощью Jest
    Шутка для победы, когда дело касается автоматизации тестирования Одной очень важной частью разработки программного обеспечения является автоматизация тестирования, поскольку она создает..

    Работа с векторными символическими архитектурами, часть 4 (искусственный интеллект)
    Hyperseed: неконтролируемое обучение с векторными символическими архитектурами (arXiv) Автор: Евгений Осипов , Сачин Кахавала , Диланта Хапутантри , Тимал Кемпития , Дасвин Де Сильва ,..

    Понимание расстояния Вассерштейна: мощная метрика в машинном обучении
    В обширной области машинного обучения часто возникает необходимость сравнивать и измерять различия между распределениями вероятностей. Традиционные метрики расстояния, такие как евклидово..

    Обеспечение масштабируемости LLM: облачный анализ с помощью AWS Fargate и Copilot
    В динамичной области искусственного интеллекта все большее распространение получают модели больших языков (LLM). Они жизненно важны для различных приложений, таких как интеллектуальные..