изменение знака при переходе от int к float и обратно

Рассмотрим следующий код, который является SSCCE моей актуальной проблемы:

#include <iostream>

int roundtrip(int x)
{
return int(float(x));
}

int main()
{
int a = 2147483583;
int b = 2147483584;
std::cout << a << " -> " << roundtrip(a) << '\n';
std::cout << b << " -> " << roundtrip(b) << '\n';
}

Вывод на моем компьютере (Xubuntu 12.04.3 LTS):

2147483583 -> 2147483520
2147483584 -> -2147483648

Обратите внимание, как положительное число b заканчивается отрицательным после туда и обратно. Это поведение хорошо определено? Я бы ожидал, что круговое отключение типа «с плавающей точкой» хотя бы правильно сохранит знак…

Хм, на идеоне, выход отличается:

2147483583 -> 2147483520
2147483584 -> 2147483647

Тем временем команда g ++ исправила ошибку, или оба результата совершенно верны?

42

Решение

Ваша программа вызывает неопределенное поведение из-за переполнения при преобразовании с плавающей точкой в ​​целое число. То, что вы видите, является лишь обычным симптомом на процессорах x86.

float значение, ближайшее к 2147483584 это 231 точно (преобразование из целого числа в число с плавающей запятой обычно округляется до ближайшего, которое может быть повышено и в этом случае повышается. Чтобы быть точным, поведение при преобразовании целого числа в число с плавающей запятой определяется реализацией, большинство реализаций определяют округление как «в соответствии с режимом округления FPU», а режим округления по умолчанию для FPU — округление до ближайшего).

Затем при конвертации из числа с плавающей точкой, представляющего 231 в intпроисходит переполнение. Это переполнение является неопределенным поведением. Некоторые процессоры возбуждают исключение, другие насыщают. Инструкция IA-32 cvttsd2si обычно генерируемые компиляторами всегда возвращаются INT_MIN в случае переполнения, независимо от того, является ли число с плавающей точкой положительным или отрицательным.

Вы не должны полагаться на это поведение, даже если вы знаете, что нацелены на процессор Intel: при нацеливании на x86-64 компиляторы могут генерировать для преобразования с плавающей точкой в ​​целое число, Последовательности инструкций, которые используют неопределенное поведение для возврата результатов, отличных от ожидаемых целочисленным типом назначения..

69

Другие решения

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

В 32 битах с плавающей точкой (IEEE 754) вы можете хранить все целые числа изнутри [-224…224] спектр. Целые числа вне диапазона могут также иметь точное представление как число с плавающей точкой, но не все из них имеют. Проблема в том, что вы можете иметь только 24 значащих бита для игры в плавающем.

Вот как обычно выглядит преобразование из int-> float на низком уровне:

fild dword ptr[your int]
fstp dword ptr[your float]

Это всего лишь последовательность из двух инструкций сопроцессора. Сначала загружает 32-битное int в стек компроцессора и преобразует его в число с плавающей запятой шириной 80 бит.

Руководство разработчика программного обеспечения для архитектуры Intel® 64 и IA-32

(ПРОГРАММИРОВАНИЕ С X87 FPU):

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

Поскольку регистры FPU имеют число с плавающей запятой шириной 80 бит, проблем с fild здесь, как 32-битный int идеально подходит для 64-битного значения и формата с плавающей запятой.

Все идет нормально.

Вторая часть — fstp это немного сложно и может быть удивительно. Предполагается хранить 80-битную плавающую точку в 32-битной памяти. Хотя все дело в целочисленных значениях (в вопросе), сопроцессор может фактически выполнять «округление». Ke? Как округлить целочисленное значение, даже если оно хранится в формате с плавающей запятой? ;-).

Я объясню это вкратце — давайте сначала посмотрим, что обеспечивает режим округления x87 (это воплощение режимов округления IEE 754). X87 fpu имеет 4 режима округления, управляемых битами № 10 и № 11 управляющего слова fpu:

  • 00 — до ближайшего четного — округленный результат является наиболее близким к бесконечно точному результату. Если два
    значения одинаково близки, результатом является четное значение (то есть,
    один с младшим значащим битом ноль). По умолчанию
  • 01 — в направлении -Inf
  • 10 — в сторону + инф
  • 11 — к 0 (т.е. усеченный)

