Сдвиг бит для арифметики с фиксированной запятой для чисел с плавающей запятой в C

Я написал следующий тестовый код для проверки арифметики с фиксированной запятой и сдвига битов.

void main(){
    float x = 2;
    float y = 3;
    float z = 1;
    unsigned int * px = (unsigned int *) (& x);
    unsigned int * py = (unsigned int *) (& y);
    unsigned int * pz = (unsigned int *) (& z);
    *px <<= 1;
    *py <<= 1;
    *pz <<= 1;
    *pz =*px + *py;
    *px >>= 1;
    *py >>= 1;
    *pz >>= 1;
    printf("%f %f %f\n",x,y,z);
  }

Результат: 2.000000 3.000000 0.000000

Почему последняя цифра 0? Я ожидал увидеть 5.000000. Я хочу использовать какую-то арифметику с фиксированной запятой, чтобы обойти использование чисел с плавающей запятой в приложении для обработки изображений. Какой самый лучший / самый простой / самый эффективный способ превратить мои массивы с плавающей запятой в целые числа? Является ли вышеупомянутый "обман компилятора" надежным обходным путем? Какие-либо предложения?


person user1410966    schedule 30.05.2012    source источник
comment
Когда вы сдвигали * px и другие на один бит, вы стирали только знаковый бит, но не показатель степени. Существует битовый формат IEEE float   -  person osgx    schedule 30.05.2012


Ответы (4)


Если вы хотите использовать фиксированную точку, не используйте тип «float» или «double», потому что они имеют внутреннюю структуру. Поплавки и двойники имеют специальный бит для знака; некоторые биты для экспоненты, некоторые для мантиссы (см. цветное изображение здесь) ; поэтому они по своей сути являются числами с плавающей запятой.

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

Существует описание расширений с плавающей запятой, реализованных в GCC: http://gcc.gnu.org/onlinedocs/gcc/Fixed_002dPoint.html

Существует некоторая ручная реализация фиксированной точки для C на основе MACRO: http://www.eetimes.com/discussion/other/4024639/Fixed-point-math-in-C

person osgx    schedule 30.05.2012
comment
К сожалению, это не выбор. У меня есть приложение, которое работает на процессоре ARM с использованием чисел с плавающей запятой, и мне нужно отправлять данные на DSP для обработки. DSP не имеет блока с плавающей запятой, поэтому перед отправкой данных я должен преобразовать их в фиксированную точку. Расширения с плавающей запятой не переносятся. - person user1410966; 30.05.2012
comment
@ user1410966, вы можете выполнять вычисления с плавающей запятой на ARM, но перед отправкой данных в DSP вы должны вручную преобразовать плавающую точку в фиксированную. Какой фиксированный формат можно использовать на DSP? - person osgx; 30.05.2012
comment
Есть вариант конвертации: ссылка цитата: double f = 1.2345; int n; n=(int)(f*65536); (если нужен формат с фиксированной точкой 16:16). - person osgx; 30.05.2012

То, что вы делаете, жестоко обращается с числами.

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

x = 2.0 = 1 * 2^1   : sign = 0, mantissa = 1,   exponent = 1 -> 0 10000000 00000000000000000000000 = 0x40000000
y = 3.0 = 1.5 * 2^1 : sign = 0, mantissa = 1.5, exponent = 1 -> 0 10000000 10000000000000000000000 = 0x40400000
z = 1.0 = 1 * 2^0   : sign = 0, mantissa = 1,   exponent = 0 -> 0 01111111 00000000000000000000000 = 0x3F800000

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

В твоем случае:

  • ваш 2.0 становится 0x80000000, что приводит к -0.0,
  • ваш 3.0 становится 0x80800000, что приводит к -1.1754943508222875e-38,
  • ваш 1.0 становится 0x7F000000, в результате чего 1.7014118346046923e + 38.

Последнее вы потеряете, добавив -0.0 и -1.1754943508222875e-38, что станет последним, а именно 0x80800000, который должен быть после >> увеличения на 1 снова на 3,0. Не знаю, почему это не так, наверное, потому, что я здесь ошибся.

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

