web-dev-qa-db-ja.com

Cプログラミングの固定小数点演算

株価を高精度で保存するアプリケーションを作成しようとしています。現在、そのためにdoubleを使用しています。メモリを節約するために、他のデータ型を使用できますか?これは固定小数点演算と関係があることは知っていますが、わかりません。

23
AndroidDev93

固定小数点演算の背後にある考え方は、特定の量で乗算された値を保存し、すべての計算に乗算された値を使用し、結果が必要なときに同じ量で除算することです。この手法の目的は、整数演算(int、long ...)を使用しながら、分数を表すことです。

Cでこれを行う通常の最も効率的な方法は、ビットシフト演算子(<<および>>)を使用することです。ビットのシフトは、ALUの非常にシンプルで高速な操作であり、これを行うには、各シフトで整数値を2で乗算(<<)および除算(>>)するプロパティがあります(さらに、同じことに対して多くのシフトを実行できます)単一の価格)。もちろん、欠点は乗数が2の累乗でなければならないことです(正確な乗数の値はあまり気にしないので、通常はそれだけでは問題になりません)。

ここで、値の保存に32ビット整数を使用するとします。 2のべき乗の乗数を選択する必要があります。ケーキを2つに分けてみましょう。たとえば65536です(これが最も一般的なケースですが、正確に必要に応じて2の累乗を実際に使用できます)。これは2です16 ここで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はCでは必ずしも32ビットではないことに注意してください。また、それ以上の宣言がなければ、デフォルトで署名されます。確実に宣言に符号なしを追加できます。それよりも、コードが整数ビットサイズに大きく依存している場合は、uint32_tまたはuint_least32_t(stdint.hで宣言)を使用できます(ハックが発生する可能性があります)。疑わしい場合は、固定小数点型にtypedefを使用すると、安全です。

この値で計算を行いたい場合、4つの基本演算子、+、-、*、/を使用できます。値(+と-)を加算および減算するとき、その値もシフトする必要があることに注意してください。 500の価格に10を追加するとします。

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 ...

これを小数として見る簡単な方法は、この時点でCの組み込み浮動小数点演算に頼ることです。

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の数を過大評価しないでください(実際のトラップはありません。これは明らかです)。 sizeof(long)がsizeof(int)と等しくなる可能性があるため、simple longを使用しないでください。 long longが保証されているintが32ビットの場合、long longを使用する最小64ビット(または、stdint.hで宣言されているint64_t、int_least64_tなどを使用)。言い換えれば、固定小数点型の2倍のサイズの型を使用すれば十分です。最後に、64ビット以上の型にアクセスできない場合は、少なくとも出力については、エミュレートを実行する時間になるかもしれません。

これらは、固定小数点演算の背後にある基本的な考え方です。

負の値には注意してください。特に最終的な値を表示するときは、特に注意が必要です。さらに、Cは符号付き整数について実装定義されています(これが問題となっているプラ​​ットフォームは最近では非常にまれですが)。環境内で常に最小限のテストを行い、すべてが期待どおりに動作することを確認する必要があります。そうでない場合は、あなたが何をするか知っていれば、それをハックできます(これについては開発しませんが、これは算術シフトvs論理シフトと2の補数表現と関係があります)。ただし、符号なし整数を使用すると、動作はとにかく明確に定義されるため、何をするにしてもほとんど安全です。

32ビット整数が2より大きい値を表現できない場合も注意してください32 -1、2の固定小数点演算を使用16 範囲を2に制限します16 -1! (そして、このすべてを符号付き整数で2で除算します。この例では、使用可能な範囲2が残ります。15 -1)。目標は、状況に適したSHIFT_AMOUNTを選択することです。これは、整数部の大きさと小数部の精度のトレードオフです。

さて、本当の警告です。この手法は、精度が最優先事項である分野(金融、科学、軍事など)には明らかに適していません。通常の浮動小数点(float/double)も、全体的に固定小数点よりも優れたプロパティを持っているにもかかわらず、しばしば十分に正確ではありません。固定小数点は値に関係なく同じ精度を持ちます(これは場合によっては利点になります)。ここで、floatの精度は値の大きさに反比例します(つまり、大きさが小さいほど精度が高くなります...それよりも複雑ですが、ポイントを取得します)。また、浮動小数点数は、(ビット数で)整数(固定小数点であるかどうかにかかわらず)よりもはるかに大きな値を持ち、高い値で精度が失われます(1またはより大きい値はまったく効果がなく、整数では起こりえません)。

