定点数运算

在电脑中，定点数（fixed-point number）是指用固定整数位数表达分数的格式，属于实数数据类型中一种。例如美元常会表示到二位小数，以分来表示，即为一种定点数。有时定点数也会要求要有固定的整数位数。定点数与更复杂的浮点数相对。

在定点数表示法中，小数部分和整数部分一样，也会表示为进制底数b的幂次，不过是以负数幂次来表示。最常见的定点数表示法是十进制（底数为10）和二进制（底数为2）。若储存了n位的小数，其数值一定是b−n的整数倍。定点数表示法也会用来省略整数中较低位数的值，例如将金钱表示为1000美元的整数倍。

在人们处理有小数的十进制数字时，会在整数和小数之间加上小数点（'.'或是','）。不过定点数中，整数和小数的位数长度是依程式的规划来决定。

在机械计算器中主要会使用定点数运算。由于大多数现代的中央处理器就有浮点运算器（FPU），只有在特殊的应用中才使用定点数运算，例如低价的嵌入式系统微处理器以及单片机，这类的应用强调高需求速度，低电力需求及小集成电路区域，例如影像、视频处理或数字信号处理，或是一些这种表示法比较适合问题本质的议题。后者的例子是会计学的金钱单位，非整数的金额也需要进行四舍五入，另一种情形是在产生函数的查找表。

## 表示法

小数数值｜用整数表示的值

0.00｜0

0.5｜50

0.99｜99

2｜200

−14.1｜−1410

314.160｜31416

定点数类型的值其实就是个整数，需要额外做比例进位，进多少位需要根据具体的定点数类型决定。例如 1.23 使用 1/1000 缩放系数的定点数表示时是 1230；1,230,000 使用 1000 缩放系数的定点数表示也是 1230。与浮点数不同，相同类型的定点数中所有值的缩放系数都是一致的，在计算过程中也保持不变。此表示法可以用标准的整数算术逻辑单元来进行有理数的计算。

二进制定点数下的负数常常会用二补数型式下的有号整数表示，其中隐含着缩放系数（或小数长度）。数值的正负号会依最高位元而定（1表示负值，0表示正值）。就是小数位数等于或大于整个数字的位置也是如此。例如，8位元二进制有号整数(11110101)2 = −11，若分别配合-3, +5及+12的隐含小数长度，数值为−11/2−3 = −88、−11/25 = −0.34375、和−11/212 = −0.002685546875。

另一种作法，负号可以用“符号-大小”的有符号数处理表示法以整数来表示，另外表示正负号，也有隐含的缩放系数。此作法常用于十进制的定点数贞算中。例如有号的十进制五位数 (−00025)10，配合-3, +5, and +12的隐含十进制小数长度，数值为−25/10−3 = −25000、−25/105 = −0.00025和−25/1012 = −0.000000000025。

程式一般会假设所有储存在变数中的定点数，或是指令集架构产生的定点数，都会有相同的缩放系数。程式设计者可以依需要的测量精度以及实际数值的范围来选择缩放系数。

变数或是公式的缩放系数不一定会直接在程式码中出现。好皊软件工程实务会要求在软件文档中说明缩放系数，至少也要在源代码的注释中说明。

定点数的最大值，可以通过将其内部所使用的整数的最大值乘以缩放系数求得，最小值同理。

### 缩放系数的选择

为了效率考量，缩放系数（scaling factor）一般会是基数b（2 或是 10）的正幂次，或是负幂次，因此实际内部仍然可以用类似整数的方式处理。不过缩放系数也需依应用而定。因此许多的数字可能其数值其实是用二进制记录，但为了使用方便人类读写，缩放系数仍选择10的幂，10的幂的缩放系数也可以配合国际单位制，因为选择特定的缩放系数，可能相当于使用另外一个大小较适合的单位，例如使用厘米或微米，而不是使用米。

不过有时也会使用其它缩放系数，例如可以用 1/3600 缩放系数的定点数来表示以小时为单位的时间值，可以精确到秒。

就算针对定点数进行最仔细的四舍五入，缩放系数S的定点数，其误差最大会到其整数数±0.5，因此误差的实际数值范围是±0.5 S。若缩放系数越小，所得到结果越准确。

