Контрольная сумма поплавков с круговым обходом через текстовый файл

Мне нужно записать пару поплавков в текстовый файл и сохранить с ними контрольную сумму CRC32. Затем, когда я читаю поплавки из текстового файла, я хочу пересчитать контрольную сумму и сравнить ее с той, которая была вычислена ранее при сохранении файла. Моя проблема в том, что контрольная сумма иногда не работает. Это связано с тем, что одинаковые числа с плавающей запятой могут быть представлены разными битовыми комбинациями. Для полноты я подытожу код в следующих абзацах.

Я адаптировал это Алгоритм CRC32, который я нашел после прочтения этого вопроса. Вот как это выглядит:

uint32_t updC32(uint32_t octet, uint32_t crc) {
    return CRC32Tab[(crc ^ octet) & 0xFF] ^ (crc >> 8);
}

template <typename T>
uint32_t updateCRC32(T s, uint32_t crc) {
    const char* buf = reinterpret_cast<const char*>(&s);
    size_t len = sizeof(T);

    for (; len; --len, ++buf)
        crc = updC32(static_cast<uint32_t>(*buf), crc);
    return crc;
}

CRC32Tab содержит точно такие же значения, как большой массив в файле, указанном выше.

Это сокращенная версия того, как я записываю числа с плавающей запятой в файл и вычисляю контрольную сумму:

float x, y, z;

// set them to some values

uint32_t crc = 0xFFFFFFFF;
crc = Utility::updateCRC32(x, crc);
crc = Utility::updateCRC32(y, crc);
crc = Utility::updateCRC32(z, crc);
const uint32_t actualCrc = ~crc;

// stream is a FILE pointer, and I don't mind the scientific representation
fprintf(stream, " ( %g %g %g )", x, y, z);
fprintf(stream, " CRC %u\n", actualCrc);

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

float x = std::atof(getNextFloat());
float y = std::atof(getNextFloat());
float z = std::atof(getNextFloat());

uint32_t crc = 0xFFFFFFFF;
crc = Utility::updateCRC32(x, crc);
crc = Utility::updateCRC32(y, crc);
crc = Utility::updateCRC32(z, crc);
const uint32_t actualCrc = ~crc;

const uint32_t fileCrc = // read the CRC from the file
assert(fileCrc == actualCrc); // fails often, but not always

Источником этой проблемы является то, что std::atof вернет другое битовое представление числа с плавающей запятой, закодированное в строке, которая была прочитана из файла, чем битовое представление числа с плавающей запятой, которое использовалось для записи этой строки в файл.

Итак, мой вопрос: есть ли другой способ достичь моей цели контрольной суммы поплавков, которые передаются через текстовое представление, кроме контрольной суммы самих строк?

Спасибо за чтение!


person Kristian Duske    schedule 15.03.2013    source источник
comment
Почему бы вам не проверить свое предположение, выведя битовый шаблон?   -  person PlasmaHH    schedule 15.03.2013
comment
Вы правы, я должен был сделать это сразу. У меня есть сейчас, и битовые шаблоны не совпадают. Я удалю эту часть вопроса.   -  person Kristian Duske    schedule 15.03.2013
comment
Двоичные значения с плавающей запятой IEEE 754 не имеют множественных представлений, кроме нуля (+0 и -0) и NaN. Если преобразование любого другого значения с плавающей запятой в число (включая «бесконечность»), а затем обратно в значение с плавающей запятой не дает тех же битов, что и исходное значение, то преобразования были выполнены неточно.   -  person Eric Postpischil    schedule 15.03.2013


Ответы (5)


Источник проблемы очевиден из вашего комментария:

Если я не совсем ошибаюсь, здесь не происходит округления. Спецификатор %g выбирает кратчайшее строковое представление, точно представляющее число.

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

Если вам нужен удобочитаемый двусторонний формат, %a, безусловно, лучший выбор. В противном случае вам нужно будет указать точность не менее 9 (при условии, что float в вашей системе соответствует одинарной точности IEEE-754).

Вы все еще можете быть сбиты с толку кодировками NaN, поскольку стандарт не указывает, как и должны ли они быть напечатаны.

person Stephen Canon    schedule 15.03.2013
comment
Спасибо. Я понимаю, что у моего кода есть несколько проблем: во-первых, он не записывает точные числа в файл, потому что они округляются на %g. Во-вторых, контрольная сумма битового представления чисел с плавающей запятой даст несовпадающие контрольные суммы для таких чисел, которые могут быть представлены несколькими битовыми шаблонами. Я полагаю, что лучше всего вместо этого проверить контрольную сумму строковых представлений. - person Kristian Duske; 15.03.2013
comment
Еще один вопрос: я не могу использовать %a, потому что другие программы, которые читают этот файл, не могут с ним работать. Является ли %f хорошим вариантом? - person Kristian Duske; 15.03.2013
comment
Не обращайте внимания на последний вопрос. Мне нужно указать точность вручную. - person Kristian Duske; 15.03.2013

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

