Почему при заказе с помощью Linq-to-Objects элементы сравниваются сами с собой?

Рассмотрим следующий простой код с LINQ OrderBy и ThenBy:

static void Main()
{
  var arr1 = new[] { "Alpha", "Bravo", "Charlie", };

  var coStr = Comparer<string>.Create((x, y) =>
  {
    Console.WriteLine($"Strings: {x} versus {y}");
    return string.CompareOrdinal(x, y);
  });

  arr1.OrderBy(x => x, coStr).ToList();

  Console.WriteLine("--");

  var arr2 = new[]
  {
    new { P = "Alpha", Q = 7, },
    new { P = "Bravo", Q = 9, },
    new { P = "Charlie", Q = 13, },
  };

  var coInt = Comparer<int>.Create((x, y) =>
  {
    Console.WriteLine($"Ints: {x} versus {y}");
    return x.CompareTo(y);
  });

  arr2.OrderBy(x => x.P, coStr).ThenBy(x => x.Q, coInt).ToList();
}

Это просто использует некоторые компараторы, которые выводят на консоль то, что они сравнивают.

На моем оборудовании и версии Framework (.NET 4.6.2) это вывод:

Strings: Bravo versus Alpha
Strings: Bravo versus Bravo
Strings: Bravo versus Charlie
Strings: Bravo versus Bravo
--
Strings: Bravo versus Alpha
Strings: Bravo versus Bravo
Ints: 9 versus 9
Strings: Bravo versus Charlie
Strings: Bravo versus Bravo
Ints: 9 versus 9

Мой вопрос: Зачем им сравнивать элемент из запроса с самим собой?

В первом случае перед разделителем -- делают четыре сравнения. Два из них сравнивают запись с самой собой («Strings: Bravo vs Bravo»). Почему?

Во втором случае никогда не должно возникать необходимости прибегать к сравнению Q свойств (целых чисел); поскольку в значениях P нет дубликатов (относительно порядкового сравнения), поэтому никогда не требуется разрыва связей из ThenBy. Тем не менее мы дважды видим «Ints: 9 vs 9». Зачем использовать компаратор ThenBy с одинаковыми аргументами?

Примечание. Любой компаратор должен возвращать 0 при сравнении чего-либо с самим собой. Итак, если алгоритм просто не хочет проверить, правильно ли мы реализовали компаратор (что он все равно никогда не сможет сделать полностью), что происходит?

Имейте в виду: в элементах, выданных запросами в моих примерах, нет дубликатов.

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


