Производительность HashSet Добавить против Содержит для существующих элементов - PullRequest
13 голосов
/ 10 марта 2009

По некоторым причинам, операция Add на HashSet медленнее, чем операция Contains, когда элемент уже существует в HashSet.

Вот доказательство:

    Stopwatch watch = new Stopwatch();
    int size = 10000;
    int iterations = 10000;


    var s = new HashSet<int>();
    for (int i = 0; i < size; i++) {
        s.Add(i);
    }

    Console.WriteLine(watch.Time(() =>
    {
        for (int i = 0; i < size; i++) {
            s.Add(i);
        }
    }, iterations));

    s = new HashSet<int>();
    for (int i = 0; i < size; i++) {
        s.Add(i);
    }

    // outputs: 47,074,764

    Console.WriteLine(watch.Time(() =>
    {
        for (int i = 0; i < size; i++) {
            if (!s.Contains(i))
                s.Add(i);
        }
    }, iterations));

    // outputs: 41,125,219

Почему Contains быстрее, чем Add для уже существующих элементов?

Примечание: я использую это расширение Stopwatch из другого SO вопроса.

    public static long Time(this Stopwatch sw, Action action, int iterations) {
        sw.Reset();
        sw.Start();
        for (int i = 0; i < iterations; i++) {
            action();
        }
        sw.Stop();

        return sw.ElapsedTicks;
    }

ОБНОВЛЕНИЕ : внутреннее тестирование показало, что большая разница в производительности происходит только в 64-разрядной версии .NET Framework. С 32-битной версией фреймворка кажется, что добавление выполняется с идентичной скоростью (на самом деле кажется, что версия с контейнерами работает на процент медленнее в некоторых тестовых прогонах) В версиях фреймворка X64 версия с контентом кажется бегать примерно на 15% быстрее.

Ответы [ 3 ]

9 голосов
/ 10 марта 2009

AddIfNotPresent делает дополнительное деление, которое Contains не выполняет. Взгляните на IL для Содержит:

IL_000a:  call       instance int32 class System.Collections.Generic.HashSet`1<!T>::InternalGetHashCode(!0)
  IL_000f:  stloc.0
  IL_0010:  ldarg.0
  IL_0011:  ldfld      int32[] class System.Collections.Generic.HashSet`1<!T>::m_buckets
  IL_0016:  ldloc.0
  IL_0017:  ldarg.0
  IL_0018:  ldfld      int32[] class System.Collections.Generic.HashSet`1<!T>::m_buckets
  IL_001d:  ldlen
  IL_001e:  conv.i4
  IL_001f:  rem
  IL_0020:  ldelem.i4
  IL_0021:  ldc.i4.1
  IL_0022:  sub
  IL_0023:  stloc.1

Это вычисление местоположения сегмента для хеш-кода. Результат сохраняется в локальной памяти 1.

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

IL_0011:  call       instance int32 class System.Collections.Generic.HashSet`1<!T>::InternalGetHashCode(!0)
  IL_0016:  stloc.0
  IL_0017:  ldloc.0
  IL_0018:  ldarg.0
  IL_0019:  ldfld      int32[] class System.Collections.Generic.HashSet`1<!T>::m_buckets
  IL_001e:  ldlen
  IL_001f:  conv.i4
  IL_0020:  rem
  IL_0021:  stloc.1
  IL_0022:  ldarg.0
  IL_0023:  ldfld      int32[] class System.Collections.Generic.HashSet`1<!T>::m_buckets
  IL_0028:  ldloc.0
  IL_0029:  ldarg.0
  IL_002a:  ldfld      int32[] class System.Collections.Generic.HashSet`1<!T>::m_buckets
  IL_002f:  ldlen
  IL_0030:  conv.i4
  IL_0031:  rem
  IL_0032:  ldelem.i4
  IL_0033:  ldc.i4.1
  IL_0034:  sub
  IL_0035:  stloc.2

В любом случае, я думаю, что дополнительный разрыв - это то, что заставляет Add занимать больше времени, чем Contains. На первый взгляд кажется, что это дополнительное разделение может быть учтено, но я не могу сказать наверняка, не потратив немного больше времени на расшифровку IL.

1 голос
/ 10 марта 2009

Я предполагаю, что вы запустили свой тест из Visual Studio, что привело к подавлению встраивания AddIfNotPresent в Add, поэтому вы видите результат дополнительного уровня косвенности в вызовах методов.

Если я скомпилирую и запущу из командной строки, чтобы удалить любую хитрость VS ...

> csc /o+ /t:exe Program.cs
> Program.exe

... тогда нет разницы в производительности.

Пример выходных данных (представитель большего числа тестов):

35036174
35153818

35225763
34862330

35047377
35033323
1 голос
/ 10 марта 2009

Интересно, что на моей машине (Dell Latitude D630, двухъядерный 2,2 ГГц) я получаю практически одинаковые результаты для обоих тестов, если только перед тестами не запускаю секундомер против действия null. Например:

Я запускаю тесты с точным кодом, который вы дали в вопросе:

Without Contains(): 8205794
With Contains():    8207596

Если я изменю код таким образом:

После того, как:

Stopwatch watch = new Stopwatch();
int size = 10000;
int iterations = 10000;

Добавить:

watch.Time(null, 0);

Мои результаты становятся:

Without Contains(): 8019129
With Contains():    8275771

Мне кажется, что что-то странное происходит внутри Stopwatch, которое вызывает эти колебания.

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