Почему может система.Объект String не кэширует свой хэш-код?

взгляд на исходный код string.GetHashCode используя отражатель показывает следующее (Для mscorlib.dll версии 4.0):

public override unsafe int GetHashCode()
{
    fixed (char* str = ((char*) this))
    {
        char* chPtr = str;
        int num = 0x15051505;
        int num2 = num;
        int* numPtr = (int*) chPtr;
        for (int i = this.Length; i > 0; i -= 4)
        {
            num = (((num << 5) + num) + (num >> 0x1b)) ^ numPtr[0];
            if (i <= 2)
            {
                break;
            }
            num2 = (((num2 << 5) + num2) + (num2 >> 0x1b)) ^ numPtr[1];
            numPtr += 2;
        }
        return (num + (num2 * 0x5d588b65));
    }
}

теперь, я понимаю, что реализация GetHashCode не определен и зависит от реализации, так что вопрос"GetHashCode реализовано в виде X или Y?- на самом деле не отвечает. Мне просто интересно несколько вещей:

  1. если рефлектор демонтировал DLL правильно и это is реализация GetHashCode (в моей среде), правильно ли я интерпретирую этот код, чтобы указать, что a string объект, основанный на этой конкретной реализации, не будет кэшировать свой хэш-код?
  2. предполагая, что ответ да, почему это должно быть? Мне кажется, что стоимость памяти будет минимальной (еще одно 32-битное целое число, капля в пруду по сравнению с размером самой строки), тогда как экономия будет значительной, особенно в случаях, когда, например, строки используются в качестве ключей в коллекции на основе hashtable, такой как Dictionary<string, [...]>. И с тех пор string класс неизменяем, это не похоже на значение, возвращаемое GetHashCode никогда даже изменить.

что я могу потерять?


обновление: в ответ на заключительное замечание Андраса Золтана:

есть также мнение, высказанное в Тим ответ(+1 есть). Если он прав, и я думаю, он, потом нет никакой гарантии что строка на самом деле неизменяема после строительства, поэтому кэшировать результат будет неправильным.

Воу, Эй там! Это интересный момент, чтобы сделать (и да, это правда), но очень сомневаюсь что это было принято во внимание при осуществлении GetHashCode. Утверждение "поэтому кэшировать результат было бы неправильно" подразумевает для меня, что отношение фреймворка что касается строк "Ну, они должен быть неизменяемый, но на самом деле, если разработчики хотят стать подлыми, они изменчивы, поэтому мы будем относиться к ним как к таковым."это определенно не так, как фреймворк рассматривает строки. Он полностью полагается на их неизменность во многих отношениях (интернирование строковых литералов, присвоение всех строк нулевой длины string.Empty, etc.) что, в принципе, если вы мутируете строку, вы пишете код, поведение которого полностью не определено и непредсказуемый.

Я думаю, что моя точка зрения заключается в том, что автор(ы) этой реализации должен беспокоиться: "что, если этот экземпляр строки изменяется между вызовами, даже если класс, как он публично открыт, неизменен?"было бы похоже на то, что кто-то планирует случайный открытый барбекю, чтобы подумать о себе:" что, если кто-то принесет атомную бомбу на вечеринку?- Послушай, если кто-то принесет атомную бомбу, вечеринка окончена.

5 ответов


очевидный потенциальный ответ: потому что это будет стоить памяти.

здесь есть анализ затрат / выгод:

стоимостью: 4 байта для каждой строки (и быстрый тест на каждый вызов GetHashCode). Также сделайте объект string изменяемым, что, очевидно, означает, что вам нужно быть осторожным с реализация - если вы всегда вычислить хэш-код спереди, что является стоимостью его вычисления один раз для каждый строка, независимо от о том, есть ли у тебя вообще хэш.

пользу: избегайте пересчета хэша для строковых значений хешировать более одного раза

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

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

EDIT: я только что говорил с Эриком Липпертом об этом (NDC потрясающий :), и в основном это is о дополнительной памяти хит против ограниченных преимуществ.


во-первых-неизвестно, действительно ли кэширование этого результата улучшится Dictionary<string, ...> et al, потому что они не обязательно используют строку.GetHashCode, потому что он использует IComparer для получения хэш-кода для строки.

и если вы следуете вероятной цепочке вызовов для класса StringComparer, это заканчивается переходом в систему.Глобализация.Класс CompareInfo, который, наконец, завершается этим методом:

[SecurityCritical, SuppressUnmanagedCodeSecurity, DllImport("QCall",
   CharSet=CharSet.Unicode)]
private static extern int InternalGetGlobalizedHashCode(IntPtr handle, string
   localeName, string source, int length, int dwFlags);

нет никакого знания, если эта библиотека - которая, как представляется, быть собственным методом-не использовать некоторую форму внутреннего кэширования на основе базовой структуры данных объекта .Net, которую мы не можем получить сразу внутри среды выполнения .Net.

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

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

