在C中进行整数乘除运算并将结果存储为整数或浮点数时,精度在哪里以及如何损失?

2nbm6dog  于 2023-06-21  发布在  其他
关注(0)|答案(4)|浏览(147)

对于在ESP32上运行的C程序中的计算,我必须以以下方式乘和除以下整数:
150 × 10000 ÷ 155 ÷ 138 × 100 ÷ 220 × 100,它为float变量生成3100.000000,为32位无符号整数生成3100。
我尝试使用以下代码测试https://www.onlinegdb.com/上的计算结果:

int main () {
    float calc = 150 * 10000 / 155 / 138 * 100 / 220 * 100 ;
    printf ( "calc = %f\n", calc ) ;    // 3100.000000

    uint32_t calc0 = 150 * 10000 / 155 / 138 * 100 / 220 * 100 ;
    printf ( "calc0 = %u\n", calc0 ) ;  // 3100
}

其再次产生3100.000000用于浮点数,且3100用于32位无符号整数。
如果我在我的掌上电脑或笔记本电脑上的计算器中输入相同的数字,结果在两种情况下都是3187,555782226。
所以,我在ESP32上有一个准确性损失(如果我没有搞砸公式的话)。(3187−3100)÷3187×100 ~= 2,73 %
差异来自哪里以及如何产生,是否可以在32位微控制器上获得与PC上相同的准确结果?

xqkwcwgp

xqkwcwgp1#

您不会在计算器或移动设备上丢失任何精度。The precise result is 3187.5557822261889583067701...和您的移动设备是近似相当准确。
问题是在表达式150 * 10000 / 155 / 138 * 100 / 220 * 100中,所有的乘法和除法都是在整数之间进行的。即使您使用这个表达式来初始化float,也为时已晚;精度已经丧失。
为了获得更精确的结果,通过添加.f后缀使第一个操作数为float,所有操作都将在浮点数之间进行,然后:

#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

int main(void) {
    float calc = 150.f * 10000 / 155 / 138 * 100 / 220 * 100;
    printf( "calc = %f\n", calc );

    uint32_t calc0 = 150.f * 10000 / 155 / 138 * 100 / 220 * 100;
    // note: PRIu32 expands to the correct format specifier for uint32_t
    printf( "calc0 = %" PRIu32 "\n", calc0 );

    // tip: we can use unsigned long long (ull suffix) and shift all of the
    //      multiplications to the start to minimize precision loss
    //      (unsigned long long is needed to prevent overflow)
    uint32_t calc1 = 150ull * 10000 * 100 * 100 / 155 / 138 / 220; // 1)
    printf( "calc1 = %" PRIu32 "\n", calc1 );
}

图纸:

calc = 3187.555420
calc0 = 3187
calc1 = 3187

1)除了/ 155 / 138 / 120,我们还可以写/ (155ull * 138 * 120)The result is guaranteed to be the same

htrmnn0y

htrmnn0y2#

如果你的表达式只有int项,它会进行int计算,不管它是否被赋值给float(1)。* 赋值 * 对计算本身没有影响。
例如,子表达式150 * 10000 / 155将给予精确的值9677,而不是更精确的值(约为9677.419)。
你需要 * 告诉 * 它以浮点方式进行计算,这可以通过简单地将第一项设置为150.0而不是150来完成。
请记住,这实际上将按照double进行计算(比float的范围和精度更高)。然后,当将其分配给float目标时,将进行任何精度损失调整。如果这是您的平台(可能是有限的)的问题,您仍然可以通过使用150f(2)在计算中使用float
这也是为什么你的“使用uint64_t并先做乘法”(在你的一条评论中)在这里没有帮助。这只影响 final 类型,计算仍然是用int类型完成的,因此不会避免任何溢出。同样,您可以通过在表达式中使用类似(uint64_t)150而不是150来解决这个问题。
(1)表达式operand1 operation operand2的一般规则是,两个操作数首先被修改为具有相同的“公共真实的类型”。
因此,例如,两个int操作数将保持int。对于intlongint将转换为long。结果则是相同类型。这在ISO(C17)标准的6.3.1 Arithmetic operands6.3.1.8 Usual arithmetic conversions下涵盖。前者给出了各种整数类型的秩,后者详细解释了如何执行转换。
重要的是,floatint在进行计算之前会将后者“升级”为float
(2)在不同的网络位置有很多关于EPS 32上浮点性能的讨论,因此double操作可能会有问题,具体取决于您的需求。但是,与所有优化问题一样,您应该“测量,而不是猜测”。然后,您可以明智地做出成本/效益决策,在速度和范围/精度之间进行选择。

jucafojl

jucafojl3#

整数除法将值向零截断。在150 * 10000 / 155的情况下,结果是9677.41935484,但变成9677,然后除以138,这应该是70.126227209,但变成70。随后的乘法放大了误差的幅度。简而言之,整数除法需要精确度。可能这可以通过重新排序计算来减轻,最后进行除法,从而具有更少的乘法,“放大”错误。但要注意溢出。

6tqwzwtp

6tqwzwtp4#

在没有FPU的微控制器中,您通常希望坚持定点算法并手动设置精度。在没有FPU的情况下使用float意味着糟糕的程序性能,因为编译器将通过软件库来处理浮点。忘记这一点,停止接受PC程序员的坏建议。
定点算术不是火箭科学-你需要知道整数限制,小学数学和一点常识:)基本上它的意思是:先乘后除,因为只有在除法过程中才会损失精度,而整数除法会截断所有小数。
在无符号32的情况下,上限是2^32 - 1,或者如果你愿意的话是42.9亿。最坏情况计算在任何时候都不应超过此值。
注意:所有整数常量都有一个类型,对于像150这样的常量,它是int,它的符号是-32,实际上不是一个有用的类型。我们可以用uul后缀所有常量以确保正确的类型,或者我们可以确保其中一个操作数是正确的类型,其中隐式提升将提升另一个操作数。
如果我们想要3位小数,那么我们只需乘以10^3,然后重新排列等式,这样我们就不会在任何地方达到上限:

uint32_t calc = 1000; // 3 decimals
    calc *= 150;
    calc *= 10000;
    calc /= 155;
    calc *= 100;
    calc /= 138;
    calc *= 100;
    calc /= 220;
printf("%lu.%lu", calc/1000, calc%1000);

输出:3187.555
当然,在每次除法时,你会损失一点精度,因为在这个例子中,计算的上限是3位小数。并且最终结果是截断的,而不是四舍五入的。如果精度很重要,那么您可以升级到64位算术。这在32位MCU上较慢,但它仍然比软件浮点快得多。事实证明,可能更准确:

uint64_t calc = 10000000000ull; // 10 decimals
    calc *= 150;
    calc *= 10000;
    calc /= 155;
    calc *= 100;
    calc /= 138;
    calc *= 100;
    calc /= 220;
printf("%llu.%llu", calc/10000000000ull, calc%10000000000ull);

输出:3187.5557822261

相关问题