Арифметика с фиксированной точкой в программировании на языке C

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

3 ответов


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

обычным и наиболее эффективным способом сделать это в C является использование операторов сдвига битов (>). Shifting bits-довольно простой и быстрый операция для ALU и выполнение этого имеют свойство умножать ( > ) целочисленное значение на 2 на каждый сдвиг (кроме того, многие сдвиги могут быть сделаны по точно такой же цене одного). Конечно, недостатком является то, что множитель должен быть в степени 2 (что обычно не является проблемой само по себе, поскольку нас не волнует это точное значение множителя).

теперь предположим, что мы хотим использовать 32-битные целые числа для хранения наших значений. Мы должны выбрать силу 2 множитель. Давайте разделим торт на два, скажем, 65536 (это самый распространенный случай, но вы действительно можете использовать любую мощность 2 в зависимости от ваших потребностей в точности). Это 216 и 16 здесь означает, что мы будем использовать 16 наименее значимых битов (LSB) для дробной части. Остальное (32-16 = 16) - для наиболее значимых битов (MSB), целочисленной части.

     integer (MSB)    fraction (LSB)
           v                 v
    0000000000000000.0000000000000000

давайте поставим это в коде:

#define SHIFT_AMOUNT 16 // 2^16 = 65536
#define SHIFT_MASK ((1 << SHIFT_AMOUNT) - 1) // 65535 (all LSB set, all MSB clear)

int price = 500 << SHIFT_AMOUNT;

это значение, которое вы должны поместить в хранилище (структура, база данных, что угодно). Обратите внимание, что int не обязательно 32 бита в C, хотя в настоящее время это в основном так. Также без дальнейшего объявления он подписан по умолчанию. Вы можете добавить unsigned к объявлению, чтобы быть уверенным. Более того, вы можете использовать uint32_t или uint_least32_t (объявленный в stdint.h) если ваш код сильно зависит от размера целого бита (вы можете ввести некоторые хаки об этом). Сомневаясь, используйте typedef для вашего типа с фиксированной точкой, и вы будете в безопасности.

когда вы хотите сделать исчисление по этому значению, вы можете использовать 4 основных оператора:+, -, * и /. Вы должны иметь в виду, что при добавлении и вычитании значения (+ и -) это значение также должно быть сдвинуто. Предположим, мы хотим добавить 10 к нашей цене 500:

price += 10 << SHIFT_AMOUNT;

но для умножения и деления (*и/) множитель/делитель не должен смещаться. Допустим, мы хотим умножить на 3:

price *= 3;

теперь давайте сделаем вещи более интересными, разделив цену на 4 так мы компенсируем ненулевую дробную часть:

price /= 4; // now our price is ((500 + 10) * 3) / 4 = 382.5

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

printf("price integer is %d\n", price >> SHIFT_AMOUNT);

Если вам нужна дробная часть, вы должны замаскировать его:

printf ("price fraction is %d\n", price & SHIFT_MASK);

конечно, это не то, что мы можем назвать десятичную дробь, на самом деле это целое число в диапазоне [0 - 65535]. Но он точно сопоставляется с десятичным диапазоном дробей [0-0.9999...]. Другими словами, отображение похоже: 0 => 0, 32768 => 0.5, 65535 => 0.9999...

простой способ увидеть его как десятичную дробь-прибегнуть к встроенной арифметике с поплавком в этот момент:

printf("price fraction in decimal is %f\n", ((double)(price & SHIFT_MASK) / (1 << SHIFT_AMOUNT)));

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

printf("price is roughly %d.%lld\n", price >> SHIFT_AMOUNT, (long long)(price & SHIFT_MASK) * 100000 / (1 << SHIFT_AMOUNT));

число 0 в выражении-это примерно количество цифр, которое вы хотите после десятичной точки. Не переоценивайте число 0 ваша точность дроби (здесь нет реальной ловушки, это совершенно очевидно). Не используйте simple long, поскольку sizeof(long) может быть равен sizeof (int). Использовать долго в случае, если int 32 бит как долго гарантируется как минимум 64 бита (или используйте int64_t, int_least64_t и такие, объявленные в stdint.ч.) Другими словами, используйте тип в два раза больше вашего типа с фиксированной точкой, это достаточно справедливо. Наконец, если у вас нет доступа к типам > = 64 бит, возможно, пришло время для упражнений подражая им, по крайней мере, для выходного.

это основные идеи фиксированной арифметики.

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

также обратите внимание, что если 32-битное целое число не может представлять значения больше 232 - 1, используя арифметику с фиксированной точкой с 216 ограничивает диапазон 216 - 1! (и разделите все это на 2 со знаковыми целыми числами, которые в нашем примере оставят нам доступный диапазон 215 - 1). Цель состоит в том, чтобы выбрать SHIFT_AMOUNT подходящий к ситуации. Это компромисс между величиной целочисленной части и точностью дробной части.

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

если вы работаете в этих разумных областях, вам лучше использовать библиотеки, посвященные произвольной точности (посмотрите на gmplib, это бесплатно). В вычислительной науке, по существу, получение точности-это количество битов, используемых для хранения значений. Вы хотите высокую точность? Используйте биты. Вот и все.


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

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

теперь с 32-битными целыми числами, умножение на 10000 оставляет мало места для больших количество долларов. Практический 32-битный предел в 2 миллиарда означает, что могут быть выражены только цены до $20000: 2000000000 / 10000 = 20000. Это ухудшается, если вы умножите этот 20000 на что-то, так как может не быть места для хранения результата. По этой причине, я рекомендую использовать 64-битные целые числа (long long). Даже если вы умножите все цены на 10000, все равно остается много запаса для хранения больших значений, даже через умножения.

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

Если все это кажется сложным, это. Я думаю, что самый простой вариант - использовать двойные и купить больше ОЗУ, если вам это нужно. Они имеют 53 бита точности, что составляет примерно 9 квадриллионов, или почти 16 десятичных цифр. Да, вы все еще можете потерять Пенни, когда работаете с миллиардами, но если вы заботитесь об этом, вы не являетесь миллиардером правильным способом. :)


Я бы не рекомендовал вам сделать это, если ваша единственная цель-сохранить память. Ошибка в расчете цены может быть накоплена, и вы собираетесь испортить ее.

Если вы действительно хотите реализовать аналогичный материал, можете ли вы просто взять минимальный интервал цены, а затем напрямую использовать операцию int и integer для управления вашим номером? Вам только нужно преобразовать его к номеру с плавающей запятой когда дисплей, который делает вашу жизнь более легким.