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

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

Рассмотрим следующий код, который представляет собой 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 заканчивается отрицательным после кругового движения. Хорошо ли это поведение? Я бы ожидал, что int-to-float round-trip, по крайней мере, сохранит знак правильно...

Hm, на ideone, результат отличается:

2147483583 -> 2147483520
2147483584 -> 2147483647

Была ли команда g++ исправлена ​​ошибка в то же время, или оба выхода идеально подходят?

4b9b3361

Ответ 1

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

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

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

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

Ответ 2

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

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

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

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

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

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

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

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

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

Пока все хорошо.

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

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

  • 00 - до ближайшего четного - округленный результат близок к бесконечно точному результату. Если два значения одинаково близки, результатом является четное значение (т.е. один с наименьшим значащим разрядом нуля). По умолчанию
  • 01 - в направлении -Inf
  • 10 - в сторону + inf
  • 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 to float conversion check, наиболее очевидный способ преобразования 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

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

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

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

Это достаточно хорошо? Hey 67108872 прекрасно представим 32-битным поплавком и должен быть намного лучше, чем 67108864 вправо? CORRECT, и здесь вы можете поговорить об округлении при преобразовании int в 32-битный float.

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

67108871 = 00000100_00000000_00000000_00000111

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

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

Процедура "округления до ближайшего четного" включает в себя поиск двух чисел, которые вводят входное значение скобки 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 (округление до ближайшего четного режима).

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

 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

Последний случай OP показывает проблему, в которой имеет место слово даже.

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

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

top: 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]
}

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

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