person Jeppe Stig Nielsen    schedule 29.08.2017    source источник
comment
Они используют простую итерацию без проверки перед вызовом сравнения? неудача ;-)   -  person Jeroen van Langen    schedule 29.08.2017
comment
Это может быть вопросом излишне надуманной оценки. Было бы относительно легко сравнивать индексы в начале, но если тот же код продолжает проверять это, когда выходной массив уже сместил некоторые его элементы, это означает, что логика OrderBy должна начаться отслеживание того, какой элемент он поместил, чтобы знать, что это то же самое. Это может создать дополнительные накладные расходы для сравнительно незначительного повышения производительности, поскольку OrderBy обычно используется для больших наборов данных (поэтому минимизируется частота проверки одинаковых элементов).   -  person Flater    schedule 29.08.2017
comment
@Flater Возможно, он не захочет отслеживать исходные индексы. Но будет ли он когда-нибудь дублировать одну запись и размещать ее в двух местах одновременно? Тогда как он может не забыть удалить эти лишние дубликаты в конце? Поскольку количество запросов не может увеличиваться при сортировке.   -  person Jeppe Stig Nielsen    schedule 29.08.2017
comment
справочный источник. Вы можете использовать свой собственный версия этой EnumerableSorter<TElement> и отладьте то, что происходит. Я предполагаю, что границы (i и j) в конечном итоге будут указывать на ту же запись карты, что и x. И избежать этого было бы больше накладных расходов, чем редкие сравнения элементов с самими собой.   -  person René Vogt    schedule 29.08.2017
comment
Как только вы узнаете его быструю сортировку, вы можете найти множество вопросов (здесь и в других местах) людей, спрашивающих, почему иногда элементы сравнивают сами с собой.   -  person Damien_The_Unbeliever    schedule 29.08.2017
comment
@JeppeStigNielsen: возможно, потому что он сначала проверяет правильность положения, а затем удаляет и размещает элемент. Что касается того, почему проверка не пропускается для этого элемента; Я думаю, что здесь практически нет прироста производительности. Вместо одного ненужного сравнения одинаковых элементов вам всегда нужно будет предварительно проверять, не равно ли currentItem thisItemInTheList, прежде чем сравнивать их, что для больших наборов данных может создать больше работы, чем на самом деле. решает. Это также создало бы дополнительное различие в логике для типов значений и ссылочных типов, что усложняет ситуацию.   -  person Flater    schedule 29.08.2017
comment
т.е. stackoverflow.com/questions/7373652/, может быть дубликатом... Если все элементы одинаковы, избежать этого сравнения невозможно.   -  person Alexei Levenkov    schedule 29.08.2017
comment
@Damien_The_Unbeliever Предполагается, что это стабильная сортировка (задокументировано), так что если это быстрая сортировка, то это не обычный вариант быстрой сортировки. Но я согласен с Рене Фогтом, что если вы достаточно внимательно прочитаете источник, то в конце концов поймете, что происходит.   -  person Jeppe Stig Nielsen    schedule 29.08.2017
comment
@AlexeiLevenkov Другой поток связан, хотя алгоритм для List<>.Sort и Array.Sort<> отличается от алгоритма OrderBy. И первый был изменен и улучшен, так как другой поток был активен.   -  person Jeppe Stig Nielsen    schedule 29.08.2017
comment
Без возможности задать вопросы первоначальным авторам алгоритма любые ответы, скорее всего, будут в основном основаны на мнении. Мой основанный на мнении ответ заключается в том, что авторы получили (довольно сложный) код, работающий без дополнительной оптимизации для предотвращения сравнения дубликатов, и решили, что не стоит тратить время (и рисковать поломкой кода), чтобы добавить то, что, вероятно, будет незначительный выигрыш в производительности,   -  person Matthew Watson    schedule 29.08.2017
comment
Мое мнение по этому поводу: тот факт, что сравнение со средством равно, может в некоторых крайних случаях быть недействительным... возможно, в некоторых сценариях на самом деле не равно a (вы можете вернуть -1 или +1 для какая-то непонятная причина). Вы не можете судить обо ВСЕХ возможных сценариях при создании универсальной библиотеки.   -  person Jcl    schedule 29.08.2017


Ответы (4)


В источнике ссылок QuickSort метод, используемый OrderBy, вы можете увидеть эти две строки:

while (i < map.Length && CompareKeys(x, map[i]) > 0) i++;
while (j >= 0 && CompareKeys(x, map[j]) < 0) j--;

Эти циклы while выполняются до тех пор, пока не найдут элемент, который больше не является «больше» (соответственно «меньше»), чем тот, на который указывает x. Поэтому они сломаются при сравнении идентичного элемента.

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

person René Vogt    schedule 29.08.2017
comment
Вы: ваш компаратор должен быть реализован достаточно умно, чтобы быстро возвращать 0 для идентичных элементов Это возможно не во всех случаях. Если TKey (тип, на который мы проецируем с помощью OrderBy<TSource, TKey>) является типом значения (копирование по значению) и содержит ряд полей экземпляра, то упрощение невозможно. Мы должны проверить каждое поле, чтобы определить, равны ли два составных значения. - person Jeppe Stig Nielsen; 29.08.2017
comment
Вы заставили меня прочитать часть исходного кода, а вместо этого я нашел ошибку! Если comparer возвращает int.MinValue и вы используете OrderByDescending, он попытается инвертировать int.MinValue, но останется отрицательным. И сорт идет не так! Из строки 2537. У меня есть короткая репродукция, которую я могу опубликовать в другом месте. - person Jeppe Stig Nielsen; 29.08.2017
comment
@JeppeStigNielsen — это вот этот? - person Damien_The_Unbeliever; 29.08.2017
comment
@Damien_The_Unbeliever да, это он. Это исправлено в CoreFX и помечено как netfx-port-consider, поэтому мы можем увидеть исправление, внесенное в NetFX в какой-то момент: github.com/dotnet/corefx/pull/2240 - person Jon Hanna; 29.08.2017
comment
@Damien_The_Unbeliever Да, это был тот самый! - person Jeppe Stig Nielsen; 29.08.2017
comment
Я думаю, что здесь дело немного в другом. CompareKeys объявлен как internal abstract int CompareKeys(int index1, int index2);, т.е. предназначен для сравнения 2 индексов массива элементов. Следовательно, можно с уверенностью предположить, что если index1 == index2, результатом будет 0 (равно). - person Ivan Stoev; 30.08.2017
comment
@IvanStoev Какое блестящее наблюдение. В реализации в том же файле написано internal override int CompareKeys(int index1, int index2) { int c = comparer.Compare(keys[index1], keys[index2]); [...] } Здесь перед вызовом Compare мы могли бы просто использовать литерал 0, если index1 и index2 совпадают! - person Jeppe Stig Nielsen; 30.08.2017
comment
На самом деле, если мы просто сделаем internal override int CompareKeys(int index1, int index2) { if (index1 == index2) return 0; int c = comparer.Compare(keys[index1], keys[index2]); if (c == 0) { if (next == null) return index1 - index2; return next.CompareKeys(index1, index2); } return (descending != (c > 0)) ? 1 : -1; }, мы все решим. Мы не спрашиваем comparer о чем-то, что может занять время (возможно, потребуется проверить каждое поле в TKey), но всегда должны давать 0. И мы не продолжаем спрашивать next (ThenBy). Это должно быть улучшение. - person Jeppe Stig Nielsen; 30.08.2017
comment
@JeppeStigNielsen Действительно. Не вижу причин не проводить такую ​​оптимизацию. Более того, это имело бы очевидную пользу при сравнении нескольких ключей, как во втором сценарии. Но почему-то не успели :) - person Ivan Stoev; 30.08.2017