С точки зрения производительности, то, если мы также примите, что эти компараторы используются Dictionary<,> etc не может использовать внутреннюю реализацию, не кэшируя этот результат, вероятно, не оказывает большого влияния, потому что, честно говоря, как часто этот метод будет вызываться в реальном мире: поскольку большую часть времени хэш-код строки, скорее всего, вычисляется с помощью какого-то другого механизма.

редактировать

есть также точка зрения, сделанная в ответе Тима (+1 там). Если он прав, а я думаю, что прав, значит, нет гарантии, что строка на самом деле неизменяема после построения, поэтому кэшировать результат было бы неправильно.

ДОПОЛНИТЕЛЬНОЕ РЕДАКТИРОВАНИЕ(!)

Dan указывает, что строки должны быть неизменяемыми в чистой сфере, и поэтому эта строка должна быть свободной для кэширования собственного хэш-кода на основе этого. Проблема здесь в том, что .Net framework также предоставляет законный способ изменить якобы незыблемым строка это не связано с привилегированным отражением или чем-либо еще. Это фундаментальная проблема со строками, это указатель на буфер, который вы не можете контролировать. Неважно, что в мире C#, как насчет C++, где векторизация и изменение буферов памяти является обычным местом. Просто потому, что вы идеально не стоит do это не означает, что фреймворк должен ожидать, что вы этого не сделаете.

.Net предоставляет эту функциональность, и поэтому, если это было дизайнерское решение команды .Net в ответ на вид двоичного бандитизма, предложенный Тимом, тогда они были очень мудры, чтобы принять его во внимание. Сделали они это или по счастливой случайности-это совсем другое дело! :)


возможно, я сделал неправильный вывод здесь, но не правда ли, что, хотя строка неизменяема в контексте объекта .NET String, все еще можно изменить значение?

например, если бы Вы были так склонны к этому...

String example = "Hello World";

unsafe
{
    fixed (char* strPointer = myString) {
        strPointer[1] = 'a';
    }
} 

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


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

но, как уже упоминалось, основная причина не кэширования значения что дополнительное использование памяти, вероятно, намного перевешивает потенциальную экономию кэширования хэш-кода. Время выполнения GetHashCode равно O (N) по длине строки, поэтому наихудший сценарий повторного хэширования хорошо ограничен.


любое значение int является допустимым хэш-код. Это означает, что нет значения int по умолчанию, такого как -1 или 0, которое мы можем использовать, чтобы указать, что мы еще не вычисляли хэш-код. Поэтому, если строка должна кэшировать свой хэш-код, ей нужно будет сделать одно из следующих действий:

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

оба варианта имеют недостаток; первый требует еще больше дополнительной памяти, а второй имеет стоимость производительности вычисления хэш-кодов, которые могут никогда не понадобиться.

Теперь рассмотрим случай Dictionary<TKey,TValue>. Хэш-код, используемый словарем, зависит от того, какой компаратор используется. Компаратор по умолчанию будет использовать нормальный метод GetHashCode объекта() метод. Но вы можете создать словарь, который использует, например, без учета регистра, и хэш-код, используемый словарем, будет создан этим компаратором, который, вероятно, создаст совершенно другой хэш-код, чем String.GetHashCode(). Итак, какой хэш-код кэширует строку? Строка может быть в двух словарях, каждый из которых использует другой компаратор, ни один из которых не использует обычный строковый GetHashCode. Таким образом, строка может кэшировать хэш-код ни один из Словари даже используют.

В случае Dictionary<TKey,TValue>, есть еще более важная причина, по которой наличие строк кэширования их хэш-кодов, вероятно, не обеспечит преимущества производительности. Внутренняя реализация словаря делает следующее При добавлении новой записи:

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

когда словарь выполняет поиск ключа, он вычисляет измененный (т. е. положительный) хэш-код искомого ключа, получает ведро, к которому сопоставляется хэш-код, а затем просматривает список записей в этом ведре. Чтобы проверить, соответствует ли запись, сначала проверяет, совпадают ли измененные хэш-коды (если ключи равны, хэш-коды также должны быть равны), и если они равны, проверяет, равны ли также два ключа. В случае строк этот алгоритм достигает двух вещей; во-первых, он избегает многих сравнений строк, используя простое целочисленное сравнение, во-первых, чтобы увидеть, стоит ли делать сравнение строк, и, во-вторых, он кэширует хэш-коды каждого ключа в словаре. хэш-код каждого ключа в словаре вычисляется только один раз, когда пара ключ/значение добавляются в словарь.

(Если вам интересно, почему словарь удаляет бит знака из хэш-кода, это потому, что он использует -1 в качестве значения флага маркера в поле хэш-кода для слотов ввода, которые в настоящее время пусты.)