これらの賢明な領域で作業する場合は、任意の精度の目的専用のライブラリを使用することをお勧めします( gmplib をご覧ください、無料です)。コンピューティングサイエンスでは、本質的に、精度を高めることは、値を格納するために使用するビット数になります。高精度が必要ですか?ビットを使用します。それで全部です。

54
Alex

2つの選択肢があります。金融サービス業界で働いている場合、コードが正確さと正確さのために準拠する必要がある標準がある可能性が高いため、メモリコストに関係なく、それに従う必要があります。私は、そのビジネスは一般的に十分に資金を供給されているので、より多くのメモリにお金を払っても問題にならないことを理解しています。 :)

これが個人的な使用である場合、最大の精度を得るには、整数を使用し、保存する前にすべての価格に固定係数を掛けることをお勧めします。たとえば、正確なもの(おそらく十分ではない)が必要な場合は、すべての価格に100を掛けて、ユニットがドルではなく実質的にセントになるようにします。より正確にしたい場合は、さらに掛けます。たとえば、100分の1セント(私が聞いた標準が一般的に適用される)に正確であるためには、価格に10000(100 * 100)を掛けます。

現在、32ビット整数では、10000を掛けると、大量のドルを入れる余地がほとんどなくなります。実用的な32ビットの制限である20億は、20000ドルという高い価格しか表現できないことを意味します:2000000000/10000 =20000。結果を保持する余地がないため、その20000に何かを掛けると悪化します。このため、64ビット整数(long long)。すべての価格に10000を掛けても、掛け算を超えて大きな値を保持するための余裕が十分にあります。

固定小数点のコツは、計算を行うときは常に、各値が実際に基礎となる値に定数を乗算したものであることを覚えておく必要があるということです。加算または減算する前に、値を小さい定数で乗算して、大きい定数の値と一致させる必要があります。乗算後、目的の定数で乗算される結果に戻すには、何かで除算する必要があります。定数として2のべき乗以外を使用する場合、整数除算を行う必要がありますが、これは時間的に高価です。多くの人は定数として2のべき乗を使用しているため、除算の代わりにシフトできます。

これがすべて複雑に思える場合は、そうです。最も簡単なオプションは、倍精度を使用し、必要に応じてRAMを追加購入することです。53ビットの精度、およそ9兆、つまり16進数で約16桁です。はい、まだあなたが何十億人と働いているときにペニーを失うかもしれませんが、あなたがそれを気にするなら、あなたは正しい方法で億万長者になっていません:)

5
Randall Cook

@Alexは素晴らしい答えを出しました here 。しかし、たとえば、エミュレートされたフロート(整数を使用してフロートのように振る舞う)を任意の小数点以下の桁に丸める方法を示すことにより、彼が行ったことにいくつかの改善を加えたいと思いました。以下のコードでそれを示します。しかし、私はさらに進んで、固定小数点数学を学ぶためのコードチュートリアル全体を書くことになりました。ここにあります:

fixed_point_mathチュートリアル
-固定小数点演算、整数のみを使用した手動の「浮動」型印刷、「浮動」型整数丸め、および大きな整数での小数固定小数点演算の実行方法を学習するチュートリアル型の練習コード。

固定小数点の数学を本当に学びたいのなら、これは慎重に検討する価値のあるコードだと思いますが、書くのに週末全体を費やしたので、すべてを徹底的に調べるにはおそらく数時間かかると予想しています。 ただし、丸め処理の基本は最上部にあり、数分で学習できます。

GitHubの完全なコード: https://github.com/ElectricRCAircraftGuy/fixed_point_math

または、以下(スタックオーバーフローではそれほど多くの文字が許可されないため、省略されています):