Я бы подумал о том, чтобы преобразовать их в целое число или другую фиксированную точку на ARM и отправить их по линии, как они есть.

person glglgl    schedule 30.05.2012
comment
Идеально! Это именно та информация, с которой мне нужно было начать. Какой самый безопасный способ превратить их в целые числа с меньшей точностью? - person user1410966; 30.05.2012
comment
Возьмите наибольшее число, которое вы можете придумать (например, 10), и перейдите к следующей по величине степени двойки (16). Затем выберите целочисленный тип (например, uint16) и сделайте значение 16 эквивалентным 65536 в целочисленном пространстве, применив коэффициент 4096. Таким образом, для 1 вы получите 4096, для 0,25 вы получите 1024 и для любого другого значения с неопределенным точности вы получите нечетное число. Имейте в виду, что, например, .1 очень странно, когда представлено как число с плавающей запятой, поэтому он также даст вам нечетное число в целочисленном пространстве. - person glglgl; 30.05.2012

Вероятно, ваш компилятор использует формат IEEE 754 для floats, который в битовых терминах выглядит так:

SEEEEEEEEFFFFFFFFFFFFFFFFFFFFFFF
^ bit 31                       ^ bit 0

S - это знаковый бит. S = 1 означает, что число отрицательное.

E бит - это показатель степени. Имеется 8 битов экспоненты, дающих диапазон от 0 до 255 , но показатель степени смещен - вам нужно вычесть 127, чтобы получить истинный показатель степени.

F бит - это дробная часть, однако вам нужно представить невидимую единицу спереди, чтобы дробь всегда была 1. что-то, а все, что вы видите, - это цифры двоичной дроби.

Число 2 равно 1 x 2 1 = 1 x 2 128–127, поэтому кодируется как

01000000000000000000000000000000

Итак, если вы используете небольшой сдвиг, чтобы сдвинуть его вправо, вы получите

10000000000000000000000000000000

который по соглашению равен -0 в IEEE754, поэтому вместо умножения вашего числа на 2 ваш сдвиг сделал его нулевым.

Число 3 равно [1 + 0,5] x 2 128–127.

который представлен как

01000000010000000000000000000000

Сдвиг влево дает вам

10000000100000000000000000000000

что составляет -1 x 2 -126 или какое-то очень маленькое число.

Вы можете сделать то же самое для z, но вы, вероятно, поняли, что сдвиг просто портит числа с плавающей запятой.

person JeremyP    schedule 30.05.2012

Фиксированная точка не работает. Вы хотите сделать что-то вроде этого:

void main(){
    // initing 8bit fixed point numbers
    unsigned int x = 2 << 8;
    unsigned int y = 3 << 8;
    unsigned int z = 1 << 8;

    // adding two numbers
    unsigned int a = x + y;

    // multiplying two numbers with fixed point adjustment
    unsigned int b = (x * y) >> 8;

    // use numbers
    printf("%d %d\n", a >> 8, b >> 8);
  }
person Tobias Schlegel    schedule 30.05.2012
comment
Правильный. Для целых чисел. Но моя проблема в том, как это сделать с помощью поплавков. - person user1410966; 30.05.2012
comment
Вы не можете выполнять математику с фиксированной запятой с данными в представлении с плавающей запятой. Вы можете преобразовать свои числа с плавающей запятой в целые числа с фиксированной запятой и сделать то, что я написал выше, или вы можете эмулировать арифметику с плавающей запятой, но это, вероятно, слишком сложно. Однако ваш исходный код не обрабатывает данные с плавающей запятой каким-либо разумным образом. - person Tobias Schlegel; 30.05.2012
comment
@ user1410966: Вам следует прочитать больше, чтобы лучше понимать рациональные числа с фиксированной, плавающей запятой и двоичные рациональные числа. У каждого типа с фиксированной точкой есть фиксированная позиция для десятичной точки, просто переместите мантиссу поплавка в правильную позицию, чтобы целочисленная и десятичная части имели правильное значение. - person phuclv; 25.09.2013