65.9K
CodeProject 正在变化。 阅读更多。
Home

二进制编码的十进制

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (14投票s)

2019年12月19日

MIT

15分钟阅读

viewsIcon

18648

downloadIcon

407

一个二进制编码的十进制类和 ODBC 接口。

引言

计算机以二进制计算数字。我们常常忘记这一点,因为数学机器的错觉非常有说服力。忘记二进制舍入误差,并假装计算是精确的,这要容易得多。

唉!对于会计和簿记等枯燥的目的来说,这并不奏效。在这里,1美分的舍入误差就足以让一位会计师尖叫着要解释,并引发一项价值数百万美元的调查。正是出于会计原因,数据库才有了像 NUMERICDECIMAL 这样的特殊数据类型,它们是精确的数字格式。这与 FLOATREAL 等近似数据类型形成对比。

在 C++ 语言中,内置数据类型“double”(IEEE 754)是基数为 2 的,所以它也是一个近似的 datatype。尽管 Intel x386 处理器有一个‘bcd’类型的运算符来纠正二进制计算的影响,但如今没有 C++ 编译器具有内置的精确数字数据类型。(Borland C++ 2.0 是最后一个拥有这个的!)

解决方案是将这些数字存储在所谓的“二进制编码的十进制”格式中。简称 BCD。在此类实现中,不会发生舍入误差,因为使用了额外的位来表示十进制数字而不是二进制数字。

缺乏二进制编码的十进制 datatype 使得在 C++ 中进行会计和簿记计算变得繁琐。此外,将 NUMERICDECIMAL 数字存入和取出数据库需要进行冗长的计算来转换数字,从而消耗宝贵的 CPU 周期。

ODBC 的案例

ODBC(开放数据库连接)标准有一个数据结构(SQL_NUMERIC_STRUCT)来传输 NUMERICDECIMAL 数字的数据。这些 datatype 的数据通常在 ODBC 应用程序中被绑定并用作 string。对于大多数程序员来说,在日常工作中,将数据以基数为 256 的数字 struct 进行二进制传输通常是一个巨大的挑战。

关于“BCD 和 ODBC 标准”的章节解释了此类 bcd 如何解决这个问题。

BCD 的前身

曾经(并且仍然存在!)存在一些此 bcd 类的先驱。

4GL 编程语言

与特定数据库平台绑定的第四代语言(例如 INFORMIX-4GL)具有内置的 DECIMAL 数据类型。在该平台上,二进制编码的十进制计算是默认的,这使得它非常适合构建会计软件并将结果存储在数据库中。

Borland C++ 编译器

在 Borland 将其 C++ 编译器出售给 Embarcadero 之前,它具有内置的‘bcddatatype,可以直接使用,就像使用‘int’或‘double’一样。据我所知,这个 data­type 在该编译器的后期版本中已被删除。

Microsoft 从未在其 C++ 语言实现中捆绑过‘bcddatatype。该语言本身,即使通过 ISO 标准过程,也从未具备过这样的 datatype。因此,Borland 在这方面是独一无二的。

整数编码的十进制

为了进行精确的数值计算,我的第一次尝试是实现一个存储数字为整数格式的数字类。小数点前 4 个整数,小数点后 4 个整数。这个实现——称为整数编码的十进制——允许小数点前和小数点后有 16 位小数。足以处理一个大型跨国公司或一个小国家的簿记。😊

尽管像加法和减法这样的经典运算符在此格式中易于实现,但仍有许多不足之处。一旦我们想实现更多的数学运算符,这种实现就会变得麻烦,而且正如我们稍后将看到的:速度很慢。

任意浮点数

在数学领域进行了一些搜索后,我找到了 Henrik Vestermark 的任意浮点数(AFP)类。该库采用单独存储数学尾数和分数部分的方法。分数部分存储在字符数组中,并在每个数学运算中进行解释。因此,它是一个精确的二进制编码的十进制实现。

该库的源代码包含在 BCD 项目中。但也可以参考“Numerical Methods at Work”网站,因为自从我从它那里获得这个想法以来,这个项目已经发展了。

该库的缺点是(除了性能问题)其格式不易转码为 NUMERICDECIMAL 等数据库格式。

两全其美

这两种方法(整数编码的十进制和任意浮点数)共同促成了本文提出的二进制编码的十进制类的设计。它将指数和尾数分开存储。然而,尾数以一组整数的形式存储。在当前配置中,使用 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。它们的显示方式取决于我们使用数字的应用程序。与簿记应用程序相比,科学或工程应用程序的显示方式可能会有很大的不同。这些差异大致定义为

  1. 在簿记应用程序中,我们倾向于显示带有小数点和一个或多个千位分隔符的数字。我们显示定义数量的小数位数,并舍去其余的小数位数;
  2. 在工程应用程序中,我们倾向于打印精确数字,只带有一个小数点。如果数字变得太大(或太小),我们会切换到以 10 的幂的指数显示。
  3. 在这两种情况下,我们总是用负号(-)打印负数,但我们可以选择也打印正号(+);
  4. 小数点和千位分隔符由机器的当前系统区域设置定义,因此也由桌面当前使用的语言定义。

以下是两种数字显示方式的一些示例

文件读写

应用程序可能需要将信息写入二进制文件。因此,有两种方法可以与二进制文件集成。第一种(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 数据库的 NUMERICDECIMAL datatype

直接绑定到数据库查询中的 DECIMALNUMERIC 列将导致在内存中检索 SQL_NUMERIC_STRUCT。在 updateinsert 语句中更改和使用该 struct 将使用该 struct 的内容并将其传输到我们的数据库记录。

但是,当我们获取数据时会发生什么。查看一些开源数据库实现(如 MySql、MariaDB、Firebird 或 PostgreSQL)的源代码,会发现大多数 odbc 驱动程序只是将 string 转换为 SQL_NUMERIC_STRUCT。编写此类 dataclass 时,这些转换相当复杂、耗时且容易出错。近年来,情况有所改善,但是……

  1. 此转换仅转换带有小数点的标准数字格式。无法转换指数数字。
  2. NUMERICDECIMAL 的使用仍然存在很多困惑。在 stackoverflow 平台上查找答案,即使是经验丰富的程序员也选择让数据库将数据转换为字符串,然后从查询中提取该 string 数据。

BCD 类被设计为可以轻松地与 SQL_NUMERIC_STRUCT 相互转换。使用以下两个方法

  • bcd::SetValueNumeric(SQL_NUMERIC_STRUCT*);
  • bcd::AsNumeric(SQL_NUMERIC_STRUCT*);

数据直接转换为 ODBC 绑定区域并传输到数据库。这些转换是对尾数和符号位的简单迭代复制。

主要优势

这里的关键因素是我们无需先将其转换为 string 再转换回我们可以进行计算的格式,就可以直接使用我们的 NUMERICDECIMAL 数字。反之亦然:我们可以直接计算结果并将其存储在数据库中,而无需将所有内容都转换为 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”的结果进行比较,并显示四种实现的性能结果

  1. C++ 内置“double
  2. 任意浮点数
  3. 整数编码的十进制
  4. 二进制编码的十进制

测试程序的典型输出如下所示

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 日:本文的第一个版本
© . All rights reserved.