/*
fixed_point_math tutorial
- A tutorial-like practice code to learn how to do fixed-point math, manual "float"-like prints using integers only,
  "float"-like integer rounding, and fractional fixed-point math on large integers. 

By Gabriel Staples
www.ElectricRCAircraftGuy.com
- email available via the Contact Me link at the top of my website.
Started: 22 Dec. 2018 
Updated: 25 Dec. 2018 

References:
- https://stackoverflow.com/questions/10067510/fixed-point-arithmetic-in-c-programming

Commands to Compile & Run:
As a C program (the file must NOT have a C++ file extension or it will be automatically compiled as C++, so we will
make a copy of it and change the file extension to .c first):
See here: https://stackoverflow.com/a/3206195/4561887. 
    cp fixed_point_math.cpp fixed_point_math_copy.c && gcc -Wall -std=c99 -o ./bin/fixed_point_math_c fixed_point_math_copy.c && ./bin/fixed_point_math_c
As a C++ program:
    g++ -Wall -o ./bin/fixed_point_math_cpp fixed_point_math.cpp && ./bin/fixed_point_math_cpp

*/

#include <stdbool.h>
#include <stdio.h>
#include <stdint.h>

// Define our fixed point type.
typedef uint32_t fixed_point_t;

#define BITS_PER_BYTE 8

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

// // Conversions [NEVERMIND, LET'S DO THIS MANUALLY INSTEAD OF USING THESE MACROS TO HELP ENGRAIN IT IN US BETTER]:
// #define INT_2_FIXED_PT_NUM(num)     (num << FRACTION_BITS)      // Regular integer number to fixed point number
// #define FIXED_PT_NUM_2_INT(fp_num)  (fp_num >> FRACTION_BITS)   // Fixed point number back to regular integer number

// Private function prototypes:
static void print_if_error_introduced(uint8_t num_digits_after_decimal);

