Безопасно пробивая символ *, чтобы удвоить в C - PullRequest
11 голосов
/ 21 октября 2008

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

В основном, легко преобразовать из 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;
}

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

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 более явно выражает то, что я пытаюсь сделать, и должна работать даже на самых наивных компиляторах.

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

Ответы [ 5 ]

12 голосов
/ 21 октября 2008

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

2 голосов
/ 21 октября 2008

Поскольку вы, кажется, знаете достаточно о своей реализации, чтобы быть уверенными, что 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);

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

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

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

2 голосов
/ 21 октября 2008

Стандарт гласит, что запись в одно поле объединения и немедленное чтение из него - это неопределенное поведение. Так что, если вы пойдете по книге правил, метод на основе объединения не будет работать.

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

0 голосов
/ 21 октября 2008

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

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


Что касается double / float, просто сохраните их в целых числах путем приведения в память:

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 может вызвать ошибки, если вы делаете это неправильно, как и любое программирование на любом языке. По этому аргументу вы должны полностью прекратить программирование.

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

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

На что он ссылается, так это код:

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. Определяется, что double всегда должен быть 64-битным (в отличие от int, он не изменяется по размеру, он фиксирован на 64-битном), следовательно, он всегда будет соответствовать в переменную размера int64_t.

0 голосов
/ 21 октября 2008

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

Короче говоря, изменив это:

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

в это:

((wrongend >> 56) & 0xff)

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

...