不过，缩放系数越小也表示相同长度下所能记录的数值会比较小。定点数下可以储存的最大数值会对应可储存的最大整数，再乘以转换系数，其最小值也类似。例如，下表列出在16位元有号二进制定点数下，不同小数位元f下的缩放系数S，可以表示的最大值和最小值Vmin和Vmax，以及其精度δ = S/2。

f｜S｜δ｜Vmin｜Vmax

−3｜1/2−3 = 8｜4｜−0.262144｜+0.262143

0｜1/20 = 1｜0.5｜−0.032768｜+0.032767

5｜1/25 = 1/32｜< 0.016｜−1024.00000｜+1023.96875

14｜1/214 = 1/16384｜< 0.000031｜−2.00000000000000｜+1.99993896484375

15｜1/215 = 1/32768｜< 0.000016｜−1.000000000000000｜+0.999969482421875

16｜1/216 = 1/65536｜< 0.000008｜−0.5000000000000000｜+ 0.4999847412109375

20｜1/220 = 1/1048576｜< 0.0000005｜−0.03125000000000000000｜+0.03124904632568359375

### 精确值

在二进制的定点数中，考虑二进制下的分数a/2m（例如1/16或17/32），若其缩放系数为1/2n，且n ≥ m，即可以精确的用二进制定点数表示。不过大部分的十进制小数（如0.1或0.123）在二进制下会是无穷循环小数，因此无法用二进制表示其精确值。

