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

Mar*_*tov 3 c gcc esp32

对于在 ESP32 上运行的 C 程序中的计算,我必须按以下方式乘以和除以以下整数:

\n

150 \xc3\x97 10000 \xc3\xb7 155 \xc3\xb7 138 \xc3\x97 100 \xc3\xb7 220 \xc3\x97 100对于浮点变量,它生成 3100.000000;对于 32 位无符号整数,它生成 3100。

\n

我尝试使用以下代码在https://www.onlinegdb.com/上测试计算结果:

\n
int main () {\n    float calc = 150 * 10000 / 155 / 138 * 100 / 220 * 100 ;\n    printf ( "calc = %f\\n", calc ) ;    // 3100.000000\n\n    uint32_t calc0 = 150 * 10000 / 155 / 138 * 100 / 220 * 100 ;\n    printf ( "calc0 = %u\\n", calc0 ) ;  // 3100\n}\n
Run Code Online (Sandbox Code Playgroud)\n

它再次为浮点数生成 3100.000000,为 32 位无符号整数生成 3100。

\n

如果我在手提电脑或笔记本电脑上的计算器中输入相同的数字,则两种情况的结果都是 3187,555782226。

\n

因此,我在 ESP32 上的精度损失为(如果我没有弄乱公式的话)大约。(3187\xe2\x88\x923100)\xc3\xb73187\xc3\x97100 ~= 2,73 %

\n

差异从何而来、如何产生?是否有可能在 32 位微控制器上获得与 PC 上一样的精确结果?

\n

Jan*_*tke 8

您的计算器或移动设备不会失去任何精度。精确的结果是3187.5557822261889583067701...,您的移动设备非常准确地近似该结果。

问题在于,在表达式 中150 * 10000 / 155 / 138 * 100 / 220 * 100,所有乘法和除法都是在整数之间进行的。即使您使用此表达式来初始化 a float,也为时已晚;精度已经丢失了。

为了获得更精确的结果,将第一个操作数float添加.f后缀作为 a,然后所有操作都将在浮点数之间进行:

#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 );
}
Run Code Online (Sandbox Code Playgroud)

印刷:

calc = 3187.555420
calc0 = 3187
calc1 = 3187
Run Code Online (Sandbox Code Playgroud)

查看实例


1)除了 之外/ 155 / 138 / 120,我们还可以写成/ (155ull * 138 * 120)结果保证是一样的


pax*_*blo 5

如果您的表达式只有int项,它就会进行int计算,无论它是否被分配给float后来的(1)。赋值对计算本身没有影响

例如,子表达式150 * 10000 / 155将为您提供精确的值9677,而不是更准确的值(大约9677.419)。

您需要告诉它以浮点计算进行计算,这可以通过简单地使用第一项150.0而不是 来完成150

请记住,这实际上会进行计算double(比 更高的范围和精度float)。然后,当将其分配给目标时,将进行任何精度损失调整float。如果这是您的(可能有限的)平台的问题,您仍然可以使用(2)float进行计算。150f

这也是为什么你的“uint64_t首先使用和做乘法”(在你的评论中)在这里没有帮助。这仅影响最终类型,计算仍然使用该int类型完成,因此无法避免任何溢出。同样,您可以通过在表达式中使用类似(uint64_t)150而不是的东西来解决这个问题。150


(1)表达式的一般规则operand1 operation operand2是首先修改两个操作数,使它们具有相同的“共同实数类型”。

因此,例如,两个int操作数将保留int。对于intand longint会转换为long。结果是相同类型的。这在 ISO (C17) 标准的 和 中有所 6.3.1 Arithmetic operands涉及6.3.1.8 Usual arithmetic conversions。前者给出了各种整数类型的排名,后者详细解释了如何执行转换。

重要的是,afloat和 an在进行计算之前int会将后者“升级”为 a 。float


(2)各个网站对 EPS32 上的浮点性能有很多讨论,因此double根据您的需要,操作可能会出现问题。但是,与所有优化问题一样,您应该测量,而不是猜测。然后,您可以明智地做出成本/效益决策,在速度和范围/精度之间进行选择。