Вы можете играть с режимами округления, используя этот простой код (хотя это может быть сделано по-другому — показывая низкий уровень здесь):

enum ROUNDING_MODE
{
RM_TO_NEAREST  = 0x00,
RM_TOWARD_MINF = 0x01,
RM_TOWARD_PINF = 0x02,
RM_TOWARD_ZERO = 0x03 // TRUNCATE
};

void set_round_mode(enum ROUNDING_MODE rm)
{
short csw;
short tmp = rm;

_asm
{
push ax
fstcw [csw]
mov ax, [csw]
and ax, ~(3<<10)
shl [tmp], 10
or ax, tmp
mov [csw], ax
fldcw [csw]
pop ax
}
}

Хорошо, хорошо, но все же, как это связано с целочисленными значениями? Терпение … чтобы понять, почему вам могут понадобиться режимы округления, связанные с преобразованием int в float, проверьте наиболее очевидный способ преобразования int в float — усечение (не по умолчанию) — это может выглядеть так:

  • запись знак
  • отрицать ваш int, если меньше нуля
  • найти положение крайнего левого 1
  • сдвиньте int вправо / влево так, чтобы 1, найденный выше, был помещен в бит № 23
  • запишите число смен в процессе, чтобы вы могли рассчитать показатель

И код, имитирующий это поведение, может выглядеть так:

float int2float(int value)
{
// handles all values from [-2^24...2^24]
// outside this range only some integers may be represented exactly
// this method will use truncation 'rounding mode' during conversion

// we can safely reinterpret it as 0.0
if (value == 0) return 0.0;

if (value == (1U<<31)) // ie -2^31
{
// -(-2^31) = -2^31 so we'll not be able to handle it below - use const
value = 0xCF000000;
return *((float*)&value);
}

int sign = 0;

// handle negative values
if (value < 0)
{
sign = 1U << 31;
value = -value;
}

// although right shift of signed is undefined - all compilers (that I know) do
// arithmetic shift (copies sign into MSB) is what I prefer here
// hence using unsigned abs_value_copy for shift
unsigned int abs_value_copy = value;

// find leading one
int bit_num = 31;
int shift_count = 0;

for(; bit_num > 0; bit_num--)
{
if (abs_value_copy & (1U<<bit_num))
{
if (bit_num >= 23)
{
// need to shift right
shift_count = bit_num - 23;
abs_value_copy >>= shift_count;
}
else
{
// need to shift left
shift_count = 23 - bit_num;
abs_value_copy <<= shift_count;
}
break;
}
}

// exponent is biased by 127
int exp = bit_num + 127;

// clear leading 1 (bit #23) (it will implicitly be there but not stored)
int coeff = abs_value_copy & ~(1<<23);

// move exp to the right place
exp <<= 23;

int ret = sign | exp | coeff;

return *((float*)&ret);
}

Теперь пример — режим усечения конвертирует 2147483583 в 2147483520,

2147483583 = 01111111_11111111_11111111_10111111

Во время преобразования int-> float вы должны сдвинуть крайний левый 1 в бит # 23. Теперь ведущий 1 находится в бите # 30. Чтобы поместить его в бит № 23, необходимо выполнить сдвиг вправо на 7 позиций. При этом вы теряете (они не поместятся в 32-битном формате с плавающей запятой) на 7 бит lsb справа (вы усекаете / рубите). Они были:

01111111 = 63

И 63 — это то, что потерял оригинальный номер:

2147483583 -> 2147483520 + 63

Усечение легко, но не обязательно то, что вы хотите и / или лучше всего для всех случаев. Рассмотрим пример ниже:

67108871 = 00000100_00000000_00000000_00000111

Выше значение не может быть точно представлено с плавающей точкой, но проверьте, что с ним делает усечение. Как и раньше — нам нужно сдвинуть крайний левый 1 в бит № 23. Для этого необходимо, чтобы значение было смещено вправо ровно на 3 позиции, теряя 3 бита LSB (на данный момент я буду писать цифры по-разному, показывая, где находится неявный 24-й бит с плавающей запятой, и будет заключать в скобки явные 23 бита значения):

00000001.[0000000_00000000_00000000] 111 * 2^26 (3 bits shifted out)

Усечение прерывает 3 конечных бита, оставляя нас с 67108864 (67108864 + 7 (3 расколотых бита)) = 67108871 (помните, что, хотя мы сдвигаемся, мы компенсируем манипулирование экспонентой — здесь опущено).

