Подтвердить что ты не робот

Безопасное наказание char *, чтобы удвоить C

В Open Source программа I написал, я читаю двоичные данные (написанные другой программой) из файла и выводя int, double, и другие типы данных. Одна из проблем заключается в том, что она должна работайте на 32-битных и 64-битных машинах обеих степеней, что означает, что я в конечном итоге приходится делать немного бит-бит. Я знаю (очень) немного о типе punning и строгом aliasing и хочу удостовериться, что я делая все правильно.

В принципе, легко конвертировать из char * в int различных размеров:

int64_t snativeint64_t(const char *buf) 
{
    /* Interpret the first 8 bytes of buf as a 64-bit int */
    return *(int64_t *) buf;
}

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

int64_t swappedint64_t(const int64_t wrongend)
{
    /* Change the endianness of a 64-bit integer */
    return (((wrongend & 0xff00000000000000LL) >> 56) |
            ((wrongend & 0x00ff000000000000LL) >> 40) |
            ((wrongend & 0x0000ff0000000000LL) >> 24) |
            ((wrongend & 0x000000ff00000000LL) >> 8)  |
            ((wrongend & 0x00000000ff000000LL) << 8)  |
            ((wrongend & 0x0000000000ff0000LL) << 24) |
            ((wrongend & 0x000000000000ff00LL) << 40) |
            ((wrongend & 0x00000000000000ffLL) << 56));
}

Во время выполнения программа обнаруживает конечность машины и назначает один из указанных выше указатель функции:

int64_t (*slittleint64_t)(const char *);
if(littleendian) {
    slittleint64_t = snativeint64_t;
} else {
    slittleint64_t = sswappedint64_t;
}

Теперь сложная часть возникает, когда я пытаюсь использовать char * для двойника. Я бы подобно повторному использованию кода замены под заголовком:

union 
{
    double  d;
    int64_t i;
} int64todouble;

int64todouble.i = slittleint64_t(bufoffset);
printf("%lf", int64todouble.d);

Однако некоторые компиляторы могли оптимизировать задание "int64todouble.i" и сломать программу. Существует ли более безопасный способ сделать это, учитывая что эта программа должна оставаться оптимизированной для производительности, а также что я предпочитают не писать параллельный набор преобразований для литья char * в двойной напрямую? Если профсоюзный метод наказания безопасен, должен ли я быть перезаписывать мои функции, такие как snativeint64_t, чтобы использовать его?


В итоге я использовал ответ Стив Джессоп, потому что функции преобразования переписаны для использования memcpy, например:

int64_t snativeint64_t(const char *buf) 
{
    /* Interpret the first 8 bytes of buf as a 64-bit int */
    int64_t output;
    memcpy(&output, buf, 8);
    return output;
}

скомпилирован в тот же самый ассемблер, что и исходный код:

snativeint64_t:
        movq    (%rdi), %rax
        ret

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

Адам, ваш ответ тоже был замечательным, и я многому научился у него. Спасибо за публикацию!

4b9b3361

Ответ 1

Поскольку вы, похоже, достаточно знаете о своей реализации, чтобы убедиться, что int64_t и double имеют одинаковый размер и имеют соответствующие представления хранилища, вы можете столкнуться с memcpy. Тогда вам даже не нужно думать об псевдониме.

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

int64_t i = slittleint64_t(buffoffset);
double d;
memcpy(&d,&i,8); /* might emit no code if you're lucky */
printf("%lf", d);

Изучите полученный код или просто выполните его. Шансы даже в худшем случае не будут медленными.

В общем, однако, делать что-то слишком умное с использованием batwapping приводит к проблемам с переносимостью. Существуют ABI с двойниками среднего конца, где каждое слово малозначительное, но первое слово на первом месте.

Обычно вы можете рассмотреть возможность сохранения ваших удвоений с помощью sprintf и sscanf, но для вашего проекта форматы файлов не находятся под вашим контролем. Но если ваше приложение просто сгребает IEEE удвоения из входного файла в одном формате в выходной файл в другом формате (не уверен, что это так, так как я не знаю форматы данных, о которых идет речь, но если это так), то, возможно, вы может забыть о том, что он двойной, поскольку вы все равно не используете его для арифметики. Просто рассматривайте его как непрозрачный char [8], требующий байтов, только если форматы файлов отличаются.

