gpt4 book ai didi

c - C中的 union 改变浮点加法的机器行为

转载 作者:太空宇宙 更新时间:2023-11-04 04:25:20 25 4
gpt4 key购买 nike

C 编程新手,有人告诉我要避免使用通常非常有意义的 union ,我同意。但是,作为学术练习的一部分,我正在编写一个模拟器,用于通过对无符号 32 位整数进行位操作操作来实现硬件单精度浮点加法。我只提到这一点来解释我为什么要使用 union ;我对仿真没有任何问题。

为了测试这个模拟器,我写了一个测试程序。但当然,我正在尝试在我的硬件上找到浮点数的位表示,所以我认为这可能是 union 的完美用途。我写了这个 union :

typedef union {
float floatRep;
uint32_t unsignedIntRep;
} FloatExaminer;

这样,我可以使用 floatRep 初始化浮点数成员,然后使用 unsignedIntRep 检查位成员。

这在大多数情况下都有效,但是当我到达 NaN 时另外,我开始遇到麻烦了。确切的情况是我编写了一个函数来自动化这些测试。它的要点是:
void addTest(float op1, float op2){
FloatExaminer result;
result.floatRep = op1 + op2;

printf("%f + %f = %f\n", op1, op2, result.floatRep);
//print bit pattern as well
printf("Bit pattern of result: %08x", result.unsignedIntRep);
}

好的,现在是令人困惑的部分:

我添加了 NANNAN用不同的尾数位模式来区分两者。在我的特定硬件上,它应该返回第二个 NAN操作数(如果它正在发出信号,则使其安静)。 (我将在下面解释我是如何知道这一点的。)但是,传递位模式 op1=0x7fc00001, op2=0x7fc00002将返回 op1, 0x7fc00001 , 每次!

我知道它应该返回第二个操作数,因为我尝试过——在函数之外——初始化为整数并转换为浮点数,如下所示:
uint32_t intRep1 = 0x7fc00001;
uint32_t intRep2 = 0x7fc00002;
float *op1 = (float *) &intRep1;
float *op2 = (float *) &intRep2;
float result = *op1 + *op2;
uint32_t *intResult = (uint32_t *)&result;
printf("%08x", *intResult); //bit pattern 0x7fc00002

最后,我得出结论, union 是邪恶的,我永远不应该使用它们。但是,有谁知道为什么我会得到我的结果?我犯了愚蠢的错误或假设吗? (我知道硬件架构各不相同,但这似乎很奇怪。)

最佳答案

我假设当您说“我的特定硬件”时,您指的是使用 SSE 浮点的英特尔处理器。但事实上,根据英特尔® 64 和 IA-32 架构,该架构有不同的规则
软件开发人员手册。这是该文档第 1 卷中表 4.7(“处理 NaN 的规则”)的摘要,它描述了算术运算中 NaN 的处理:(QNaN 是安静的 NaN;SNaN 是信号 NaN;我只包含了信息关于二操作数指令)

  • SNaN 和 QNaN
  • x87 FPU — QNaN 源操作数。
  • SSE — 第一个源操作数,转换为 QNaN。

  • 两个 SNaN
  • x87 FPU — 具有较大有效位的 SNaN 源操作数,转换为 QNaN
  • SSE — 第一个源操作数,转换为 QNaN。

  • 两个 QNaN
  • x87 FPU — 具有较大有效位的 QNaN 源操作数
  • SSE — 第一个源操作数

  • NaN 和浮点值
  • x87/SSE — NaN 源操作数,转换为 QNaN。


  • SSE 浮点机器指令的格式一般为 op xmm1, xmm2/m32 ,其中第一个操作数是目标寄存器,第二个操作数是寄存器或内存位置。该指令实际上将执行 xmm1 <- xmm1 (op) xmm2/m32 ,所以第一个操作数既是操作的左侧操作数又是目标。这就是上图中“第一个操作数”的含义。 AVX 添加了三操作数指令,其中目标可能是不同的寄存器;然后它是第三个操作数,并没有出现在上图中。 x87 FPU 使用基于栈的架构,栈顶始终是操作数之一,结果替换栈顶或另一个操作数;在上面的图表中,将注意到规则并不试图决定哪个操作数是“第一个”,而是依靠简单的比较。
    现在,假设我们正在为 SSE 机器生成代码,并且我们必须处理 C 语句:
    a = b + c;
    这些变量都没有在寄存器中。这意味着我们可能会发出类似这样的代码:(我在这里没有使用真正的指令,但原理是一样的)
    LOAD  r1, b  (r1 <- b)
    ADD r1, c (r1 <- r1 + c)
    STORE r1, a (a <- r1)
    但我们也可以这样做,结果(几乎)相同:
    LOAD  r1, c  (r1 <- c)
    ADD r1, b (r1 <- r1 + b)
    STORE r1, a (a <- r1)
    这将产生完全相同的效果,除了涉及 NaN 的添加(并且仅在使用 SSE 时)。由于 C 标准未指定涉及 NaN 的算术,因此编译器没有理由关心它选择这两个选项中的哪一个。特别是,如果 r1碰巧已经有值 c在其中,编译器可能会选择第二个选项,因为它保存了加载指令。 (谁会提示呢?我们都希望编译器生成运行速度尽可能快的代码,不是吗?)
    所以,简而言之, ADD 的操作数的顺序指令将随着编译器如何选择优化代码的复杂细节以及发出加法运算符时寄存器的特定状态而变化。这可能会通过使用 union 来实现,但同样或更有可能的是,在使用 union 的代码中,添加的值是函数的参数,因此是已经放入寄存器。
    实际上,不同版本的 gcc 和不同的优化设置会为您的代码产生不同的结果。并且强制编译器发出 x87 FPU 指令会产生不同的结果,因为硬件根据不同的逻辑运行。

    笔记:
    如果您想在睡前阅读,您可以从 their site 下载整个 Intel SDM(目前 4,684 页/23.3MB,但它会不断变大) .

    关于c - C中的 union 改变浮点加法的机器行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42180983/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com