Как стать автором
Обновить

Комментарии 19

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

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

if(_fooAction == null)
    _fooAction = Foo;

Execute(_fooAction);

Кстати говоря, если написать Execute(() => Foo()); то компилятор сам сгенерирует статическое поле. Странно, почему он не делает этого всегда.
Не ошибка, просто показал пример того, как сохранять в поле. Ну то есть, его можно использовать потом в любых других вызовах.
Получается, что та же самая проблема будет при использовании ConcurrentDictionary с ключём-строкой?

Я не знаю, где автор нашёл такие интересные словари, что Dictionary, что ConcurrentDictionary используют Comparer, вызывающий обычный Equals. Видимо, какая-то древность времён .NET 1.0 из серии NameValueCollection.
SortedList, например. Но да, вы правы, при сравнении на равенство такой проблемы нет. Поправлю в статье, чтобы не вводить в заблуждение людей.
Допишите ещё про Contains, IndexOf и StartsWith. Они, если мне не изменяет память, по-умолчанию используют культуру.
Отсюда: referencesource.microsoft.com/#mscorlib/system/string.cs

Contains
public bool Contains( string value ) {
        return ( IndexOf(value, StringComparison.Ordinal) >=0 );
}



StartsWith (EndsWith такой же)
public Boolean StartsWith(String value) {
        if ((Object)value == null) {
                throw new ArgumentNullException("value");
        }
        Contract.EndContractBlock();
        return StartsWith(value, (LegacyMode ? StringComparison.Ordinal : StringComparison.CurrentCulture));
}



IndexOf
public int IndexOf(String value) {
        return IndexOf(value, (LegacyMode ? StringComparison.Ordinal : StringComparison.CurrentCulture));
}

Стало быть только IndexOf
Почему? Если не LegacyMode, то с ним тоже всё в порядке же:
public int IndexOf(String value) {
        return IndexOf(value, (LegacyMode ? StringComparison.Ordinal : StringComparison.CurrentCulture));
}
Экхм. Вообще-то наоборот. Если НЕ LegacyMode (а это всё кроме некоторых версий сервелата), то используется StringComparison.CurrentCulture.
Забавно, а в Mono оно таки использует CurrentCulture как минимум для StartsWith и IndexOf, но не для Contains без всяких LegacyMode.
UPDATED: а так это вы нас решили запутать!
Execute(() => Foo()) то закэширует в статическое поле, но вот такой код:

Execute(() => Foo());
Execute(() => Foo());

почему-то создаст два статических поля, а не воспользуется на второй строке первым закэшированным. Туповат компилятор (или я).
Вероятно компилятор даже не пытается «доказать», что 2 функции эквивалентны, т.к. насколько я мне известно из [1, c. 60-62] эта задача неразрешима в общем случае.
[1] Дж. Харрисон. Введение в функциональное программирование (5.8 Равенство функций)
В общем случае — да, но когда выражение — это вызов функции без параметров — тут не сложно сравнить ;-) Но в целом согласен, глупая предьява к компилятору.
String.Compare(strings[j], strings[length — j — 1], StringComparison.CurrentCulture) == 0
Вы сейчас напугали народ длинной конструкцией, забыв сказать, что для сравнения на совпадение через Ordinal можно смело использовать ==. То есть, Ordinal нужен только в тех местах, где используются сравнение больше/меньше, таких как сортировки.
Декомпилированные исходники
    public static bool operator ==(string a, string b)
    {
      return string.Equals(a, b);
    }
    public static bool Equals(string a, string b)
    {
      if (a == b)
        return true;
      if (a == null || b == null || a.Length != b.Length)
        return false;
      else
        return string.EqualsHelper(a, b);
    }
    private static unsafe bool EqualsHelper(string strA, string strB)
    {
      int length = strA.Length;
      fixed (char* chPtr1 = &strA.m_firstChar)
        fixed (char* chPtr2 = &strB.m_firstChar)
        {
          char* chPtr3 = chPtr1;
          char* chPtr4 = chPtr2;
          while (length >= 10)
          {
            if (*(int*) chPtr3 != *(int*) chPtr4 || *(int*) (chPtr3 + 2) != *(int*) (chPtr4 + 2) || (*(int*) (chPtr3 + 4) != *(int*) (chPtr4 + 4) || *(int*) (chPtr3 + 6) != *(int*) (chPtr4 + 6)) || *(int*) (chPtr3 + 8) != *(int*) (chPtr4 + 8))
              return false;
            chPtr3 += 10;
            chPtr4 += 10;
            length -= 10;
          }
          while (length > 0 && *(int*) chPtr3 == *(int*) chPtr4)
          {
            chPtr3 += 2;
            chPtr4 += 2;
            length -= 2;
          }
          return length <= 0;
        }
    }