int main(int argc, char * argv[])
{
    printf("Begin.\n");

    // We know how many bits we will use for the fraction, but how many bits are remaining for the whole number, 
    // and what's the whole number's max range? Let's calculate it.
    const uint8_t WHOLE_NUM_BITS = sizeof(fixed_point_t)*BITS_PER_BYTE - FRACTION_BITS;
    const fixed_point_t MAX_WHOLE_NUM = (1 << WHOLE_NUM_BITS) - 1;
    printf("fraction bits = %u.\n", FRACTION_BITS);
    printf("whole number bits = %u.\n", WHOLE_NUM_BITS);
    printf("max whole number = %u.\n\n", MAX_WHOLE_NUM);

    // Create a variable called `price`, and let's do some fixed point math on it.
    const fixed_point_t PRICE_ORIGINAL = 503;
    fixed_point_t price = PRICE_ORIGINAL << FRACTION_BITS;
    price += 10 << FRACTION_BITS;
    price *= 3;
    price /= 7; // now our price is ((503 + 10)*3/7) = 219.857142857.

    printf("price as a true double is %3.9f.\n", ((double)PRICE_ORIGINAL + 10)*3/7);
    printf("price as integer is %u.\n", price >> FRACTION_BITS);
    printf("price fractional part is %u (of %u).\n", price & FRACTION_MASK, FRACTION_DIVISOR);
    printf("price fractional part as decimal is %f (%u/%u).\n", (double)(price & FRACTION_MASK) / FRACTION_DIVISOR,
           price & FRACTION_MASK, FRACTION_DIVISOR);

    // Now, if you don't have float support (neither in hardware via a Floating Point Unit [FPU], nor in software
    // via built-in floating point math libraries as part of your processor's C implementation), then you may have
    // to manually print the whole number and fractional number parts separately as follows. Look for the patterns.
    // Be sure to make note of the following 2 points:
    // - 1) the digits after the decimal are determined by the multiplier: 
    //     0 digits: * 10^0 ==> * 1         <== 0 zeros
    //     1 digit : * 10^1 ==> * 10        <== 1 zero
    //     2 digits: * 10^2 ==> * 100       <== 2 zeros
    //     3 digits: * 10^3 ==> * 1000      <== 3 zeros
    //     4 digits: * 10^4 ==> * 10000     <== 4 zeros
    //     5 digits: * 10^5 ==> * 100000    <== 5 zeros
    // - 2) Be sure to use the proper printf format statement to enforce the proper number of leading zeros in front of
    //   the fractional part of the number. ie: refer to the "%01", "%02", "%03", etc. below.
    // Manual "floats":
    // 0 digits after the decimal
    printf("price (manual float, 0 digits after decimal) is %u.", 
           price >> FRACTION_BITS); print_if_error_introduced(0);
    // 1 digit after the decimal
    printf("price (manual float, 1 digit  after decimal) is %u.%01lu.", 
           price >> FRACTION_BITS, (uint64_t)(price & FRACTION_MASK) * 10 / FRACTION_DIVISOR); 
    print_if_error_introduced(1);
    // 2 digits after decimal
    printf("price (manual float, 2 digits after decimal) is %u.%02lu.", 
           price >> FRACTION_BITS, (uint64_t)(price & FRACTION_MASK) * 100 / FRACTION_DIVISOR); 
    print_if_error_introduced(2);
    // 3 digits after decimal
    printf("price (manual float, 3 digits after decimal) is %u.%03lu.", 
           price >> FRACTION_BITS, (uint64_t)(price & FRACTION_MASK) * 1000 / FRACTION_DIVISOR); 
    print_if_error_introduced(3);
    // 4 digits after decimal
    printf("price (manual float, 4 digits after decimal) is %u.%04lu.", 
           price >> FRACTION_BITS, (uint64_t)(price & FRACTION_MASK) * 10000 / FRACTION_DIVISOR); 
    print_if_error_introduced(4);
    // 5 digits after decimal
    printf("price (manual float, 5 digits after decimal) is %u.%05lu.", 
           price >> FRACTION_BITS, (uint64_t)(price & FRACTION_MASK) * 100000 / FRACTION_DIVISOR); 
    print_if_error_introduced(5);
    // 6 digits after decimal
    printf("price (manual float, 6 digits after decimal) is %u.%06lu.", 
           price >> FRACTION_BITS, (uint64_t)(price & FRACTION_MASK) * 1000000 / FRACTION_DIVISOR); 
    print_if_error_introduced(6);
    printf("\n");


    // Manual "floats" ***with rounding now***:
    // - To do rounding with integers, the concept is best understood by examples: 
    // BASE 10 CONCEPT:
    // 1. To round to the nearest whole number: 
    //    Add 1/2 to the number, then let it be truncated since it is an integer. 
    //    Examples:
    //      1.5 + 1/2 = 1.5 + 0.5 = 2.0. Truncate it to 2. Good!
    //      1.99 + 0.5 = 2.49. Truncate it to 2. Good!
    //      1.49 + 0.5 = 1.99. Truncate it to 1. Good!
    // 2. To round to the nearest tenth place:
    //    Multiply by 10 (this is equivalent to doing a single base-10 left-shift), then add 1/2, then let 
    //    it be truncated since it is an integer, then divide by 10 (this is a base-10 right-shift).
    //    Example:
    //      1.57 x 10 + 1/2 = 15.7 + 0.5 = 16.2. Truncate to 16. Divide by 10 --> 1.6. Good.
    // 3. To round to the nearest hundredth place:
    //    Multiply by 100 (base-10 left-shift 2 places), add 1/2, truncate, divide by 100 (base-10 
    //    right-shift 2 places).
    //    Example:
    //      1.579 x 100 + 1/2 = 157.9 + 0.5 = 158.4. Truncate to 158. Divide by 100 --> 1.58. Good.
    //
    // BASE 2 CONCEPT:
    // - We are dealing with fractional numbers stored in base-2 binary bits, however, and we have already 
    //   left-shifted by FRACTION_BITS (num << FRACTION_BITS) when we converted our numbers to fixed-point 
    //   numbers. Therefore, *all we have to do* is add the proper value, and we get the same effect when we 
    //   right-shift by FRACTION_BITS (num >> FRACTION_BITS) in our conversion back from fixed-point to regular
    //   numbers. Here's what that looks like for us:
    // - Note: "addend" = "a number that is added to another".
    //   (see https://www.google.com/search?q=addend&oq=addend&aqs=chrome.0.0l6.1290j0j7&sourceid=chrome&ie=UTF-8).
    // - Rounding to 0 digits means simply rounding to the nearest whole number.
    // Round to:        Addends:
    // 0 digits: add 5/10 * FRACTION_DIVISOR       ==> + FRACTION_DIVISOR/2
    // 1 digits: add 5/100 * FRACTION_DIVISOR      ==> + FRACTION_DIVISOR/20
    // 2 digits: add 5/1000 * FRACTION_DIVISOR     ==> + FRACTION_DIVISOR/200
    // 3 digits: add 5/10000 * FRACTION_DIVISOR    ==> + FRACTION_DIVISOR/2000
    // 4 digits: add 5/100000 * FRACTION_DIVISOR   ==> + FRACTION_DIVISOR/20000
    // 5 digits: add 5/1000000 * FRACTION_DIVISOR  ==> + FRACTION_DIVISOR/200000
    // 6 digits: add 5/10000000 * FRACTION_DIVISOR ==> + FRACTION_DIVISOR/2000000
    // etc.

    printf("WITH MANUAL INTEGER-BASED ROUNDING:\n");

    // Calculate addends used for rounding (see definition of "addend" above).
    fixed_point_t addend0 = FRACTION_DIVISOR/2;
    fixed_point_t addend1 = FRACTION_DIVISOR/20;
    fixed_point_t addend2 = FRACTION_DIVISOR/200;
    fixed_point_t addend3 = FRACTION_DIVISOR/2000;
    fixed_point_t addend4 = FRACTION_DIVISOR/20000;
    fixed_point_t addend5 = FRACTION_DIVISOR/200000;

    // Print addends used for rounding.
    printf("addend0 = %u.\n", addend0);
    printf("addend1 = %u.\n", addend1);
    printf("addend2 = %u.\n", addend2);
    printf("addend3 = %u.\n", addend3);
    printf("addend4 = %u.\n", addend4);
    printf("addend5 = %u.\n", addend5);

    // Calculate rounded prices
    fixed_point_t price_rounded0 = price + addend0; // round to 0 decimal digits
    fixed_point_t price_rounded1 = price + addend1; // round to 1 decimal digits
    fixed_point_t price_rounded2 = price + addend2; // round to 2 decimal digits
    fixed_point_t price_rounded3 = price + addend3; // round to 3 decimal digits
    fixed_point_t price_rounded4 = price + addend4; // round to 4 decimal digits
    fixed_point_t price_rounded5 = price + addend5; // round to 5 decimal digits

    // Print manually rounded prices of manually-printed fixed point integers as though they were "floats".
    printf("rounded price (manual float, rounded to 0 digits after decimal) is %u.\n", 
           price_rounded0 >> FRACTION_BITS); 
    printf("rounded price (manual float, rounded to 1 digit  after decimal) is %u.%01lu.\n", 
           price_rounded1 >> FRACTION_BITS, (uint64_t)(price_rounded1 & FRACTION_MASK) * 10 / FRACTION_DIVISOR); 
    printf("rounded price (manual float, rounded to 2 digits after decimal) is %u.%02lu.\n", 
           price_rounded2 >> FRACTION_BITS, (uint64_t)(price_rounded2 & FRACTION_MASK) * 100 / FRACTION_DIVISOR); 
    printf("rounded price (manual float, rounded to 3 digits after decimal) is %u.%03lu.\n", 
           price_rounded3 >> FRACTION_BITS, (uint64_t)(price_rounded3 & FRACTION_MASK) * 1000 / FRACTION_DIVISOR); 
    printf("rounded price (manual float, rounded to 4 digits after decimal) is %u.%04lu.\n", 
           price_rounded4 >> FRACTION_BITS, (uint64_t)(price_rounded4 & FRACTION_MASK) * 10000 / FRACTION_DIVISOR); 
    printf("rounded price (manual float, rounded to 5 digits after decimal) is %u.%05lu.\n", 
           price_rounded5 >> FRACTION_BITS, (uint64_t)(price_rounded5 & FRACTION_MASK) * 100000 / FRACTION_DIVISOR); 


    // =================================================================================================================

    printf("\nRELATED CONCEPT: DOING LARGE-INTEGER MATH WITH SMALL INTEGER TYPES:\n");

    // RELATED CONCEPTS:
    // Now let's practice handling (doing math on) large integers (ie: large relative to their integer type),
    // withOUT resorting to using larger integer types (because they may not exist for our target processor), 
    // and withOUT using floating point math, since that might also either not exist for our processor, or be too
    // slow or program-space-intensive for our application.
    // - These concepts are especially useful when you hit the limits of your architecture's integer types: ex: 
    //   if you have a uint64_t nanosecond timestamp that is really large, and you need to multiply it by a fraction
    //   to convert it, but you don't have uint128_t types available to you to multiply by the numerator before 
    //   dividing by the denominator. What do you do?
    // - We can use fixed-point math to achieve desired results. Let's look at various approaches.
    // - Let's say my goal is to multiply a number by a fraction < 1 withOUT it ever growing into a larger type.
    // - Essentially we want to multiply some really large number (near its range limit for its integer type)
    //   by some_number/some_larger_number (ie: a fraction < 1). The problem is that if we multiply by the numerator
    //   first, it will overflow, and if we divide by the denominator first we will lose resolution via bits 
    //   right-shifting out.
    // Here are various examples and approaches.

    // -----------------------------------------------------
    // EXAMPLE 1
    // Goal: Use only 16-bit values & math to find 65401 * 16/127.
    // Result: Great! All 3 approaches work, with the 3rd being the best. To learn the techniques required for the 
    // absolute best approach of all, take a look at the 8th approach in Example 2 below.
    // -----------------------------------------------------
    uint16_t num16 = 65401; // 1111 1111 0111 1001 
    uint16_t times = 16;
    uint16_t divide = 127;

    printf("\nEXAMPLE 1\n");

    // Find the true answer.
    // First, let's cheat to know the right answer by letting it grow into a larger type. 
    // Multiply *first* (before doing the divide) to avoid losing resolution.
    printf("%u * %u/%u = %u. <== true answer\n", num16, times, divide, (uint32_t)num16*times/divide);

    // 1st approach: just divide first to prevent overflow, and lose precision right from the start.
    uint16_t num16_result = num16/divide * times;
    printf("1st approach (divide then multiply):\n");
    printf("  num16_result = %u. <== Loses bits that right-shift out during the initial divide.\n", num16_result);

    // 2nd approach: split the 16-bit number into 2 8-bit numbers stored in 16-bit numbers, 
    // placing all 8 bits of each sub-number to the ***far right***, with 8 bits on the left to grow
    // into when multiplying. Then, multiply and divide each part separately. 
    // - The problem, however, is that you'll lose meaningful resolution on the upper-8-bit number when you 
    //   do the division, since there's no bits to the right for the right-shifted bits during division to 
    //   be retained in.
    // Re-sum both sub-numbers at the end to get the final result. 
    // - NOTE THAT 257 IS THE HIGHEST *TIMES* VALUE I CAN USE SINCE 2^16/0b0000,0000,1111,1111 = 65536/255 = 257.00392.
    //   Therefore, any *times* value larger than this will cause overflow.
    uint16_t num16_upper8 = num16 >> 8; // 1111 1111
    uint16_t num16_lower8 = num16 & 0xFF; // 0111 1001
    num16_upper8 *= times;
    num16_lower8 *= times;
    num16_upper8 /= divide;
    num16_lower8 /= divide;
    num16_result = (num16_upper8 << 8) + num16_lower8;
    printf("2nd approach (split into 2 8-bit sub-numbers with bits at far right):\n");
    printf("  num16_result = %u. <== Loses bits that right-shift out during the divide.\n", num16_result);

    // 3rd approach: split the 16-bit number into 2 8-bit numbers stored in 16-bit numbers, 
    // placing all 8 bits of each sub-number ***in the center***, with 4 bits on the left to grow when 
    // multiplying and 4 bits on the right to not lose as many bits when dividing. 
    // This will help stop the loss of resolution when we divide, at the cost of overflowing more easily when we 
    // multiply.
    // - NOTE THAT 16 IS THE HIGHEST *TIMES* VALUE I CAN USE SINCE 2^16/0b0000,1111,1111,0000 = 65536/4080 = 16.0627.
    //   Therefore, any *times* value larger than this will cause overflow.
    num16_upper8 = (num16 >> 4) & 0x0FF0;
    num16_lower8 = (num16 << 4) & 0x0FF0;
    num16_upper8 *= times;
    num16_lower8 *= times;
    num16_upper8 /= divide;
    num16_lower8 /= divide;
    num16_result = (num16_upper8 << 4) + (num16_lower8 >> 4);
    printf("3rd approach (split into 2 8-bit sub-numbers with bits centered):\n");
    printf("  num16_result = %u. <== Perfect! Retains the bits that right-shift during the divide.\n", num16_result);

    // -----------------------------------------------------
    // EXAMPLE 2
    // Goal: Use only 16-bit values & math to find 65401 * 99/127.
    // Result: Many approaches work, so long as enough bits exist to the left to not allow overflow during the 
    // multiply. The best approach is the 8th one, however, which 1) right-shifts the minimum possible before the
    // multiply, in order to retain as much resolution as possible, and 2) does integer rounding during the divide
    // in order to be as accurate as possible. This is the best approach to use.
    // -----------------------------------------------------
    num16 = 65401; // 1111 1111 0111 1001 
    times = 99;
    divide = 127;

    printf("\nEXAMPLE 2\n");

    // Find the true answer by letting it grow into a larger type.
    printf("%u * %u/%u = %u. <== true answer\n", num16, times, divide, (uint32_t)num16*times/divide);

    // 1st approach: just divide first to prevent overflow, and lose precision right from the start.
    num16_result = num16/divide * times;
    printf("1st approach (divide then multiply):\n");
    printf("  num16_result = %u. <== Loses bits that right-shift out during the initial divide.\n", num16_result);

    // 2nd approach: split the 16-bit number into 2 8-bit numbers stored in 16-bit numbers, 
    // placing all 8 bits of each sub-number to the ***far right***, with 8 bits on the left to grow
    // into when multiplying. Then, multiply and divide each part separately. 
    // - The problem, however, is that you'll lose meaningful resolution on the upper-8-bit number when you 
    //   do the division, since there's no bits to the right for the right-shifted bits during division to 
    //   be retained in.
    // Re-sum both sub-numbers at the end to get the final result. 
    // - NOTE THAT 257 IS THE HIGHEST *TIMES* VALUE I CAN USE SINCE 2^16/0b0000,0000,1111,1111 = 65536/255 = 257.00392.
    //   Therefore, any *times* value larger than this will cause overflow.
    num16_upper8 = num16 >> 8; // 1111 1111
    num16_lower8 = num16 & 0xFF; // 0111 1001
    num16_upper8 *= times;
    num16_lower8 *= times;
    num16_upper8 /= divide;
    num16_lower8 /= divide;
    num16_result = (num16_upper8 << 8) + num16_lower8;
    printf("2nd approach (split into 2 8-bit sub-numbers with bits at far right):\n");
    printf("  num16_result = %u. <== Loses bits that right-shift out during the divide.\n", num16_result);

    /////////////////////////////////////////////////////////////////////////////////////////////////
    // TRUNCATED BECAUSE STACK OVERFLOW WON'T ALLOW THIS MANY CHARACTERS.
    // See the rest of the code on github: https://github.com/ElectricRCAircraftGuy/fixed_point_math
    /////////////////////////////////////////////////////////////////////////////////////////////////

    return 0;
} // main

