Перечислять список быстрее, чем IList, ICollection и IEnumerable - PullRequest
4 голосов
/ 16 октября 2019

Недавно я исследовал некоторые соглашения в написании функций, которые возвращают коллекцию. Мне было интересно, должна ли функция, которая на самом деле использует List<int>, возвращать List<int> или, скорее, IList<int>, ICollection<int> или IEnumerable<int>. Я создал несколько тестов на производительность и был весьма удивлен результатами.

static List<int> list = MakeList();
static IList<int> iList = MakeList();
static ICollection<int> iCollection = MakeList();
static IEnumerable<int> iEnumerable = MakeList();

public static TimeSpan Measure(Action f)
{
    var stopWatch = new Stopwatch();
    stopWatch.Start();
    f();
    stopWatch.Stop();
    return stopWatch.Elapsed;
}

public static List<int> MakeList()
{
    var list = new List<int>();
    for (int i = 0; i < 100; ++i)
    {
        list.Add(i);
    }
    return list;
}

public static void Main()
{
    var time1 = Measure(() => { // Measure time of enumerating List<int>
        for (int i = 1000000; i > 0; i-- ) {
            foreach (var item in list)
            {
                var x = item;
            }
        }
    });
    Console.WriteLine($"List<int> time: {time1}");

    var time2 = Measure(() => { // IList<int>
        for (int i = 1000000; i > 0; i-- ) {
            foreach (var item in iList)
            {
                var x = item;
            }
        }
    });
    Console.WriteLine($"IList<int> time: {time2}");

    var time3 = Measure(() => { // ICollection<int>
        for (int i = 1000000; i > 0; i-- ) {
            foreach (var item in iCollection)
            {
                var x = item;
            }
        }
    });
    Console.WriteLine($"ICollection<int> time: {time3}");

    var time4 = Measure(() => { // IEnumerable<int>
        for (int i = 1000000; i > 0; i-- ) {
            foreach (var item in iEnumerable)
            {
                var x = item;
            }
        }
    });
    Console.WriteLine($"IEnumerable<int> time: {time4}");
}

Вывод:

List<int> time: 00:00:00.7976577
IList<int> time: 00:00:01.5599382
ICollection<int> time: 00:00:01.7323919
IEnumerable<int> time: 00:00:01.6075277

Я пробовал другой порядокиз мер или заставить MakeList() вернуть один из вышеуказанных интерфейсов, но все это только подтверждает, что возврат List<int> и обработка его как List<int> примерно в два раза быстрее , чем с интерфейсами.

Однако различные источники, включая этот ответ утверждают, что вы никогда не должны возвращать List<> и всегда использовать интерфейс.

Так что мой вопросэто:

  • Почему обработка List<int> примерно в два раза быстрее, чем интерфейсы?
  • Что нам следует возвращать из функции и как управлять кодом, если мы заботимся о производительности?

1 Ответ

8 голосов
/ 16 октября 2019

Почему обработка List<int> примерно в два раза быстрее, чем интерфейсы?

Отличный вопрос. При попытке foreach что-то C # first проверяет, есть ли у типа коллекции уже метод с именем GetEnumerator, который возвращает тип, который имеет MoveNext и Current. Если это так, вызывает их напрямую . Если нет, то он возвращается к использованию IEnumerable<T> или IEnumerable и IEnumerator<T> или IEnumerator для получения перечислителя, чтобы он мог вызывать MoveNext и Current.

Этот выбор дизайнабыл сделан по двум причинам. Во-первых, в мире C # 1.0 до появления дженериков это означало, что вы можете вызвать Current, который вернул int;IEnumerator.Current - это, конечно, object, поэтому можно указать int, что является как скоростью, так и потерей памяти. Во-вторых, это означало, что авторы коллекций могли проводить эксперименты, чтобы выяснить, какая реализация MoveNext и Current показала наилучшую производительность.

Разработчики List<T> сделали именно это;если вы посмотрите GetEnumerator на List<T>, вы обнаружите что-то интересное: он возвращает изменяемый тип значения. Да, изменяемые типы значений считаются легко злоупотребляемой плохой практикой. Но поскольку 99,999% случаев использования этой перегрузки GetEnumerator вызвано от вашего имени foreach, в подавляющем большинстве случаев вы даже не замечаете, что для вас существует изменчивая ценность злоупотребления, и, следовательно, не злоупотребляйте ею.

(ПРИМЕЧАНИЕ. Вывод предыдущего абзаца не должен заключаться в том, чтобы «использовать изменяемые типы значений, потому что они быстрые». Выводом следует: понять схемы использования ваших пользователей и затем разработать безопасный, эффективныйинструмент, который отвечает их потребностям . Обычно изменяемый тип значений не является подходящим инструментом.)

В любом случае, если коротко, мы избегаем всевозможных виртуальных вызовов, проверок типов интерфейса и т. д. прямая привязка к методам с изменяемыми типами значений при итерации чего-либо, известного во время компиляции, как List<T>.

Что нам следует возвращать из функции и как управлять кодом, если мы заботимся о производительности?

Если вы заботитесь о быстродействии, вам следует сконцентрироваться на самой медленной вещи вЭлектронная программа . Самая медленная вещь в вашей программе, вызывающая MoveNext в коллекции? Если так, поздравляю, у вас очень быстрая программа;MoveNext - следующая вещь для оптимизации. Но в этом случае на самом деле вы должны спросить: «Как мне избежать или задержать этот цикл полностью?»если вы находитесь в этой лодке.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...