C # Почему равные десятичные дроби могут давать неравные значения хеша? - PullRequest
42 голосов
/ 16 декабря 2011

Мы столкнулись с магическим десятичным числом, которое сломало нашу хэш-таблицу. Я свел это к следующему минимальному случаю:

decimal d0 = 295.50000000000000000000000000m;
decimal d1 = 295.5m;

Console.WriteLine("{0} == {1} : {2}", d0, d1, (d0 == d1));
Console.WriteLine("0x{0:X8} == 0x{1:X8} : {2}", d0.GetHashCode(), d1.GetHashCode()
                  , (d0.GetHashCode() == d1.GetHashCode()));

Дает следующий вывод:

295.50000000000000000000000000 == 295.5 : True
0xBF8D880F == 0x40727800 : False

Что действительно странно: измените, добавьте или удалите любую из цифр в d0, и проблема исчезнет. Даже добавление или удаление одного из завершающих нулей! Знак, похоже, не имеет значения.

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

decimal d0 = 295.50000000000000000000000000m / 1.000000000000000000000000000000000m;

Но мой вопрос: как C # делает это неправильно?

Ответы [ 6 ]

27 голосов
/ 16 декабря 2011

Начнем с того, что C # не делает ничего плохого вообще.Это ошибка framework .

Это действительно похоже на ошибку - в принципе, любая нормализация, используемая для сравнения на равенство, должна использоваться одинаково для вычисления хеш-кода.Я проверил и могу воспроизвести его тоже (используя .NET 4), включая проверку методов Equals(decimal) и Equals(object), а также оператора ==.

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

Я удивлен, что это не так (и, как вы говорите, это работает в большинстве случаев), но выдолжен сообщить об ошибке на Connect .

4 голосов
/ 30 ноября 2012

Еще одна ошибка (?), Которая приводит к разному представлению байтов для одного и того же десятичного числа в разных компиляторах: попробуйте скомпилировать следующий код на VS 2005, а затем на VS 2010. Или посмотрите мою статью о Code Project.

class Program
{
    static void Main(string[] args)
    {
        decimal one = 1m;

        PrintBytes(one);
        PrintBytes(one + 0.0m); // compare this on different compilers!
        PrintBytes(1m + 0.0m);

        Console.ReadKey();
    }

    public static void PrintBytes(decimal d)
    {
        MemoryStream memoryStream = new MemoryStream();
        BinaryWriter binaryWriter = new BinaryWriter(memoryStream);

        binaryWriter.Write(d);

        byte[] decimalBytes = memoryStream.ToArray();

        Console.WriteLine(BitConverter.ToString(decimalBytes) + " (" + d + ")");
    }
}

Некоторые люди используют следующий код нормализации d=d+0.0000m, который не работает должным образом на VS 2010. Ваш код нормализации (d=d/1.000000000000000000000000000000000m) выглядит хорошо - я использую тот же самый, чтобы получить тот же байтовый массив дляте же десятичные дроби.

3 голосов
/ 10 января 2013

столкнулся с этой ошибкой тоже ...: - (

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

var data = new decimal[] {
//    123456789012345678901234567890
    1.0m,
    1.00m,
    1.000m,
    1.0000m,
    1.00000m,
    1.000000m,
    1.0000000m,
    1.00000000m,
    1.000000000m,
    1.0000000000m,
    1.00000000000m,
    1.000000000000m,
    1.0000000000000m,
    1.00000000000000m,
    1.000000000000000m,
    1.0000000000000000m,
    1.00000000000000000m,
    1.000000000000000000m,
    1.0000000000000000000m,
    1.00000000000000000000m,
    1.000000000000000000000m,
    1.0000000000000000000000m,
    1.00000000000000000000000m,
    1.000000000000000000000000m,
    1.0000000000000000000000000m,
    1.00000000000000000000000000m,
    1.000000000000000000000000000m,
    1.0000000000000000000000000000m,
    1.00000000000000000000000000000m,
    1.000000000000000000000000000000m,
    1.0000000000000000000000000000000m,
    1.00000000000000000000000000000000m,
    1.000000000000000000000000000000000m,
    1.0000000000000000000000000000000000m,
};

for (int i = 0; i < 1000; ++i)
{
    var d0 = i * data[0];
    var d0Hash = d0.GetHashCode();
    foreach (var d in data)
    {
        var value = i * d;
        var hash = value.GetHashCode();
        Console.WriteLine("{0};{1};{2};{3};{4};{5}", d0, value, (d0 == value), d0Hash, hash, d0Hash == hash);
    }
}
1 голос
/ 17 декабря 2011

Я протестировал это в VB.NET (v3.5) и получил то же самое.

Интересная вещь о хэш-кодах:

A) 0x40727800 = 1081243648

B) 0xBF8D880F = -1081243648

Использование Decimal.GetBits () Я нашел

формат: мантисса (хххххххххххххххххххххххххх), 'e' является показателем степени, 0 должно быть нулями)

d1 ==> 00000000 00000000 00000B8B - 00010000 = (2955/10 ^ 1) = 295,5

do ==> 5F7B2FE5 D8EACD6E 2E000000- 001A0000

... который преобразуется в 29550000000000000000000000000/10 ^ 26 = 295.5000000 ... и т. Д.

** редактировать: хорошо, я написал 128-битный шестнадцатеричный калькулятор и вышеточно правильно

Это определенно похоже на внутреннюю ошибку преобразования какого-то рода.Microsoft прямо заявляет, что не гарантирует реализацию по умолчанию GetHashCode.Если вы используете его для чего-то важного, то, вероятно, имеет смысл написать собственный GetHashCode для десятичного типа.Форматирование его с фиксированным десятичным знаком, строка фиксированной ширины и хеширование, кажется, работают, например (> 29 знаков после запятой,> 58 ширины - подходит для всех возможных десятичных знаков).

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

28 или 29 цифр не должны иметь значения, если нет зависимыхкод, который не оценивает внешние экстенты должным образом.Самое большое доступное 96-битное целое число:

79228162514264337593543950335

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

1 голос
/ 16 декабря 2011

Это десятичная ошибка округления.

Требуется слишком большая точность, чтобы установить d0 с .000000000000000, в результате алгоритм, отвечающий за него, делает ошибку и в итоге дает другой результат. В этом примере это может быть классифицировано как ошибка, хотя обратите внимание, что тип «десятичный» должен иметь точность 28 цифр , и здесь вам фактически требуется точность 29 цифр для d0. 1005 *

Это можно проверить, запросив полное необработанное шестнадцатеричное представление d0 и d1.

0 голосов
/ 16 декабря 2011

Документация предполагает, что из-за непредсказуемости GetHashCode() вы должны создать свою собственную.Это считается непредсказуемым, потому что у каждого Типа есть своя собственная реализация, и, поскольку мы не знаем его внутренности, мы должны создать свою собственную в соответствии с тем, как мы оцениваем уникальность.

Однако я думаю, что ответ таков: GetHashCode()не использует математическое десятичное значение для создания хеш-кода.

Математически мы видим 295.50000000 и 295.5 одинаковыми.Когда вы смотрите на десятичные объекты в IDE, это тоже верно.Однако, если вы введете ToString() с обоими десятичными знаками, вы увидите, что компилятор видит их по-разному, т.е. вы все равно увидите 295.50000000.GetHashCode() явно не использует математическое представление десятичного числа для создания хеш-кода.

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

...