Список.Субоптимальная реализация AddRange

профилирование моего приложения c# показало, что значительное время тратится на List<T>.AddRange. Использование Reflector для просмотра кода в этом методе показало, что он вызывает List<T>.InsertRange который реализован как таковой:

public void InsertRange(int index, IEnumerable<T> collection)
{
    if (collection == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection);
    }
    if (index > this._size)
    {
        ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_Index);
    }
    ICollection<T> is2 = collection as ICollection<T>;
    if (is2 != null)
    {
        int count = is2.Count;
        if (count > 0)
        {
            this.EnsureCapacity(this._size + count);
            if (index < this._size)
            {
                Array.Copy(this._items, index, this._items, index + count, this._size - index);
            }
            if (this == is2)
            {
                Array.Copy(this._items, 0, this._items, index, index);
                Array.Copy(this._items, (int) (index + count), this._items, (int) (index * 2), (int) (this._size - index));
            }
            else
            {
                T[] array = new T[count];          // (*)
                is2.CopyTo(array, 0);              // (*)
                array.CopyTo(this._items, index);  // (*)
            }
            this._size += count;
        }
    }
    else
    {
        using (IEnumerator<T> enumerator = collection.GetEnumerator())
        {
            while (enumerator.MoveNext())
            {
                this.Insert(index++, enumerator.Current);
            }
        }
    }
    this._version++;
}

private T[] _items;

можно утверждать, что простота интерфейса (только с одной перегрузкой InsertRange) оправдывает накладные расходы на производительность проверки типа времени выполнения и литья. Но что может быть причиной за 3 строками, которые я указал с (*) ? Я думаю, что может переписать на более быструю альтернативу:

is2.CopyTo(this._items, index);

вы видите какие-либо причины не использовать эту более простую и, по-видимому, более быструю альтернативу?

Edit:

Спасибо за ответы. Таким образом, по общему мнению, это защитная мера против входной коллекции, реализующей CopyTo дефектным/злонамеренным образом. Мне кажется, что это перебор, чтобы постоянно платить цену 1) проверка типа времени выполнения 2) динамическое распределение временный массив 3) удвоить операцию копирования, когда все это можно было бы сохранить, определив 2 или несколько перегрузок InsertRange, один получает IEnumerable как сейчас, второй становится List<T>, третьим становится T[]. Более поздние два могли быть реализованы, чтобы работать в два раза быстрее, чем в текущем случае.

Edit 2:

я реализовал класс FastList, идентичный List, За исключением того, что он также обеспечивает перегрузку AddRange, которая занимает T[] аргумент. Эта перегрузка не требует проверки динамического типа и двойного копирования элементов. Я сделал профиль этого FastList.AddRange против списка.AddRange путем добавления 4-байтовых массивов 1000 раз в список, который изначально был emtpy. Моя реализация превосходит скорость стандартного списка.AddRange с коэффициентом 9 (девять!). Список.AddRange занимает около 5% времени выполнения в одном из важных сценариев использования нашего приложения, замена List классом, обеспечивающим более быстрый AddRange, может улучшить приложение на 4%.

3 ответов


они препятствуют осуществлению ICollection<T> доступ к индексам списка назначения за пределами вставки. Реализация выше приводит к IndexOutOfBoundsException если неисправная (или" манипулятивная") реализация CopyTo называется.

имейте в виду, что T[].CopyTo - это в буквальном смысле внутренне реализован как memcpy, поэтому накладные расходы на производительность добавления этой строки-минута. Когда вы имеете такую низкую цену добавлять безопасность к большущему количеству звонки, вы можете сделать это.

Edit: часть, которую я нахожу странной, заключается в том, что вызов ICollection<T>.CopyTo (копирование во временный массив) не происходит сразу после вызова EnsureCapacity. Если он был перемещен в это место, то после любого синхронно исключением список останется без изменений. Как есть, это условие выполняется только в том случае, если вставка происходит в конце списка. Рассуждение здесь такое:

  • все необходимое распределение происходит перед изменением элементов списка.
  • звонки Array.Copy не может потерпеть неудачу, потому что
    • память уже выделена
    • границы уже проверены
    • типы элементов исходного и целевого массивов соответствуют
    • нет "конструктора копирования", используемого как в C++ - это просто memcpy
  • единственными элементами, которые могут вызвать исключение, являются внешний вызов ICollection.CopyTo и ассигнования, необходимые для изменения размера списка и выделения временного массива. Если все три из них происходят перед перемещением элементов для вставки, транзакция для изменения списка не может вызвать Синхронное исключение.
  • Конечная нота: этот адрес строго исключительное поведение-выше обоснование делает не добавить потокобезопасности.

Edit 2 (ответ на редактирование OP): вы профилировали это? Вы делаете некоторые смелые утверждения, что Microsoft должна была выбрать более сложный API, поэтому вы должны убедиться, что вы правы в утверждениях, что текущий метод медленный. У меня никогда не было проблем с исполнением InsertRange, и я уверен, что любые проблемы с производительностью, с которыми кто-то сталкивается, будут лучше решены с помощью редизайна алгоритма, чем путем переопределения динамического списка. Просто чтобы ты не воспринимала меня как сурового в негативном смысле, продолжай в том же духе. ум:

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

Это хороший вопрос, я пытаюсь придумать причину. В справочном источнике нет никаких намеков. Одна из возможностей заключается в том, что они пытаются избежать проблемы, когда класс, реализующий ICollection.Объекты метода CopyTo () против копирования в начальный индекс, отличный от 0. Или в качестве меры безопасности, препятствующей коллекции возиться с элементами массива, к которым она не должна иметь доступа.

еще один заключается в том, что это счетчик, когда используется коллекция в потоке-небезопасным способом. Если элемент был добавлен в коллекцию другим потоком, это будет метод CopyTo() класса коллекции, который завершается неудачей, а не код Microsoft. Правильный человек получит вызов службы.

Это не большие объяснения.


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

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

по сути, вы бы цементировали реализацию класса List, даже если объектно-ориентированное программирование говорит нам, что внутренняя реализация datastructure должна быть чем-то, что можно изменить, не нарушая другой код.