В первом случае перед разделителем -- выполняется четыре сравнения. Два из них сравнивают запись с самой собой («Strings: Bravo vs Bravo»). Почему?

Эффективность. Конечно, можно было бы сначала проверить, что объект не является самим собой, но это означает выполнение дополнительной операции при каждом сравнении, чтобы избежать случая, который возникает относительно редко и который в большинстве случаев довольно дешев (большинство компараторов). Это был бы чистый убыток.

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

Во втором случае никогда не должно возникать необходимости прибегать к сравнению Q-свойств (целых чисел); поскольку в значениях P нет дубликатов (относительно порядкового сравнения), поэтому никогда не требуется разрыва связей из ThenBy. Тем не менее мы дважды видим «Ints: 9 vs 9». Зачем использовать компаратор ThenBy с идентичными аргументами?

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

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

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

person Jon Hanna    schedule 29.08.2017
comment
Может быть, это правильно. Я попытался упорядочить последовательность из 1000 попарно различных записей в случайном начальном порядке с помощью OrderBy. В среднем для этого требуется около 13391,9 сравнений, из которых около 1310,7 приходится на сравнения элемента с самим собой. Таким образом, это около 10% сравнений этого типа с перетасованной последовательностью длиной 1000. В сравнении, Array.Sort<> или эквивалентно List<>.Sort, используется меньше сравнений и очень редко сравниваются записи. к себе. Я знаю, что у OrderBy более строгие требования (например, сортировка должна быть стабильной), чем у метода Array.Sort<>. - person Jeppe Stig Nielsen; 30.08.2017
comment
Однако обратите внимание на недавние комментарии Ивана Стоева и меня к ответу Рене Фогта. Кажется, очень легко не тестировать comparer, когда index1 == index2. - person Jeppe Stig Nielsen; 30.08.2017
comment
@JeppeStigNielsen легко, но не бесплатно, но обязательно попробуйте. Прошло некоторое время с тех пор, как я смотрел на это, поэтому я не могу исключить, что я сделал что-то глупое в своем эксперименте, или что изменения, внесенные с тех пор, не изменят вещи, но любое тестирование, вероятно, следует выполнять с int по умолчанию компаратор, поскольку это, казалось бы, наиболее часто используется. - person Jon Hanna; 30.08.2017

Хорошо, давайте посмотрим на возможности здесь:

  1. T — тип значения

    Чтобы проверить, сравнивает ли он элемент с самим собой, сначала необходимо проверить, являются ли оба элемента одним и тем же. Как бы Вы это сделали?

    Вы можете сначала вызвать Equals, а затем CompareTo, если элементы не совпадают. Вы действительно хотите это сделать? Стоимость Equals будет примерно такой же, как при сравнении, так что вы фактически удвоите стоимость заказа для чего именно? OrderBy просто сравнивает все элементы, и точка.

  2. T — это ссылочный тип

    c# не позволяет вам перегружаться только общими ограничениями, поэтому вам нужно будет проверить во время выполнения, является ли T ссылочным типом или нет, а затем вызвать конкретную реализацию, которая изменит поведение, описанное выше. Вы хотите нести эти расходы в каждом случае? Конечно, нет.

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

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