// PRIVATE FUNCTION DEFINITIONS:

/// @brief A function to help identify at what decimal digit error is introduced, based on how many bits you are using
///        to represent the fractional portion of the number in your fixed-point number system.
/// @details    Note: this function relies on an internal static bool to keep track of if it has already
///             identified at what decimal digit error is introduced, so once it prints this fact once, it will never 
///             print again. This is by design just to simplify usage in this demo.
/// @param[in]  num_digits_after_decimal    The number of decimal digits we are printing after the decimal 
///             (0, 1, 2, 3, etc)
/// @return     None
static void print_if_error_introduced(uint8_t num_digits_after_decimal)
{
    static bool already_found = false;

    // Array of power base 10 values, where the value = 10^index:
    const uint32_t POW_BASE_10[] = 
    {
        1, // index 0 (10^0)
        10, 
        100, 
        1000, 
        10000, 
        100000,
        1000000,
        10000000,
        100000000,
        1000000000, // index 9 (10^9); 1 Billion: the max power of 10 that can be stored in a uint32_t
    };

    if (already_found == true)
    {
        goto done;
    }

    if (POW_BASE_10[num_digits_after_decimal] > FRACTION_DIVISOR)
    {
        already_found = true;
        printf(" <== Fixed-point math decimal error first\n"
               "    starts to get introduced here since the fixed point resolution (1/%u) now has lower resolution\n"
               "    than the base-10 resolution (which is 1/%u) at this decimal place. Decimal error may not show\n"
               "    up at this decimal location, per say, but definitely will for all decimal places hereafter.", 
               FRACTION_DIVISOR, POW_BASE_10[num_digits_after_decimal]);
    }

done:
    printf("\n");
}