Это достаточно хорошо? Привет 67108872 отлично представлен 32-битным float и должен быть намного лучше, чем 67108864 право? ПРАВИЛЬНО, и здесь вы можете поговорить о округлении при преобразовании int в 32-битное число с плавающей точкой.

Теперь давайте посмотрим, как работает режим «округление до ближайшего четного» по умолчанию и каковы его последствия в случае OP. Рассмотрим тот же пример еще раз.

67108871 = 00000100_00000000_00000000_00000111

Как мы знаем, нам нужно 3 сдвига вправо, чтобы поместить самый левый 1 в бит № 23:

00000000_1.[0000000_00000000_00000000] 111 * 2^26 (3 bits shifted out)

Процедура округления до ближайшего четного включает в себя поиск 2 чисел, которые заключают в скобки входное значение 67108871 снизу и сверху как можно ближе. Имейте в виду, что мы все еще работаем в FPU на 80 битах, поэтому, хотя я показываю, что некоторые биты сдвинуты, они все еще находятся в состоянии FPU, но будут удалены во время операции округления при сохранении выходного значения.

00000000_1.[0000000_00000000_00000000] 111 * 2^26 (3 bits shifted out)

2 значения, которые тесно связаны 00000000_1.[0000000_00000000_00000000] 111 * 2^26 являются:

сверху:

  00000000_1.[0000000_00000000_00000000] 111 * 2^26
+1
= 00000000_1.[0000000_00000000_00000001] * 2^26 = 67108872

и снизу:

  00000000_1.[0000000_00000000_00000000] * 2^26 = 67108864

очевидно 67108872 гораздо ближе к 67108871 чем 67108864 следовательно преобразование из 32-битного значения int 67108871 дает 67108872 (в режиме округления до ближайшего четного).

Теперь номера ОП (все еще округляются до ближайшего четного):

 2147483583 = 01111111_11111111_11111111_10111111
= 00000000_1.[1111111_11111111_11111111] 0111111 * 2^30

значения в скобках:

Топ:

  00000000_1.[1111111_111111111_11111111] 0111111 * 2^30
+1
= 00000000_10.[0000000_00000000_00000000] * 2^30
=  00000000_1.[0000000_00000000_00000000] * 2^31 = 2147483648

низ:

00000000_1.[1111111_111111111_11111111] * 2^30 = 2147483520

Имейте в виду, что четное Слово в «округлении до ближайшего четного» имеет значение только тогда, когда входное значение находится посередине между значениями в скобках. Только тогда слово четное имеет значение и «решает», какое значение скобки следует выбрать. В приведенном выше случае четное не имеет значения, и мы должны просто выбрать ближе значение, которое 2147483520

Случай последнего ОП показывает проблему где четное Слово имеет значение. :

 2147483584 = 01111111_11111111_11111111_11000000
= 00000000_1.[1111111_11111111_11111111] 1000000 * 2^30

значения в скобках такие же, как и ранее:

Топ: 00000000_1.[0000000_00000000_00000000] * 2^31 = 2147483648

низ: 00000000_1.[1111111_111111111_11111111] * 2^30 = 2147483520

Нет более близкого значения (2147483648-2147483584 = 64 = 2147483584-2147483520), поэтому мы должны полагаться на четное и выберите верхнее (четное) значение 2147483648,

И здесь проблема ОП в том, что Паскаль кратко описал. FPU работает только на подписанных значениях и 2147483648 не может быть сохранен как int со знаком, так как его максимальное значение 2147483647, следовательно, проблемы.

Простое доказательство (без кавычек документации), что FPU работает только со значениями со знаком, т.е. обрабатывает каждое значение как подписанное путем отладки этого:

unsigned int test = (1u << 31);

_asm
{
fild [test]
}

Хотя похоже, что тестовое значение следует рассматривать как неподписанное, оно будет загружено как -231 поскольку нет отдельной инструкции по загрузке значений со знаком и без знака в FPU. Точно так же вы не найдете инструкций, которые позволят вам сохранить значение без знака из FPU в mem. Все это всего лишь небольшой шаблон, который рассматривается как подписанный, независимо от того, как вы объявили его в своей программе.

Было долго, но надеюсь, что кто-то узнает что-то из этого.

10

По вопросам рекламы [email protected]