person InBetween    schedule 29.08.2017
comment
Я не понимаю. Строки "Bravo" и "Bravo" равны уже как ссылки. То же самое происходит с анонимным типом new { P = "Bravo", Q = 9, }. Если бы он сначала проверял ссылочное равенство (что было бы бесполезно, если бы он знал ответ заранее), было бы еще меньше причин также спрашивать мой пользовательский компаратор. - person Jeppe Stig Nielsen; 29.08.2017
comment
@JeppeStigNielsen Как бы вы проверили, сравниваете ли вы один и тот же объект? Вы бы сделали проверку рекомендаций. Компаратор по умолчанию, вероятно, оптимизирован для возврата 0, если ссылка та же самая, и только для сравнения значений в противном случае (если это уместно). Если вы не реализуете эту оптимизацию, вы понесете дополнительные расходы, но это связано с вашей реализацией. - person InBetween; 29.08.2017
comment
@InBetween: Но проверки равенства ссылок не будут отображаться. И я могу просто повторить Йеппе: строковые литералы времени компиляции являются интернированными строками и всегда равны по ссылке. - person György Kőszeg; 29.08.2017
comment
@JeppeStigNielsen см. обновленный ответ, я был действительно слишком сочным и трудным для понимания. - person InBetween; 29.08.2017
comment
Интернированные строки @taffer не имеют ничего общего с проблемой. Смотрите обновленный ответ. - person InBetween; 29.08.2017
comment
Я все еще не вижу актуальности вашего ответа. Я не говорю, что они должны проверять на равенство, прежде чем вызывать мой компаратор. Как вы говорите, у них нет хорошего способа сделать это (например, если T был типом значения). Я я говорю, что они не должны дублировать один элемент из запроса и сравнивать копию с оригиналом. В этом мало смысла. Рассмотрим простейший из всех случаев: new[] { "Alpha", } запрашивает .OrderBy(x => x, coStr). Почему в таком случае они сравнивают "Alpha" с "Alpha"?! В массиве только один "Alpha". - person Jeppe Stig Nielsen; 29.08.2017
comment
@JeppeStigNielsen Хорошо, возьмите массив из n элементов и напишите общую реализацию, которая принимает по два элемента за раз и сравнивает все элементы со всеми элементами, не сравнивая любые два одинаковых элемента. Сделайте это, избегая больших затрат, чем просто выполнение n дополнительных ненужных проверок элемента против самого себя. - person InBetween; 29.08.2017
comment
@JeppeStigNielsen Я хочу сказать, что самый дешевый вариант - это сравнение. Если сравнения требуют больших затрат, то ваша обязанность оптимизировать их с помощью проверки ссылок, если это возможно. - person InBetween; 29.08.2017

Проще говоря в случае 1

var coStr = Comparer<string>.Create((x, y) =>
{
    Console.WriteLine($"Strings: {x} versus {y}");
    return string.CompareOrdinal(x, y);
});

Мы просто сравниваем элементы, и нет условий для игнорирования, если результат равен 0. Таким образом, условие Console.WriteLine не зависит от результата сравнения. Если вы измените свой код, как показано ниже

var coStr = Comparer<string>.Create((x, y) =>
{
   if (x != y)
      Console.WriteLine($"Strings: {x} versus {y}");
   return string.CompareOrdinal(x, y);
});

Ваш вывод будет похож на

Strings: Bravo versus Alpha
Strings: Bravo versus Charlie

То же самое для второго оператора, здесь мы проверяем вывод обоих, поэтому для сравнения строк будет возвращено 0, затем оно пойдет для сравнения, поэтому оно возьмет это и выведет требуемое. Надеюсь, это рассеет ваши сомнения :)

person Vivek Shah    schedule 29.08.2017
comment
ОП спрашивал, почему эти лямбда-выражения вызываются, когда x==y, а не просто как предотвратить вызовы Console.WriteLine, которые были вставлены только для демонстрации того, что они вызывались, когда х==у. - person Damien_The_Unbeliever; 29.08.2017
comment
Это то, что я упомянул, что лямбда была вызвана для сравнения, вся ее работа заключается в сравнении двух значений и возвращении -1,0,1. Итак, в коде мы просто сравниваем 2 значения и с этим выводом. Задача сравнения завершена. Я упомянул условие x==y, чтобы объяснить, что сравниваемая задача состоит только в том, чтобы сравнить и отправить результат. Позже нам нужно использовать эти результаты для выполнения других операций. В случае 2 он примет первое сравнение строк, которое вернет 0, и будет использоваться Int comariosn. - person Vivek Shah; 29.08.2017
comment
@Damien_The_Unbeliever имеет ли это смысл? - person Vivek Shah; 29.08.2017