二进制编码的十进制






4.86/5 (14投票s)
一个二进制编码的十进制类和 ODBC 接口。
引言
计算机以二进制计算数字。我们常常忘记这一点,因为数学机器的错觉非常有说服力。忘记二进制舍入误差,并假装计算是精确的,这要容易得多。
唉!对于会计和簿记等枯燥的目的来说,这并不奏效。在这里,1美分的舍入误差就足以让一位会计师尖叫着要解释,并引发一项价值数百万美元的调查。正是出于会计原因,数据库才有了像 NUMERIC
和 DECIMAL
这样的特殊数据类型,它们是精确的数字格式。这与 FLOAT
和 REAL
等近似数据类型形成对比。
在 C++ 语言中,内置数据类型“double
”(IEEE 754)是基数为 2 的,所以它也是一个近似的 datatype
。尽管 Intel x386 处理器有一个‘bcd
’类型的运算符来纠正二进制计算的影响,但如今没有 C++ 编译器具有内置的精确数字数据类型。(Borland C++ 2.0 是最后一个拥有这个的!)
解决方案是将这些数字存储在所谓的“二进制编码的十进制”格式中。简称 BCD。在此类实现中,不会发生舍入误差,因为使用了额外的位来表示十进制数字而不是二进制数字。
缺乏二进制编码的十进制 datatype
使得在 C++ 中进行会计和簿记计算变得繁琐。此外,将 NUMERIC
和 DECIMAL
数字存入和取出数据库需要进行冗长的计算来转换数字,从而消耗宝贵的 CPU 周期。
ODBC 的案例
ODBC(开放数据库连接)标准有一个数据结构(SQL_NUMERIC_STRUCT
)来传输 NUMERIC
和 DECIMAL
数字的数据。这些 datatype
的数据通常在 ODBC 应用程序中被绑定并用作 string
。对于大多数程序员来说,在日常工作中,将数据以基数为 256 的数字 struct
进行二进制传输通常是一个巨大的挑战。
关于“BCD 和 ODBC 标准”的章节解释了此类 bcd 如何解决这个问题。
BCD 的前身
曾经(并且仍然存在!)存在一些此 bcd 类的先驱。
4GL 编程语言
与特定数据库平台绑定的第四代语言(例如 INFORMIX-4GL
)具有内置的 DECIMAL
数据类型。在该平台上,二进制编码的十进制计算是默认的,这使得它非常适合构建会计软件并将结果存储在数据库中。
Borland C++ 编译器
在 Borland 将其 C++ 编译器出售给 Embarcadero 之前,它具有内置的‘bcd
’ datatype
,可以直接使用,就像使用‘int
’或‘double
’一样。据我所知,这个 datatype
在该编译器的后期版本中已被删除。
Microsoft 从未在其 C++ 语言实现中捆绑过‘bcd
’ datatype
。该语言本身,即使通过 ISO 标准过程,也从未具备过这样的 datatype
。因此,Borland 在这方面是独一无二的。
整数编码的十进制
为了进行精确的数值计算,我的第一次尝试是实现一个存储数字为整数格式的数字类。小数点前 4 个整数,小数点后 4 个整数。这个实现——称为整数编码的十进制——允许小数点前和小数点后有 16 位小数。足以处理一个大型跨国公司或一个小国家的簿记。😊
尽管像加法和减法这样的经典运算符在此格式中易于实现,但仍有许多不足之处。一旦我们想实现更多的数学运算符,这种实现就会变得麻烦,而且正如我们稍后将看到的:速度很慢。
任意浮点数
在数学领域进行了一些搜索后,我找到了 Henrik Vestermark 的任意浮点数(AFP)类。该库采用单独存储数学尾数和分数部分的方法。分数部分存储在字符数组中,并在每个数学运算中进行解释。因此,它是一个精确的二进制编码的十进制实现。
该库的源代码包含在 BCD 项目中。但也可以参考“Numerical Methods at Work”网站,因为自从我从它那里获得这个想法以来,这个项目已经发展了。
该库的缺点是(除了性能问题)其格式不易转码为 NUMERIC
和 DECIMAL
等数据库格式。
两全其美
这两种方法(整数编码的十进制和任意浮点数)共同促成了本文提出的二进制编码的十进制类的设计。它将指数和尾数分开存储。然而,尾数以一组整数的形式存储。在当前配置中,使用 5 个整数,每个整数表示 8 位小数。从而允许尾数为 40 位。这足以处理最苛刻的数据库实现(Oracle 具有 38 位)。
整数可以至少容纳 9 位小数,因为一个正的 32 位“int
”或“long
”可以容纳到 2.147.483.647
。这使得它能够容纳 8 位小数,并且仍然有一个数字可以保存任何进位或借位数字,当遍历这些整数的数组时。
BCD 数据类型
BCD(二进制编码的十进制)数据类型是考虑到数据库的数字和十进制数据类型而构建的。二进制编码的十进制数字是精确数字,由于计算机 CPU(中央处理器)的二进制性质而不会产生舍入误差。这使得 bcd 数据类型特别适合财务和簿记目的。
BCD 计算在计算机科学中已经存在相当长的时间,并且有多种形式。此类 BCD 专门设计用于与 ODBC 数据库适配器共存。有关更多信息,请参阅“BCD 和 ODBC 标准”一章。
构造和初始化
您可以从几乎所有基本的 C++ 数据类型构造一个 bcd,同时初始化 bcd。这适用于字符、整数、长整型、64 位整数、浮点数和双精度数。
// Made from an integer and a floating point number
bcd num1(2);
bcd num2(4.0);
bcd num3 = num1 + num2; // will become 6
也适用于 string
和其他 bcd
数字。
// Made from a string and a different bcd number
bcd num4(“7.25”);
bcd num5(num3);
bcd num6 = num4 + num5; // will become 13.5
以下是所有构造函数类型的一个完整列表
- 默认构造函数(初始化数字为零(‘0.0’))
- 从字符数字构造(-127 到 +127)
- 从无符号字符数字构造(0 到 255)
- 从短整型构造(-32767 到 32767)
- 从无符号短整型构造(0 到 65535)
- 从整数构造(-2147483647 到 2147483647)
- 从无符号整数构造(0 到 4294967295)
- 从 64 位整数构造(-9223372036854775807 到 9223372036854775807)
- 从无符号 64 位整数构造(0 到 18446744073709551615)
- 从另一个 bcd 构造
- 从 CString 类型(MFC)的
string
构造 - 从“
const char *
”类型的string
构造 - 从
SQL_NUMERIC_STRUCT
(如 ODBC 标准所示)构造
常量
在 bcd datatype
中定义了三个常量。这些常量是
PI
:众所周知的圆周率LN2
:2 的自然对数LN10
:10 的对数
它们显示为“const bcd
”数字,可以这样使用。这里有一些简单的例子来展示它们的用法。
// Numbers made up with the help of constants
bcd ratio = 2 * PI();
bcd quart = bcd::PI() / bcd(2);
赋值
其他 bcd、整数、双精度数和字符串可以赋值给 bcd 数字。也就是说,您可以使用标准的‘=’赋值运算符或与标准数学运算组合的运算符‘+=’、‘-=’、‘*=’、‘/=’和‘%=’。由位运算符组合而成的赋值运算符,如‘|=’或‘&=’,在 bcd
类中没有逻辑对应项,因为位运算对二进制编码的十进制数没有逻辑意义。
// Calculation with assignments
bcd a = SomeFunc();
a += 2;
a *= b; // a = b + (2 * SomeFunc)
增量和减量
前缀和后缀的增量和减量都可以与 bcd
一起使用,就像与任何整数一样。
// Calculation with prefix and postfix in- and decrements
bcd a = ++b;
bcd c = a--;
运算符
bcd
类实现了标准的数学运算符‘+’(加法)、‘-‘(减法)、‘*’(乘法)、‘/’(除法)和‘%’(模)。
由于此类设计用于簿记,因此它是此类操作的“基础”。在实际应用中,百分之八十以上的计算都通过这些运算符完成。
这是一个典型的例子
// Finding an average price from a std::vector of objects
// Where GetPrice() and GetVAT() both return a bcd value
bcd total;
for(auto& obj : objectlist)
{
total += obj.GetPrice() + obj.GetVAT();
}
bcd average = total / (int)objectlist.count();
if(average > 400.0)
{
ReportAverageToHigh(average);
}
比较
bcd
类实现了所有典型的比较运算符,如相等(==)、不相等(!=)、小于(<)、小于等于(<=)、大于(>)和大于等于(>=)。
例如,请参阅上一段,我们在其中报告了过高的平均值。
数学函数
C 库包含一些仅在‘double
’基本 datatype
中实现的数学函数。例如,“pow
”用于取幂。这些函数被实现为统计函数,以 bcd
作为参数。静态数学函数包括
除了 static
函数之外,它们还作为 bcd
类的方法实现。因此,“pow
”有一个对称的方法‘Power
’。
这里有两个例子,它们执行完全相同的操作
// Calculate the side of a square
bcd surface = GetSquareSurfaceArea();
bcd side = surface.SquareRoot();
和
// Calculate the side of a square
bcd surface = GetSquareSurfaceArea();
bcd side = sqrt(surface);
注意:重载数学函数是为了方便移植现有的 double
代码转换为 bcd
。
三角函数
标准 C 的三角函数已为 bcd
类重载,就像标准数学函数一样。与标准三角函数一样,数字是以弧度为单位测量的角度。存在以下函数
这里有两个例子,它们执行完全相同的操作
// Calculate the height of a given wave
bcd waveHeight = GetSignal().Sine();
和
// Calculate the height of a given wave
bcd waveHeight = sin(GetSignal());
转换
可以将 bcd
转换为‘某某’。这个‘某某’是 C++ 语言的基本数据类型。大多数方法的名字都是“AsXXXX
”,其中 XXXX
表示我们想要的类型。存在以下方法
这是一个返回工程数字 string
(以 10 指数格式)的计算示例。
// Return a calculation in a IEEE number string
CString GetCalculation()
{
bcd number1 = SomeFunction();
bcd number2 = AnotherFunction();
bcd number3 = number1.Power(number2);
// Something like “5.6773E-03”
return number3.AsString(Engineering,false);
}
字符串显示
数字可以显示为 string
。它们的显示方式取决于我们使用数字的应用程序。与簿记应用程序相比,科学或工程应用程序的显示方式可能会有很大的不同。这些差异大致定义为
- 在簿记应用程序中,我们倾向于显示带有小数点和一个或多个千位分隔符的数字。我们显示定义数量的小数位数,并舍去其余的小数位数;
- 在工程应用程序中,我们倾向于打印精确数字,只带有一个小数点。如果数字变得太大(或太小),我们会切换到以 10 的幂的指数显示。
- 在这两种情况下,我们总是用负号(-)打印负数,但我们可以选择也打印正号(+);
- 小数点和千位分隔符由机器的当前系统区域设置定义,因此也由桌面当前使用的语言定义。
以下是两种数字显示方式的一些示例
文件读写
应用程序可能需要将信息写入二进制文件。因此,有两种方法可以与二进制文件集成。第一种(WriteToFile(FILE*)
)将 bcd
数字写入文件。第二种(ReadFromFile(FILE*)
)将 bcd
数字从该文件读回。bcd
数字的所有主要事实信息都已存储。
文件中的任何干扰(哦,抱歉:文件)都会导致错误,这意味着整个数字将被存储或读回,或者会发生错误。有关 bcd
的存储格式的详细信息,请参见实现。
在文件中存储和检索 bcd
数字也是网络无关和字节序(小端/大端)无关的,这意味着您可以以可移植的方式存储和检索数字。
信息和其他方法
存在许多尚未讨论的方法。它们提供有关 bcd
数字的某个属性的信息或执行基本操作。以下是其余列表
错误处理
错误处理通过抛出 StdException
来完成。此异常与 MS-Windows C++ 安全异常处理集成,以至于关键错误(如 null
指针引用和除零错误)**不会**获得不同的异常处理——例如,停止应用程序——而是集成到异常抛出中。
以下是 bcd
类中所有错误的列表。它们的描述应不言自明
增强和改进
bcd
类可以轻松增强。您可以通过在 mantissa 数组中使用更多整数来简单地扩展 mantissa 中的位数。请参阅类接口定义开头的常量“bcdDigits
”和“bcdLength
”。
可以轻松地将额外的方法和/或数据、运算符、流接口(如 std::iostream
)添加到此类中。
bcd
项目附带一个单元测试模块 DLL。单元测试的目标当然是在您扩展类的同时测试其他功能的正确工作。
只需在 Visual Studio 中打开测试资源管理器(从菜单“测试”/“运行所有测试”),然后检查所有单元测试是否都“正常运行”。
BCD 和 ODBC 标准
现在数学计算已牢固确立,我们可以转向 bcd
数字与 ODBC 驱动程序的组合使用。二进制数据以 SQL_NUMERIC_STRUCT
的形式流进流出数据库的 ODBC 驱动程序。该 struct
支持现代 ISO:9075 合规 SQL 数据库的 NUMERIC
和 DECIMAL datatype
。
直接绑定到数据库查询中的 DECIMAL
或 NUMERIC
列将导致在内存中检索 SQL_NUMERIC_STRUCT
。在 update
或 insert
语句中更改和使用该 struct
将使用该 struct
的内容并将其传输到我们的数据库记录。
但是,当我们获取数据时会发生什么。查看一些开源数据库实现(如 MySql、MariaDB、Firebird 或 PostgreSQL)的源代码,会发现大多数 odbc 驱动程序只是将 string
转换为 SQL_NUMERIC_STRUCT
。编写此类 dataclass
时,这些转换相当复杂、耗时且容易出错。近年来,情况有所改善,但是……
- 此转换仅转换带有小数点的标准数字格式。无法转换指数数字。
NUMERIC
和DECIMAL
的使用仍然存在很多困惑。在 stackoverflow 平台上查找答案,即使是经验丰富的程序员也选择让数据库将数据转换为字符串,然后从查询中提取该string
数据。
BCD 类被设计为可以轻松地与 SQL_NUMERIC_STRUCT
相互转换。使用以下两个方法
bcd::SetValueNumeric(SQL_NUMERIC_STRUCT*);
bcd::AsNumeric(SQL_NUMERIC_STRUCT*);
数据直接转换为 ODBC 绑定区域并传输到数据库。这些转换是对尾数和符号位的简单迭代复制。
主要优势
这里的关键因素是我们无需先将其转换为 string
再转换回我们可以进行计算的格式,就可以直接使用我们的 NUMERIC
和 DECIMAL
数字。反之亦然:我们可以直接计算结果并将其存储在数据库中,而无需将所有内容都转换为 string
并指示数据库将其转换回相同的数据!
SQLComponents
bcd
类的主要应用在于 SQLComponents
库。这是一个围绕 ODBC 驱动程序的库。您可以在 https://github.com/edwig/SQLComponents 找到该库。
在该库中,所有 datarow
都绑定到 SQLRecord
对象。每个记录的列依次绑定到 SQLVariant
类。
SQLVariant
类充当从数据库行获取的所有 datatype
的一种变量占位符。当然:datatype
之一就是 bcd
类。
SQLComponents
数据库使得使用任何给定的 ODBC 驱动程序进行编程更加容易。它已在 Oracle、MS-SQLServer、MySQL、PostgreSQL、MS-Access 和 IBM-Informix 上进行了测试。
Open ODBCQuerytool
除了一些业务应用程序外,唯一使用 SQLComponents
和 bcd 类的杀手级应用是 Open ODBC-Querytool。您可以在 github 上找到此查询工具:https://github.com/edwig/ODBCQueryTool,并在 sourceforge 上找到:https://sourceforge.net/projects/odbcquerytool/。从最后一个链接来看,它在过去几年里下载量已超过 50,000 次。
性能测量
为了能够测量我的实现的性能,我设计了一个测试程序,该程序以可配置的次数‘n
’执行任意数量的计算。当将‘n
’设置为例如 1000 次时,计算的长度足以用高性能计数器进行测量,例如 MS-Windows 内核的“QueryPerformanceCounter”。
该测试程序将每次操作的结果与 MS-Windows 桌面计算器“calc.exe”的结果进行比较,并显示四种实现的性能结果
- C++ 内置“
double
” - 任意浮点数
- 整数编码的十进制
- 二进制编码的十进制
测试程序的典型输出如下所示
Testing the function [log10] for a total of [1000] iterations:
Input: 98765432109876543210.123456789012345678901234567890
Type Time Value
------ ---------- ------------------------------------------------------
calc 0.000000 +19.994604968162151965673558368195
double 0.000005 +19.994604968162150
afp 0.982142 +19.99460496816215196567355836819543212297
icd 0.191501 +19.9946049681621519656735583681954349795885
bcd 0.050899 +19.9946049681621519656735583681954321229
在此示例中,我们看到“log10
”函数(以 10 为底的对数)的结果。正如我们所见,对于每种实现(‘double’除外 ☹),结果至少精确到 32 位小数。
BCD 实现需要 0.05 秒进行一千次迭代:每次 50 微秒。比双精度计算的 50 纳秒长得多。但精度要高得多!
示例程序中的 BCD 解决方案默认运行此测试。
这是测试运行开始的屏幕截图
这是测试运行结束时的输出样本
在一次 1000 次迭代模式的测试运行中,在现代 Intel Core i7-7700K CPU 和 ASUS Z270 主板上,我们可以比较所有数学函数的计时。以下是典型的最终结果,显示在一个表中
结论
从上面的性能表可以看出,最佳性能当然是内置双精度数据类型。但那是在舍入误差等情况下的。在其他解决方案(AFP 解决方案的直接 8 位 BCD、整数编码的十进制和 bcd 类)中,bcd 在除一项(加法)外的所有计算类别中都是赢家。在 bcd 具有最高性能的情况下,它可以比其他方案快百分之几到惊人的 20 或 50 倍,甚至更高。
Github
此项目也可以在 https://github.com/edwig/bcd 上找到。
历史
- 2019 年 12 月 19 日:本文的第一个版本