出力:

gabriel $ cp fixed_point_math.cpp fixed_point_math_copy.c gcc -Wall -std = c99 -o ./bin/fixed_point_math_c> fixed_point_math_copy.c ./bin/fixed_point_math_c
ベギン。
小数ビット= 16。
整数ビット= 16。
最大整数= 65535。

真のダブルとしての価格は219.857142857です。
整数としての価格は219です。
価格の小数部は56173(65536のうち)です。
小数部の価格の小数部は0.857132(56173/65536)です。
price(手動浮動小数点、小数点以下0桁)は219です。
price(手動浮動小数点、小数点以下1桁)は219.8です。
price(手動浮動小数点、小数点以下2桁)は219.85です。
price(手動浮動小数点、小数点以下3桁)は219.857です。
price(手動浮動小数点、小数点以下4桁)は219.8571です。
price(手動浮動小数点、小数点以下5桁)は219.85713です。 <==最初に固定小数点数学10進数エラー
固定小数点解像度(1/65536)の解像度が低くなったため、ここから導入を開始します
この小数点以下のベース10解像度(1/100000)より。 10進エラーが表示されない場合があります
この小数点以下の位置は、言うまでもありませんが、今後はすべての小数点以下の桁が確実に増加します。
price(手動浮動小数点、小数点以下6桁)は219.857131です。

