2014-01-13 11 views
25

Этого я не заметил до сегодняшнего дня. По-видимому, .NET-реализация сильно используемых классов кортежей (Tuple<T>, Tuple<T1, T2> и т. Д.) Вызывает штрафы за бокс для типов значений, когда выполняются операции, основанные на равенстве.Производительность NET Tuple и Equals

Вот как класс рода реализуется в рамках (источник через ILSpy):

public class Tuple<T1, T2> : IStructuralEquatable 
{ 
    public T1 Item1 { get; private set; } 
    public T2 Item2 { get; private set; } 

    public Tuple(T1 item1, T2 item2) 
    { 
     this.Item1 = item1; 
     this.Item2 = item2; 
    } 

    public override bool Equals(object obj) 
    { 
     return this.Equals(obj, EqualityComparer<object>.Default); 
    } 

    public override int GetHashCode() 
    { 
     return this.GetHashCode(EqualityComparer<object>.Default); 
    } 

    public bool Equals(object obj, IEqualityComparer comparer) 
    { 
     if (obj == null) 
     { 
      return false; 
     } 

     var tuple = obj as Tuple<T1, T2>; 
     return tuple != null 
      && comparer.Equals(this.Item1, tuple.Item1) 
      && comparer.Equals(this.Item2, tuple.Item2); 
    } 

    public int GetHashCode(IEqualityComparer comparer) 
    { 
     int h1 = comparer.GetHashCode(this.Item1); 
     int h2 = comparer.GetHashCode(this.Item2); 

     return (h1 << 5) + h1^h2; 
    } 
} 

Проблему я вижу это вызывает два этапа бокс-распаковка, скажет Equals вызовов, один, на comparer.Equals, в который помещается элемент, два, EqualityComparer<object> вызывает не общий кодEquals, который, в свою очередь, внутренне должен будет удалить элемент в исходный тип.

Вместо почему бы им не сделать что-то вроде:

public override bool Equals(object obj) 
{ 
    var tuple = obj as Tuple<T1, T2>; 
    return tuple != null 
     && EqualityComparer<T1>.Default.Equals(this.Item1, tuple.Item1) 
     && EqualityComparer<T2>.Default.Equals(this.Item2, tuple.Item2); 
} 

public override int GetHashCode() 
{ 
    int h1 = EqualityComparer<T1>.Default.GetHashCode(this.Item1); 
    int h2 = EqualityComparer<T2>.Default.GetHashCode(this.Item2); 

    return (h1 << 5) + h1^h2; 
} 

public bool Equals(object obj, IEqualityComparer comparer) 
{ 
    var tuple = obj as Tuple<T1, T2>; 
    return tuple != null 
     && comparer.Equals(this.Item1, tuple.Item1) 
     && comparer.Equals(this.Item2, tuple.Item2); 
} 

public int GetHashCode(IEqualityComparer comparer) 
{ 
    int h1 = comparer.GetHashCode(this.Item1); 
    int h2 = comparer.GetHashCode(this.Item2); 

    return (h1 << 5) + h1^h2; 
} 

Я был удивлен видеть равенство реализован таким образом, в классе .NET кортежей. Я использовал тип кортежа в качестве ключа в одном из словарей.

Есть ли причина, по которой это должно быть реализовано, как показано в первом коде? Его немного обескураживает использование этого класса в этом случае.

Я не думаю, что рефакторинг кода и не дублирование данных должны были быть основными проблемами. Та же не-общая/боксерская реализация отстала и от IStructuralComparable, но так как IStructuralComparable.CompareTo используется менее часто, это не проблема.


Я протестированные выше двух подходов с третьего подхода, который еще менее обременительным, как это (только первой необходимости):

public override bool Equals(object obj) 
{ 
    return this.Equals(obj, EqualityComparer<T1>.Default, EqualityComparer<T2>.Default); 
} 

public bool Equals(object obj, IEqualityComparer comparer) 
{ 
    return this.Equals(obj, comparer, comparer); 
} 

private bool Equals(object obj, IEqualityComparer comparer1, IEqualityComparer comparer2) 
{ 
    var tuple = obj as Tuple<T1, T2>; 
    return tuple != null 
     && comparer1.Equals(this.Item1, tuple.Item1) 
     && comparer2.Equals(this.Item2, tuple.Item2); 
} 

пару Tuple<DateTime, DateTime> полей а Equals вызовов 1000000. Это результат:

первый подход (оригинальная реализация .NET) - 310 мс

второй подход - 60 мс

третий подход - 130 мс

Реализация по умолчанию примерно в 4-5 раз медленнее, чем оптимальное решение.

+1

Я не вижу причин, почему использовался 'IEqualityComparer ', но я не говорю, что их нет. Но вы все равно можете сделать это немного лучше: http://pastebin.com/tNA2FYjq – MarcinJuraszek

+1

@MarcinJuraszek Как это лучше? 'Tuple <,>' реализует 'IStructuralEquatable', который имеет эти определения' bool Equals (object other, IEqualityComparer comparer); int GetHashCode (сравнитель IEqualityComparer); ' – nawfal

+1

" ... много используемых классов Tuple ... ". Там твоя проблема! – Gusdor

ответ

10

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

Но почему существующая реализация делает такое явное использование EqualityComparer<object>.Default? Это может быть случай человека, который написал эту мысленную оптимизацию для «неправильного» или, по крайней мере, другого, чем ваш сценарий скорости во внутреннем цикле. В зависимости от их ориентира это может показаться «правильным».

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

Вот одна точка знания для поддержки теории (найдена с использованием подтверждения смещения :) - Органы метода реализации EqualityComparer нельзя разделить, если T является структурой. Извлеченный из http://blogs.microsoft.co.il/sasha/2012/09/18/runtime-representation-of-genericspart-2/

Когда CLR необходимо создать экземпляр закрытого универсального типа, , такие как список, он создает таблицу методов и EEClass на основе открытого типа. Как всегда, таблица методов содержит указатели на методы, которые скомпилированы на лету компилятором JIT. Однако здесь существует важная оптимизация : скомпилированные тела методов в закрытом родовом типах, которые имеют параметры ссылочного типа, могут совместно использоваться. [...] Эта же идея не подходит для типов значений. Например, когда T является длинным, элементы инструкции присваивания [size] = item требуют другую команду , потому что вместо 4 нужно скопировать 8 байтов. Даже более крупные типы значений могут даже потребовать более одной инструкции; и так далее.

+0

Хороший ответ на другие возможные соображения. Я соглашусь это сейчас. – nawfal

+0

@nawfal Спасибо! Мне было интересно исследовать этот! –