十进制的情形也类似，考虑十进制下的分数a/10m（例如1/100或37/1000）若其缩放系数为1/10n，且n ≥ m，即可以精确的用十进制定点数表示。若分母是二的次幂的分数a/2m（如1/8 (0.125)或17/32 (0.53125），且n ≥ m，也可以精确的用十进制定点数表示。

若考虑有理数 a/b，其中a和b互质，b为正数。若用二进制的定点数表示，只有在b是2的次幂下才能表示其精确值，若用十进制的定点数表示，只有在b的质因数没有2和5以外的数字时，才能表示其精确值。

### Q格式

表示字长和二进制定点数中小数点位置的方法有很多种。下面的例子中，使用 f 表示小数部分位数、m 表示整数部分位数、s 表示符号位位数、b 表示总位数，其中“位数”均为比特位。

- Qf：称作“Q 格式”，Q15 表示 15 个小数位。这样的记法存在歧义，因为没有包含字长，但实际使用时一般可以根据目标处理器平台判断字长为 16 或者 32 位。
- Qm.f：无歧义的 Q 格式，因为整个字是补码整数，所以符号位长度可以根据其它信息推导而来。例如 Q1.30 表示该数有 1 个整数位、30 个小数位，是 32 位补码整数。
- fxm.b：“fx 格式”与上述 Q 格式类似，区别在于点号后是字长。例如 fx1.16 表示有 1 个整数位、15 个小数位的 16 位字。
- s:m:f：PS2 中所使用的表示法，包含符号位信息。0:8:0 表示 8 位无符号整数。

## 和浮点数的比较

定点数的运算比浮点数要快，需要的硬件也比较少。假如要计算的数值范围事先就已知道，而且限制在较小的范围内，定点数运算可以有效的使用其位元。例如，用32位元表示从0到1的数字，定点数下的误差会小于1.2 × 10−10，而浮点数的误差则为596 × 10−10，因为其中有九个位元表示指数以及指数的正负号，因此误差较大。

使用定点数运算的程式，不会受到是否有浮点运算器（FPU）的影响，可移植性比浮点数要高。在IEEE 754普及之前，相同资料在浮点运算后的结果会依处理器厂商而不同，因此此一优点影响很大。

许多早期的嵌入型系统没有浮点运算器，整数运算单元需要的逻辑闸和集成电路较少，而且低速备下，用软件模拟浮点运算的速度太慢，无法实用。早期个人电脑和电子游戏机的处理器（例如Intel 80386和Intel 80486）都没有浮点运算器。

定点格式下的绝对分辨率（二个连续数值之间的差），在整个数值范围下都是定值，例如是缩放系数S。相反的定点格式下的相对分辨率，在整个数值范围下都是定值，但其绝对分辨率会随数值而变化，变化甚至会到数个数量值。

在许多应用中，定点运算的四舍五入以及舍去误差比较好分析。不过在定点运算下，程式设计者需要花较多的心力，例如为了避免溢位，需要确认计算中所有中间值的范围，为了要调整到理想的缩放系数，需要有额外的程式处理信号转换。

## 应用

十进制定点数常用在储存货币价值上，因为浮点数的复杂舍入原则，往往会造成负担。例如开源的现金管理应用程序，GnuCash，以C语言撰写，就因为此原因从1.6版起由浮点数改为定点数。

自从1960年代末期，一直到1980年代，二进制定点数常用在要用到大量数学实时计算的程式，例如飞行模拟器以及核电厂控制算法。在许多数字信号处理应用或是客制的微处理器中仍常使用定点数。像有关角度的计算，就会使用二进制角度测量（BAM）。

在STM32G4系列的CORDIC协同处理器，以及JPEG影像压缩使用的离散余弦变换（DCT）算法，都使用二进制定点数。

## 运算

### 加法和减法

若要针对二个缩放系数相同的数字相加或相减，直接针对数字运算即可，运算结果也会有相同的缩放系数，因此可以储存在变数中，或是用在后续的运算中。只要运算过程没有算术溢位（也就是运算过程和结果都可以正确的储存），其结果就会是精确的。若二个缩放系数不相同的数字要相加减，需转换到相同的缩放系数，才能相加减。

### 乘法

若要将二个定点数相乘，可以直接将其数字相乘，再假设最后积的缩放系数是被乘数和乘数缩放系数的乘积即可。只要没有舍入，也没有运算溢位，结果会是精确的。

例如，要将123（缩放系数1/1000，实际数值是0.123）和25（缩放系数1/10，实际数值是2.5）会得到123×25 = 3075，缩放系数是(1/1000)×(1/10) = 1/10000，因此实际数值是0.3075。另一个例子是将第一个数字和155（缩放系数1/32，实际数值是155/32 = 4.84375）相乘，所得的数字是123×155 = 19065，其缩放系数是(1/1000)×(1/32) = 1/32000，因此实际数值是19065/32000 = 0.59578125。

为了避免溢位，储存乘积的变数长度会比被乘数和乘数的要长，例如被乘数和乘数分别是十进制的二位数，乘积需要用十进制的四位数来储存，才不会溢位。

### 除法

若要将二个定点数相除，可以直接将其数字相除，再假设最后商的缩放系数是被除数和除数缩放系数相除后的商即可。一般来说，相除时会出现舍入，因此结果无法完全精确。

例如，3456（缩放系数1/100，实际数值34.56）和1234（缩放系数1/1000，实际数值1.234）相除会得到3456÷1234 = 3（有舍入），其缩放系数为(1/100)/(1/1000) = 10，因此实际数值是30。若第一个数除以155（缩放系数1/32，实际数值155/32 = 4.84375），会得到3456÷155 = 22（有舍入），其缩放系数为(1/100)/(1/32) = 32/100 = 8/25，因此数值为22×32/100 = 7.04。

在结果不精确时，若被除数使用较小的缩放系数，可以缩小（甚至消除）因为舍入产生的误差。例如，r = 1.23，表示为定点数123，缩放系数1/100，而s = 6.25，表示为6250，缩放系数1/1000，相除后的结果是123÷6250 = 0（有舍入），其缩放系数为(1/100)/(1/1000) = 10。若r先转换为缩放系数1/1000000的数字，定点数表示会是1/1000000，其结果会是1,230,000÷625 = 197（有舍入），其缩放系数为1/1000（实际数值是0.197）。其精确值是1.23/6.25 = 0.1968，后者的计算结果比较接近。

### 缩放系数的调整

在定点数运算中，常常需要调整定点数的缩放系数，以下是一些可能的原因:

- 将一数值存到不同缩放系数的变数中。
- 将二个数值转换为相同的缩放系数，以便相加减或是比较。
- 一个数值在和其他数值相乘或是相除后，恢复成原来的缩放系数。
- 要增加除法运算的精度
- 要确保乘积或是商的缩放系数是像10n或2n，基数的次幂。
- 要确保运算结果可以在没有溢位的情形下储存在变数内。
- 为了要减少处理定点数硬件的成本。

若要将缩放系数R的定点数转换为缩放系数S的定点数，整数需要乘以R/S。以1.23（= 123/100）为例，若要从缩放系数R=1/100，转换为缩放系数S=1/1000，整数123要乘以(1/100)/(1/1000) = 10，因此即为1230/1000。

若二个缩放系数都是基数的次幂，调整缩放系数只是除去较低位数的数字，或是在较低位数增加零。不过，此一运算不能改变符号位元，因此需要像算术移位运算一样的方式，延伸其符号位元。

若R不能被S整除（特别是新的缩放系数S比原来的缩放系数R要大），产生的整数需要进行数值修约。

若r和s是定点数，其缩放系数分别为R和S，运算r ← r×s需要将整数相乘，再除以S，所得结果可能会有数值修约，也有可能会溢位。

例如，二个定点数的缩放系数都是1/100，将1.23乘以0.25，其整数相乘是123乘以25，得到3075，而缩放系数是1/10000，为了还原到1/100的缩放系数，3075需乘以1/100，也就是除以100，依数值修约方法的不同，所得结果可能是31（0.31）或是30（0.30）。

而运算r ← r/s需要将整数相除，再乘以S，所得结果可能会有数值修约，也有可能会溢位。

### 和浮点数的转换

若要将浮点数转换为定点数，可以将浮点数除以缩放系数S，再修约到最接近的整数。需确认对应的变数有足够的大小储存所得的结果。依缩放系数、变数长度，以及数值大小的不同，转换有可能需要修约。

若要将定点数转换为浮点数，可以将定点数乘以缩放系数S。若整数的绝对值超过224（IEEE单精度浮点数）或253（IEEE双精度浮点数），可能会需要修约。若|S|非常大或是非常小，可能会有溢位或算术下溢（小于可以表示的最小数）的情形。

## 硬件支援

### 比例和正规化

一般的处理器没有特别支援定点数的运算，不过大部分二进制运算的处理器会有快速的位操作指令，因此可以在很短的时间进行和二的乘幂的乘法或除法，有些还有算术移位指令，在和二的乘幂相乘除的同时，还可以保留数字的正负号。

早期像是IBM 1620或是Burroughs B3500的电脑会使用二进码十进数（BCD）表示法来处理整数，在十进制下每一个位数会分别用四个次元来储存，有些处理器还使用此一运算方式，因此可以用移位的方式来处理和十的乘幂相乘除的运算。

有些DSP架构会支援特定的定点运算格式，例如有号的n位元数字，其中有n−1位是储存小数（其储存数值范围从-1到几乎是+1的），其中会有特别包括正规化的乘法指令，在相乘后将小数位数从2n−2位调整回n−1位。若DSP没有支援此一机能，程式设计者需要用够大的暂存器或是暂存变数来储存此数值，再另外用程式进行正规化处理。

### 溢位

若运算结果的数值太大，超过要储存位置可以储存的范围，就会出现溢位。若是加法或是减法，所得结果会比运算数字多一个位元，若是乘法，所得结果的位元数会是二个运算数字位元数的和。

若出现溢位，最高位元的资讯可能会因此而更改，若储存空间有n位元，所储存的会是除以2n的余数，而最高位元会视为符号位元，因此结果的大小及正负号都可能改变。

有些处理器有溢位旗标，或是在溢位时进行的异常处理。有些处理器则会有饱和运算，若相加或是相减的结果可能会溢位，考虑其正负号，储存相同符号，绝对值最大的数值。

饱和运算只保留了正负号，无法保留结果的大小，在实务上可能不太实用。一般比较简单及安全的作法是选择适当的缩放系数以及变数储存长度，避免溢位的可能性，或者在运算之前先检查运算结果是否可能溢位。

## 程式语言支援

有些程式语言明确支援定点数表示法及运算，著名的有PL/I、COBOL、Ada、JOVIAL及Coral 66。这些程式语言中提供定点数的数值资料类型，可能是二进制或十进制，可以指定给变数或是函式。编译器会在处理有关的程式码时自动进行转换，例如处理类似r ← s × t + u的程式码，读写这些变数的值，或是将这些资料类型转换为浮点数时。

这类的程式语言多半是在1940年至1990年之间设计的，现今的程式语言多半都不提供定点数资料类型，也不提供相关的系数转换。有些较古老，但仍很受欢迎的程式语言也是如此，例如Fortran、C语言和C++。浮点处理器的普及，以及严格标准化的行为，大幅的减少二进制定点数的需求。有些程式语言支援十进制浮点数（例如C♯和Python），也去除了大多数需要十进制固点数的需求。很少数需要固点数的情形，会由程序员写程式实现，并且有明确的系数转换，这在任何程式语言都可以实现。

另一方面，所有关联式数据库以及SQL表示法都支援用十进制定点数来进行数值的储存以及运算。PostgreSQL有一种特别的numeric型态，可以精确的储存数值，位数最多可以到1000位。

ISO在2008年针对C语言提出了有关定点数资料型态的扩充提案，目的为了方便程式在嵌入式处理器上执行。GCC的编译器也支援定点数。

## 具体范例

### 十进制定点数乘法

以下是二个三位小数的定点数乘法。

(10.500)(1.050)

=1*10.500 + 0.050*10.500

= 10.500+0.525000=11.025000

因为有三位小数，因此保留小数最后的0。为了要重整为整数乘法，先将数字都乘以1000，使数字都变成整数，之后再乘以$1/1000$二次，使最终结果不会改变，其算式如下

```
(10.500)(10^(3)) (1.050)(10^(3))  (10^(-3))(10^(-3))
= (10500)(1050) (10^-6)
= 11 025 000  (10^-6)
= 11.025000
```

若用其他的进制（例如方便计算的二进制），也可以用类似的原理来进行，因为二进制的左移或右移都相当是数值的乘2或是除2。十进制的三位数接近二进制的十位数，因此可以将0.5表示为有十位小数的二进制数字。最低的近似值是0.0000110011.

```
10= 8+2=2^3+2^1
1=2^0
0.5= 2^-1
0.05= 0.0000110011（二進制）
```

乘法会变成

```
(1010.100)(2^3)(1.0000110011)(2^10) (2^-13)
=(1010100)(10000110011) (2^-13)
=(10110000010111100) (2^-13)
=1011.0000010111100
```

若转为十进制，保留三位小数，会是11.023。

### 二进制定点数运算

考虑计算1.2乘5.6的乘积，使用有16位小数的二进制定点数。为了要表示这二个数字，需先将二个数字乘以216，所得的是78643.2和367001.6，再四舍五入到最近整数，是78643和367002，此数字可以放在32位元的word中，用二补数的有号数表示法。

二个数字相乘会得到35位元的整数28862138286，小数位数则有32位。若放在32位元的整数中，会出现溢位，而且会失去最高位的位元，因此，应该存在有号的64位元整数变数或是寄存器中。

若结果要储存在也是16位小数的资料格式中，上述的整数需要除以216，结果会是440401.28，再四舍五入到最近整数，可以用先加上215，再将数字右移16位元的方式达到相同效果，其结果是440401，表示的数值是6.7199859619140625。考虑此格式的精度，结果可以表示为6.719986 ± 0.000008（不考虑运算近似产生的误差）。正确结果是1.2 × 5.6 = 6.72。

## 标示方式

为了要精确的说明定点数的参数，有许多不同的标示方式。在以下的表中，f表示小数位元数，m表示整数位元数，s表示符号位元，而b是总位元数。

- COBOL程式语言一开始支援任意大小以及小数位数的十进制定点数，其格式会用`PIC`命令功能（directive）来标示。例如`PIC S9999V99`代表六位数整数，二位小数的的十进制有号数。
- PL/I程式语言使用`REAL FIXED BINARY (`p`,`f`)`的construct来标示，标示二进制定点数，其中共有p位数（不考虑符号），其中小数是f位数，总共的有号整数有p+1位数，其转换系数是1/2f。也可以用`COMPLEX` 代替`REAL`，用`DECIMAL`代替`BINARY`来表示十进制。
- 在Ada程式语言中，数值型态的表示法会像`type F is delta 0.01 range -100.0 .. 100.0`，表示用二补数格式的有号二进制表示法，其中小数位数是7位元（缩放系数是1/128），至少要用15位元来储存（确保实际范围可以从-128.00到将近+128.00）。
- Q格式是由德州仪器定义的。用`Q`f来表示有f位小数的有号定点数，例如`Q15`是缩放系数为1/215的二补数有号数。而 `Q`m`.`f额外的标示数字的整数部分有m位（不算符号位元）。因此`Q1.30`表示有一位整数，30位小数的二进制定点数，可以存在32位元二补数整数中，缩放系数是1/230。ARM架构也有类似的标示法，但其中的m有包括符号位元，因此上述的格式要改为`Q2.30`。
- VisSim公司用`fx`m`.`b来表示共有b位元，小数有m位元的二进制定点数，因此是b位元的整数，缩放系数是1/2b−m。因此`fx1.16`是十六位元数字，整数部分1位元，小数部分15位元。
- PlayStation 2 GS（Graphics Synthesizer）使用者指南中用s`:`m`:`f的标示方式，其中s表示是否有符号位元。例如，`0:5:3`表示8位元整数，缩放系数是1/23的无号数。
- LabVIEW程式语言的标示方式是`<`s`,`b`,`m`>`，表示FXP定点数的格式。s可以是'+'或'±'，表示无号数或是二补数的有号数。b是总位元个数，而m是整数部分的位数。

## 软件应用例

- 常用的TrueType字型格式，在一些指令中，使用32位元的有号二进制定点数，来表达数值。这个格式是在考虑字体微调以及效能下，可以提供最小精度的格式。
- Sony原生PlayStation和世嘉土星的所有3D计算引擎、任天堂的Game Boy Advance（其中的二维计算机图形）、任天堂DS（2D和3D）、任天堂GameCube及GP2X Wiz电子游戏系统，这些系统没有浮点运算器，都使用定点运算。PlayStation中的转换协处理器中有硬件支援16位元的浮点运算，其中有12位的小数。
- 广为科学界和数学界使用的TeX软件，在所有位置的运算中，使用32位元有号二进制定点数运算，其小数位数是16位。这个值的单位长度对应印刷中的点。TeX font metric档案使用32位元定点数，其中有12位小数。
- Tremor (软件)、Toast (GSM)和MPEG声音解码器都是软件的函式库，可以针对Vorbis、Full Rate和MP3的声音档。这些解码都使用定点运算，因为许多解码硬件中没有浮点运算器。
- WavPack无损声音压缩器也是使用定点运算。此决定有一部分是担心不同硬件中对于浮点数的舍入规则差异会破坏压缩器的无损特性。
- Nest Labs Utilities library提供少量针对定点数的宏以及函数，特别是用在处理和感测器取样以及感测器输出有关的数字。
- OpenGL ES 1.x规格中包括定点数，因为这是针对嵌入式系统的API，不一定有浮点运算器。
- Dc和Bc程式都是高精度计算计算器，但只支扰（使用者定义的）定点小数。
- Fractint表达数字的方式类似Q格式的定点数，可以加速在Intel 80386或Intel 80486SX等没有浮点运算器的旧电脑。
- 毁灭战士是Id Software所出的第一人称射击游戏，其非整数运算（例如地图系统、地理、算绘、玩家移动等）都是用16.16的定点格式。为了相容性考量，此表达法仍用在现代的Doom source port中。
- 有时在储存或处理影像或是图像帧时，会使用定点数。有单指令流多数据流单元，主要针对影像处理的处理器，一般都有适用于定点数的指令。
- Microsoft Azure量子计算机的Q Sharp程式语言，是实现量子闸用的语言，其中包括标准的数值函数库，可以在量子位元暂存器上处理定点运算。