手動の整数ベースのラウンド:
addend0 = 32768。
addend1 = 3276。
addend2 = 327。
addend3 = 32。
addend4 = 3。
addend5 = 0。
四捨五入価格(手動浮動小数点、小数の後の0桁に丸められた)は220です。
四捨五入価格(手動浮動小数点数、小数点以下1桁に四捨五入)は219.9です。
四捨五入価格(手動浮動小数点数、小数点以下2桁に四捨五入)は219.86です。
四捨五入価格(手動浮動小数点数、小数点以下3桁に四捨五入)は219.857です。
四捨五入価格(手動浮動小数点数、小数点以下4桁に四捨五入)は219.8571です。
四捨五入価格(手動浮動小数点数、小数点以下5桁に四捨五入)は219.85713です。

関連する概念:小さい整数型で大きな整数演算を行う:

実施例1
65401 * 16/127 = 8239。<==真の答え
最初のアプローチ(分割してから乗算):
num16_result = 8224。<==初期除算中に右シフトアウトしたビットを失います。
2番目のアプローチ(右端のビットで2つの8ビットのサブ番号に分割):
num16_result = 8207。<==分割中に右シフトアウトしたビットを失います。
3番目のアプローチ(ビットを中央に配置した2つの8ビットのサブ番号に分割):
num16_result = 8239。<==パーフェクト!除算中に右シフトするビットを保持します。