После того, как обнаружилась эта проблема, мы стали в конструкторы всех реализаций IDictionary с ключом строкой передавать удобный StringComparer.Ordinal, что еще немного улучшило производительность.
Словарём для строк используется по-умолчанию System.Collections.Generic.GenericEqualityComparer<T>, который банально вызывает Equals
// System.Collections.Generic.GenericEqualityComparer<T>
public override bool Equals(T x, T y)
{
	if (x != null)
	{
		return y != null && x.Equals(y);
	}
	return y == null;
}

Equals, как видно из кода реализации string выше, культуру не использует.
По пункту 1 про сравнение строк — это детали реализации. Вполне возможно, что на другой платформе или даже на той же, но после какого нибудь мелкого обновления, результат сравнения производительности радикально изменится. Но в предлагаемом решении хотя бы нет минусов: всегда полезно указать какой именно тип сравнения используется.

По пункту 2 (про перечислители) — категорически не согласен. Ради очень сомнительной нано-оптимизации (одно выделение ссылочного объекта на весь цикл) нам рекомендуют применять сразу три анти-паттерна (изменяемый тип-значение, конкретный тип вместо интерфейса, более конкретный интерфейс вместо общего). В библиотечных классах перечислитель сделан в виде типа-значения в давние времена, до появления обобщённых (дженерик) типов, чтобы избегать оборачивания (боксинга) в каждой итерации цикла. В наше время это неактуально.

По пункту 3 про создание делегатов. Опять сомнительная нано-оптимизация. Делегаты ведь фигурируют обычно только один раз, при инициализации. И добавлять для этого отдельный член класса не забыв его правильно инициализировать — нет смысла, мы лишь добавим вероятность появления различных опечаток в коде. И опять же, никто не может быть уверен что в следующем обновлении фреймворка такая оптимизация не будет производится компилятором автоматически.

Итог: не применяйте оптимизации п.2 и 3 до тех пор, пока профилирование не покажет вам что именно они могут дать заметный прирост производительности.
В библиотечных классах перечислитель сделан в виде типа-значения в давние времена, до появления обобщённых (дженерик) типов, чтобы избегать оборачивания (боксинга) в каждой итерации цикла. В наше время это неактуально.

Что? Я привел вам List<T>, который появился вместе с generic и которой как раз имеет enumerator структуру. Мне кажется вы невнимательно прочитали в чем проблема. А в целом, насчет того, что применять, а что не применять — мне кажется вы не читали кучу моих предостережений насчет того, когда стоит прибегать к этим советам.
Само перечисление с использование «утиной типизации» было создано до появления обобщённых (дженерик) типов, а созданные позднее библиотечные коллекции просто следуют заложенному паттерну для совместимости со старыми не обобщёнными программами (а не для производительности). Что и порождает до сих пор множественные проблемы (например, вот свежее обсуждение Why does my enumerator not advance?). Уверен, что если бы в языке изначально были обобщённые типы, то «утиная типизация» не использовалась бы для foreach. Там, где нужна экстремальная производительность всегда используют массивы и for, а не перечислители и foreach.

Может я не заметил каких то предостережений в вашей статье, но вот прямо в начале написано: «На мой взгляд, есть смысл использовать наши хаки в следующих случаях: Вы пишете новый код и решили делать это сразу круто и с экономией.» Использовать анти-паттерны при написании нового (то есть ещё даже не профилированного) кода — по моему вредный совет.
Какая может быть совместимость для абсолютного нового класса? До сих пор во всех коллекциях enumerator делается структурой и сам Липперт пишет:
Yes, mutable structs are a bad programming practice, but in this case it’s not so bad because the foreach loop is never going to expose the raw enumerator to possible misuse by user code. Sometimes you really do want to avoid collection pressure, so making the enumerator a struct can be a small but measurable win.
А свежее обсуждение немного притянуто за уши, как мне кажется. Я придерживаюсь мнения, что если ты решил делать что-то нестандартное, то будь добр — изучи матчасть. Так что я не вижу проблем в том, чтобы делать mutable structure.

Насчет советов: нужно же немного подключать голову. Например в случае нового кода я всё-таки настаиваю, что нужно делать перечислитель для коллекции значимым типом. А для других советов там в начале есть предостережения:
Если есть возможность без вреда для внешнего кода иметь в качестве параметра конкретную коллекцию, а не интерфейс, то используйте ее, это сможет улучшить производительность foreach
Если возможности иметь в качестве параметра конкретную коллекцию нет, можно, например, использовать IList и for вместо foreach
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Изменить настройки темы

Истории