Ответ 2

Я настоятельно рекомендую вам прочитать Понимание строго алиасинга. В частности, см. Разделы "Кастинг через объединение". Он имеет ряд очень хороших примеров. Хотя статья находится на веб-сайте о процессоре Cell и использует примеры сборки PPC, почти все они одинаково применимы к другим архитектурам, включая x86.

Ответ 3

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

Макросы обычно представляют собой плохую идею, но это может быть исключение из правила. Должно быть возможно получить шаблонное поведение в C, используя набор макросов, используя типы ввода и вывода в качестве параметров.

Ответ 4

В качестве очень небольшого предложения, я предлагаю вам изучить, можете ли вы поменять маскировку и сдвиг в 64-битном случае. Поскольку операция заключается в замене байтов, вы всегда сможете уйти с маской только 0xff. Это должно привести к более быстрому и компактному коду, если только компилятор достаточно умен, чтобы понять, что он сам.

Вкратце, изменив это:

(((wrongend & 0xff00000000000000LL) >> 56)

в это:

((wrongend >> 56) & 0xff)

должен генерировать тот же результат.

Ответ 5

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

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


Что касается double/float, просто сохраните их в ints путем кадрирования памяти:

double d = 3.1234;
printf("Double %f\n", d);
int64_t i = *(int64_t *)&d;
// Now i contains the double value as int
double d2 = *(double *)&i;
printf("Double2 %f\n", d2);

Оберните его в функцию

int64_t doubleToInt64(double d)
{
    return *(int64_t *)&d;
}

double int64ToDouble(int64_t i)
{
    return *(double *)&i;
}

Участник предоставил эту ссылку:

http://cocoawithlove.com/2008/04/using-pointers-to-recast-in-c-is-bad.html

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

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

Это не рискованно, и это тоже неплохая практика. Это может привести к ошибкам, если вы сделаете это неправильно, так же как программирование на C может вызвать ошибки, если вы делаете это неправильно, так же как и любое программирование на любом языке. По этому аргументу вы должны вообще прекратить программирование.

Тип punning - Форма указателя сглаживание, где два указателя и ссылка в том же месте в памяти, но представляют это местоположение как разные типы. Компилятор будет обрабатывать оба "каламбуры" как несвязанные указатели. Тип пуннинг может вызвать проблемы зависимости для любых данных доступ через оба указателя.

Это правда, но, к сожалению, совершенно не связано с моим кодом.

То, что он называет, это код:

int64_t * intPointer;
:
// Init intPointer somehow
:
double * doublePointer = (double *)intPointer;

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

Мой код копируется значением, а не ссылкой. Я накладываю двойную на указатель int64 (или наоборот) и сразу на почтение. Как только функции возвращаются, указатель не удерживается ни на что. Существует int64 и double, и они полностью не связаны с входным параметром функций. Я никогда не копирую указатель на указатель другого типа (если вы видели это в своем примере кода, вы сильно неправильно читали код C, который я написал), я просто передаю значение переменной различного типа (в собственной ячейке памяти), Таким образом, определение типа punning не применяется вообще, так как оно говорит "ссылаться на одно и то же место в памяти", и здесь нет ничего общего с тем же местом памяти.

int64_t intValue = 12345;
double doubleValue = int64ToDouble(intValue);
// The statement below will not change the value of doubleValue!
// Both are not pointing to the same memory location, both have their
// own storage space on stack and are totally unreleated.
intValue = 5678;

Мой код - это не что иное, как копия памяти, просто написанная на C без внешней функции.

int64_t doubleToInt64(double d)
{
    return *(int64_t *)&d;
}

Может быть записано как

int64_t doubleToInt64(double d)
{
    int64_t result;
    memcpy(&result, &d, sizeof(d));
    return result;
}

Это не более того, поэтому нет никого, кто карает даже в поле зрения. И эта операция также полностью безопасна, так же безопасна, как операция может быть в C. Двойной определяется как всегда 64 бит (в отличие от int он не меняется по размеру, он фиксируется на 64 бит), следовательно, он всегда будет соответствовать в переменную размера int64_t.