実施例2
65401 * 99/127 = 50981。<==正解
最初のアプローチ(分割してから乗算):
num16_result =50886。<==最初の除算中に右シフトアウトしたビットを失う。
2番目のアプローチ(右端のビットで2つの8ビットのサブ番号に分割):
num16_result = 50782。<==除算中に右シフトアウトしたビットを失います。
3番目のアプローチ(ビットを中央に配置した2つの8ビットのサブ番号に分割):
num16_result = 1373。<==乗算中のオーバーフローにより完全に間違っています。
4番目のアプローチ(ビットを中央に配置した4つの4ビットのサブ番号に分割):
num16_result = 15870。<==乗算中のオーバーフローにより完全に間違っています。
5番目のアプローチ(ビットを中央に配置した8つの2ビットのサブ番号に分割):
num16_result =50922。<==除算中に右シフトアウトするいくつかのビットが失われます。
第6のアプローチ(16ビットの1ビットのサブ番号に分割され、ビットが左にずれています):
num16_result = 50963。<==除算中に右にシフトアウトする可能な限り少ないビットを失います。
7番目のアプローチ(16ビットの1ビットのサブ番号に分割され、ビットが左にスキューされています):
num16_result =50963。<== [第6のアプローチと同じ]除算中に右シフトアウトする可能な限り少ないビットを失います。
[すべてのベストアプローチ] 8番目のアプローチ(16ビットの1ビットのサブ番号に分割され、ビットが左にスキューされ、除算中に整数丸めが行われます):
num16_result = 50967。<==除算中に右にシフトアウトする可能性のある最小ビットが失われ、
&は、除算中の丸めにより精度が向上しています。

参照:

1
Gabriel Staples

メモリを節約することが唯一の目的である場合は、そうすることはお勧めしません。価格の計算におけるエラーが蓄積される可能性があり、あなたはそれを台無しにしようとしている。

本当に似たようなものを実装したい場合は、価格の最小間隔を取り、intおよび整数演算を直接使用して数値を操作できますか?表示するときに浮動小数点数に変換するだけで済み、生活が楽になります。

0
unsym