如何将浮点数与 int 正确相乘并获得仅受有效数字影响的结果

How to correctly multiply a float with an int and get a result only influenced by significant digits?

本文关键字:有效数字 结果 影响 浮点数 int      更新时间:2023-10-16

我有代码在浮点数(表示秒(和 int64(代表纳秒(之间转换,从浮点数中获取小数位6

int64_t nanos = f * 1000000000LL;

但是,存储在浮点数中的许多十进制值无法在二进制浮点数中准确表示,因此当我的浮点数14.2f时,我得到的结果14199999488.目前我通过计算基数点之后的大量数字来解决此问题

const float logOfSecs = std::log10(f);
int precommaPlaces = 0;
if(logOfSecs > 0) {
   precommaPlaces = std::ceil(logOfSecs);
}
int postcommaPlaces = 7 - precommaPlaces;
if(postcommaPlaces < 0) {
   postcommaPlaces = 0;
}

然后将浮点数打印成字符串,让Qt正确舍入浮点数。然后,我将字符串解析为逗号前后的整数,并用整数算术将它们乘以。

const QString valueStr = QString::number(f, 'f', postcommaPlaces);
qint64 nanos = 0;
nanos += valueStr.section(".", 0, 0).toLongLong() * 1000000000LL;
if(postcommaPlaces) {
   nanos += valueStr.section(".", 1).toLongLong() * 
     std::pow(10.0, 9 - postcommaPlaces);
}

这工作正常,但我想知道是否有更好、也许更快的方法来做到这一点?

通过将值存储在已经造成损害的float中,您已经丢失了原始数字。您可以猜测可能已预期的值,然后进行舍入,或者如果您只是尝试为用户显示一个值,则可以舍入到较小的小数位数。

相反,您可以通过在整个代码库中使用定点int64_t表示来解决所有这些问题,从不与float相互转换,并避免在每次转换期间丢弃精度。

例如,

如果您想四舍五入到小数点后一位

#include <iostream>
int main()
{
    float f = 14.2f;
    long long n = f * 1000000000LL;
    std::cout << "float: " << n << 'n';
    n = (f + 0.05) * 10;
    n *= 100000000LL;
    std::cout << "rounded: " << n << 'n';
    return 0;
}

小数点后两位是(f + 0.005) * 100 , ...,小数点后六位

n = ((long long)((f + 0.0000005) * 1000000)) * 1000LL;

如果要考虑有效数字(所有数字(,则必须首先取log10(f)然后调整小数位的舍入。

但正如@MarkB已经说过的,如果你首先使用int64_t,你根本不需要这个。

如其他答案所述,舍入到任意数量的十进制数字与打印浮点数密切相关。由于正确舍入的算法相当复杂,因此正确的最简单的方法是使用 printf 本身。

请注意,您不一定必须提供任意数量的位数,另一种方法是使用最短的小数点,该小数点将以 2 为基数不变地转换回来。这样的算法用于在Scheme,Java,Python,Squeak/Pharo等中打印浮点数...不幸的是,libm printf 和任何标准 C 库都不兼容。

方案甚至更好,因为它打印 *,当您施加固定数量的数字时,数字并不重要(* 表示任何数字在以 2 为底转换回时都会导致相同的浮点数(。

在本期 http://code.google.com/p/pharo/issues/detail?id=4957 有一个名为 Float-asMinimalDecimalFraction.st 的附件,其中包含 Smalltalk 中用于打印的算法与 Scheme 类似的实现,但输出分数(两个任意长度整数的比率(而不是 ASCII 字符串。

因此,例如,尽管 14.2f 在内部表示为 14.19999980926513671875,但还为时不晚,您可以检索正确舍入到它的最短小数部分是 (142/10(。

在 Smalltalk 中使用此类代码,您的问题的解决方案很简单:

nanos := (floatingPointSeconds asMinimalDecimalFraction * 1e9) rounded.

但是上面的代码使用了精确的算术(1e9是一个整数(和任意长度的整数。

请注意,在浮点数中执行乘法会很糟糕:

nanos := (aFloat * 1e9) asMinimalDecimalFraction rounded.

事实上,尽管 1e9 asFloat 转换是精确的,但其有效数跨越 21 位,因此浮点乘法很可能会累积舍入误差并加剧检索短分数的问题。

虽然在技术上以某种方式回答了这个问题,但由于以下原因,我个人认为上述算法在实用上是不合适的:

  1. 在没有任意精度算术库帮助的情况下,使用低级 C/C++ 指令执行此操作并不是获得结果的最快途径

  2. 它非常有限,因为它不适用于具有多个舍入误差的计算结果(它们在统计上需要许多数字(

  3. 如果您可以简单地避免使用 Float 并使用 nanos int,那就太过分了

尽管如此,知道它的存在总是很高兴......