person unwind    schedule 15.03.2013
comment
Спасибо за ваше предложение, но оно должно быть удобочитаемым как для человека, так и для других программ. - person Kristian Duske; 15.03.2013
comment
@KristianDuske: Может быть, вам просто нужно напечатать больше цифр, используйте std::numeric_limits<float>::max_digits10 цифры, чтобы получить все необходимое. Я думаю, что стандартный printf всего 6 или около того. - person PlasmaHH; 15.03.2013
comment
@PlasmaHH: вам может понадобиться добавить 1 (может быть, даже 2) к этому числу цифр 10. - person sellibitze; 15.03.2013
comment
Эти цифры будут просто 0 (в случае ненаучного представления), не так ли? Я думаю, что это не проблема округления. - person Kristian Duske; 15.03.2013
comment
@MSalters: Вы говорите, что невозможно создать текст -0 для -0 и 0 для +0? Стандарт может не гарантировать, что их преобразование из плавающего в текст включает знак даже для нуля. Но поставить перед текстом знак минус не составит труда. - person sellibitze; 15.03.2013
comment
@sellibitze: вам также придется написать свой собственный atof, так как это тоже не гарантирует разбор "-0" как -0.0 - person MSalters; 15.03.2013
comment
@sellibitze: не нужно добавлять 1 к этому числу, оно специально разработано, чтобы быть правильным числом именно для этой цели. - person PlasmaHH; 15.03.2013
comment
@KristianDuske: Почему вы думаете, что max_digits10 будет 0? В моей системе это 9,17,21 для обычных плавающих типов, а printf обычно выводит только 6 по умолчанию. Просто попробуйте. - person PlasmaHH; 15.03.2013
comment
@PlasmaHH: Моя реализация дает 15 (GCC) для double, но на самом деле вам нужно 17 десятичных цифр, чтобы в каждом случае не было потерь. Так что либо реализация не соответствует требованиям, либо вы ошибаетесь. ;) - person sellibitze; 15.03.2013
comment
@sellibitze: моя реализация дает 17 для двойного. Какую реализацию вы используете, и вы уверены, что не используете digits10 вместо max_digits10? Не верьте мне, прочтите 18.3.2.4-13: Количество цифр с основанием 10, необходимое для обеспечения того, чтобы различающиеся значения всегда различались. - person PlasmaHH; 15.03.2013
comment
@PlasmaHH: G++ 4.6.1, digits10: 15, max_digits10: 17. Извините, я не заметил, что вы написали max_digits10. Я знал только о цифрах 10. Похоже, это еще одна вещь C++11, о которой я раньше не знал. Спасибо, что указали на это! - person sellibitze; 15.03.2013

Если преобразование float-to-text и text-to-float в вашей стандартной библиотеке выполняет правильное округление, вам просто нужно достаточно значащих цифр, чтобы цикл float->text->float был без потерь, если у вас также нет Infs и NaN, тем не менее он должен быть «сохраняющим значение», не обязательно сохраняющим битовый шаблон, поскольку, я думаю, существует несколько представлений для бесконечности или NaN. Для IEEE-754 64-битных двойных 17 значащих цифр как раз достаточно, чтобы сделать передачу туда и обратно без потерь по сравнению с фактическим значением.

person sellibitze    schedule 15.03.2013
comment
Если я не совсем ошибаюсь, здесь не происходит округления. Спецификатор %g выбирает кратчайшее строковое представление, точно представляющее число. Я думаю, что источник проблемы в том, что битовая комбинация числа, записанного в файл, отличается от битовой комбинации числа после того, как оно было проанализировано из файла. - person Kristian Duske; 15.03.2013
comment
@KristianDuske: Что вы подразумеваете под точным представлением числа? Если вы хотите точно выразить значение числа с плавающей запятой в десятичном виде, вам нужно намного больше цифр, чем количество цифр, которое вы получите. Однако он может быть достаточно точным. Но похоже дело даже не в этом. Попробуйте увеличить количество значащих цифр, чтобы преобразование из числа с плавающей запятой в текст было инъективным. - person sellibitze; 15.03.2013
comment
@KristianDuske: Нет, %g работает иначе. Он печатает 6 значащих цифр по умолчанию. - person PlasmaHH; 15.03.2013
comment
@KristianDuske: Как этого достаточно?! - person sellibitze; 15.03.2013
comment
Я не знал, что %g приводит к округлению. Я думал, что это будет точно. Я укажу точность при написании поплавков вручную. - person Kristian Duske; 15.03.2013
comment
@KristianDuske: почти всегда происходит округление просто потому, что системы счисления разные. Например, попробуйте выразить 0,4 в двоичном формате. Попробуйте выразить 1+1/512 точно в десятичном виде... - person sellibitze; 15.03.2013

Ваш алгоритм CRC ошибочен для любого типа, который имеет несколько двоичных представлений для одного значения. IEEE 754 имеет два представления для 0,0, а именно +0,0 и -0,0. Другие, неконечные значения, такие как NaN, также могут вызвать проблемы.

person MSalters    schedule 15.03.2013
comment
Это было то, что я подозревал, поэтому я думаю, что лучше всего вместо этого будет вычислять контрольную сумму строковых представлений чисел. - person Kristian Duske; 15.03.2013

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

person Christopher Oicles    schedule 15.03.2013
comment
Спасибо за Ваш ответ. Я не думаю, что это необходимо, если я указываю точность вручную (мне нужно, чтобы строки были точными, но я не знал, что %g выполняет округление). Я думаю, что мне будет лучше, если вместо этого запустить CRC32 для строковых представлений. - person Kristian Duske; 15.03.2013