strtof()的实现,浮点乘法和尾数舍入问题
Implementation of strtof(), floating-point multiplication and mantissa rounding issues
这个问题与其说是关于C,不如说是关于算法。我需要实现 strtof()
函数,它的行为与 GCC 完全相同——并且从头开始(没有 GNU MPL 等)。
让我们跳过检查,只考虑正确的输入和正数,例如345.6e7。我的基本算法是:
- 将数字拆分为分数和整数指数,因此对于 345.6e7,分数为 3.456e2,指数为 7。
- 创建一个浮点指数。为此,我使用了这些表:
static const float powersOf10[] = {
1.0e1f,
1.0e2f,
1.0e4f,
1.0e8f,
1.0e16f,
1.0e32f
};
static const float minuspowersOf10[] = {
1.0e-1f,
1.0e-2f,
1.0e-4f,
1.0e-8f,
1.0e-16f,
1.0e-32f
};
并获取浮点指数作为整数指数中相应位的乘积,例如7 = 1+2+4 => float_exponent = 1.0e1f * 1.0e2f * 1.0e4f.
- 将分数乘以浮动指数 return 结果。
第一个问题来了:由于我们做了很多次乘法运算,每次乘法运算的结果都四舍五入,所以误差有点大。因此,我决定深入研究浮点乘法算法并自己实现它:一个函数接受多个浮点数(在我的例子中 - 最多 7 个)并在位级别上将它们相乘。假设我有 uint256_t
类型来适合尾数乘积。
现在,第二个问题:将尾数乘积舍入到23位。我尝试了几种舍入方法(四舍五入,冯诺依曼舍入 - a small article about them),但没有一种方法可以为所有测试数字给出正确的结果。其中一些真的让我感到困惑,比如这个:
7038531e-32。 GCC 的 strtof()
returns 0x15ae43fd,所以正确的无偏尾数是 2e43fd。我选择 7.038531e6(偏置尾数 d6cc86)和 1e-32(b.m.cfb11f)的乘法。由此产生的二进制形式的无偏尾数是
( 47)0001 ( 43)0111 ( 39)0010 ( 35)0001
( 31)1111 ( 27)1110 ( 23)1110 ( 19)0010
( 15)1011 ( 11)0101 ( 7)0001 ( 3)1101
我必须四舍五入到 23 位。但是,通过所有舍入方法我都必须将它四舍五入,结果我会得到 2e43fe - 错了!因此,对于这个数字,获得正确尾数的唯一方法就是将其切碎——但切碎对其他数字不起作用。
经过无数个晚上的努力,我的问题是:
这种 strtof() 方法是否正确? (我知道 GCC 为此使用 GNU MPL,并试图深入了解它。但是,尝试复制 MPL 的实现将需要移植整个库,这绝对不是我想要的)。也许这种先拆分后乘的算法不可避免地容易出错?我做了一些其他的小技巧(例如,为浮点范围内的所有整数指数创建指数表),但它们导致更多的转换失败。
如果是这样,我在四舍五入时是不是错过了什么?我也是这么想的,但是这个7038531e-32这个号码彻底把我搞糊涂了
如果我想尽可能精确,我通常会这样做(但我通常会进行反向操作 float -> text):
只使用整数(没有浮点数)
如您所知,浮点数是整数尾数按整数指数移位,因此不需要浮点数。
为了构建最终的 float 数据类型,您可以使用简单的 union
,其中包含 float 和 32 位无符号整数......或指向相同地址的此类类型的指针。
这将避免完全适合的数字的舍入误差,并减少不太适合的数字的误差。
使用十六进制数
您可以将 运行 上的十进制数文本转换为对应的十六进制数(仍然是文本),从那里创建尾数和指数整数很简单。
此处:
- How to convert a gi-normous integer (in string format) to hex format? (C#)
是 dec2hex
和 hex2dec
对文本进行数字转换的 C++ 实现示例
转换时使用更多位作为尾数
对于这样的任务和单精度浮点数,我通常使用 2 或 3 个 32 位 DWORD 作为 24 位尾数,以便在乘法后仍然保持一定的精度如果你想要精确,你必须处理 128+24 位对于数字的整数和小数部分,因此顺序为 5x32 位数字。
更多信息和灵感见(反向操作):
你的代码正好相反(很多部分都是相似的)
因为我 post 我制作了更高级的版本,可以像 printf
一样识别格式,支持更多的数据类型和更多,而不使用任何库(但是它的代码约为 22.5 KB)。我需要它用于 MCU,因为打印的 GCC 实现在那里不是很好......
这个问题与其说是关于C,不如说是关于算法。我需要实现 strtof()
函数,它的行为与 GCC 完全相同——并且从头开始(没有 GNU MPL 等)。
让我们跳过检查,只考虑正确的输入和正数,例如345.6e7。我的基本算法是:
- 将数字拆分为分数和整数指数,因此对于 345.6e7,分数为 3.456e2,指数为 7。
- 创建一个浮点指数。为此,我使用了这些表:
static const float powersOf10[] = {
1.0e1f,
1.0e2f,
1.0e4f,
1.0e8f,
1.0e16f,
1.0e32f
};
static const float minuspowersOf10[] = {
1.0e-1f,
1.0e-2f,
1.0e-4f,
1.0e-8f,
1.0e-16f,
1.0e-32f
};
并获取浮点指数作为整数指数中相应位的乘积,例如7 = 1+2+4 => float_exponent = 1.0e1f * 1.0e2f * 1.0e4f.
- 将分数乘以浮动指数 return 结果。
第一个问题来了:由于我们做了很多次乘法运算,每次乘法运算的结果都四舍五入,所以误差有点大。因此,我决定深入研究浮点乘法算法并自己实现它:一个函数接受多个浮点数(在我的例子中 - 最多 7 个)并在位级别上将它们相乘。假设我有 uint256_t
类型来适合尾数乘积。
现在,第二个问题:将尾数乘积舍入到23位。我尝试了几种舍入方法(四舍五入,冯诺依曼舍入 - a small article about them),但没有一种方法可以为所有测试数字给出正确的结果。其中一些真的让我感到困惑,比如这个:
7038531e-32。 GCC 的 strtof()
returns 0x15ae43fd,所以正确的无偏尾数是 2e43fd。我选择 7.038531e6(偏置尾数 d6cc86)和 1e-32(b.m.cfb11f)的乘法。由此产生的二进制形式的无偏尾数是
( 47)0001 ( 43)0111 ( 39)0010 ( 35)0001
( 31)1111 ( 27)1110 ( 23)1110 ( 19)0010
( 15)1011 ( 11)0101 ( 7)0001 ( 3)1101
我必须四舍五入到 23 位。但是,通过所有舍入方法我都必须将它四舍五入,结果我会得到 2e43fe - 错了!因此,对于这个数字,获得正确尾数的唯一方法就是将其切碎——但切碎对其他数字不起作用。
经过无数个晚上的努力,我的问题是:
这种 strtof() 方法是否正确? (我知道 GCC 为此使用 GNU MPL,并试图深入了解它。但是,尝试复制 MPL 的实现将需要移植整个库,这绝对不是我想要的)。也许这种先拆分后乘的算法不可避免地容易出错?我做了一些其他的小技巧(例如,为浮点范围内的所有整数指数创建指数表),但它们导致更多的转换失败。
如果是这样,我在四舍五入时是不是错过了什么?我也是这么想的,但是这个7038531e-32这个号码彻底把我搞糊涂了
如果我想尽可能精确,我通常会这样做(但我通常会进行反向操作 float -> text):
只使用整数(没有浮点数)
如您所知,浮点数是整数尾数按整数指数移位,因此不需要浮点数。
为了构建最终的 float 数据类型,您可以使用简单的
union
,其中包含 float 和 32 位无符号整数......或指向相同地址的此类类型的指针。这将避免完全适合的数字的舍入误差,并减少不太适合的数字的误差。
使用十六进制数
您可以将 运行 上的十进制数文本转换为对应的十六进制数(仍然是文本),从那里创建尾数和指数整数很简单。
此处:
- How to convert a gi-normous integer (in string format) to hex format? (C#)
是
dec2hex
和hex2dec
对文本进行数字转换的 C++ 实现示例转换时使用更多位作为尾数
对于这样的任务和单精度浮点数,我通常使用 2 或 3 个 32 位 DWORD 作为 24 位尾数,以便在乘法后仍然保持一定的精度如果你想要精确,你必须处理 128+24 位对于数字的整数和小数部分,因此顺序为 5x32 位数字。
更多信息和灵感见(反向操作):
你的代码正好相反(很多部分都是相似的)
因为我 post 我制作了更高级的版本,可以像 printf
一样识别格式,支持更多的数据类型和更多,而不使用任何库(但是它的代码约为 22.5 KB)。我需要它用于 MCU,因为打印的 GCC 实现在那里不是很好......