为什么我们将归一化分数乘以 0.5 以获得 IEEE 754 表示中的有效数字?

Why do we multiply normalized fraction with 0.5 to get the significand in IEEE 754 representation?

我对 Section 7.4 of Beej's Guide to Network Programming 中定义的 pack754() 函数有疑问。

此函数将浮点数 f 转换为其 IEEE 754 表示,其中 bits 是表示数字的总位数,expbits 是使用的位数仅代表指数。

我只关心单精度浮点数,所以这道题bits指定为32expbits指定为8。这意味着 23 位用于存储有效数字(因为一位是符号位)。

我的问题是关于这行代码。

    significand = fnorm * ((1LL<<significandbits) + 0.5f);

这段代码中+ 0.5f的作用是什么?

这是使用该函数的完整代码。

#include <stdio.h>
#include <stdint.h> // defines uintN_t types
#include <inttypes.h> // defines PRIx macros

uint64_t pack754(long double f, unsigned bits, unsigned expbits)
{
    long double fnorm;
    int shift;
    long long sign, exp, significand;
    unsigned significandbits = bits - expbits - 1; // -1 for sign bit

    if (f == 0.0) return 0; // get this special case out of the way

    // check sign and begin normalization
    if (f < 0) { sign = 1; fnorm = -f; }
    else { sign = 0; fnorm = f; }

    // get the normalized form of f and track the exponent
    shift = 0;
    while(fnorm >= 2.0) { fnorm /= 2.0; shift++; }
    while(fnorm < 1.0) { fnorm *= 2.0; shift--; }
    fnorm = fnorm - 1.0;

    // calculate the binary form (non-float) of the significand data
    significand = fnorm * ((1LL<<significandbits) + 0.5f);

    // get the biased exponent
    exp = shift + ((1<<(expbits-1)) - 1); // shift + bias

    // return the final answer
    return (sign<<(bits-1)) | (exp<<(bits-expbits-1)) | significand;
}

int main(void)
{
    float f = 3.1415926;
    uint32_t fi;

    printf("float f: %.7f\n", f);

    fi = pack754(f, 32, 8);
    printf("float encoded: 0x%08" PRIx32 "\n", fi);

    return 0;
}

+ 0.5f 在此代码中的作用是什么?

+ 0.5f 在代码中没有任何作用,可能有害或具有误导性。

表达式 (1LL<<significandbits) + 0.5f 的结果是 float。但即使对于单精度浮点数 significandbits = 23 的小情况,表达式的计算结果也是 (float)(223 + 0.5),它四舍五入到恰好 223(四舍五入)。

+ 0.5f 替换为 + 0.0f 会导致相同的行为。哎呀,完全放弃该术语,因为 fnorm 无论如何都会导致 * 的右侧参数被强制转换为 long double。这将是重写该行的更好方法:long long significand = fnorm * (long double)(1LL << significandbits);


旁注:pack754() 的这种实现正确处理了零(并将负零折叠为正零),但错误地处理了次正规数(错误位)、无穷大(无限循环)和 NaN(错误位) .最好不要把它当作参考模型函数。

该代码是不正确的四舍五入尝试。

long double fnorm;
long long significand;
unsigned significandbits
...
significand = fnorm * ((1LL<<significandbits) + 0.5f);  // bad code

第一个不正确的线索是0.5ff,它表示float,是在例程中指定float与[=18]的无意义介绍=] 和 fnormfloat数学在函数中没有应用。

然而添加 0.5f 并不意味着代码仅限于 (1LL<<significandbits) + 0.5f 中的 float 数学。请参阅 FLT_EVAL_METHOD,它可能允许更高精度的中间结果并在测试中欺骗了代码作者。

舍入尝试确实有意义,因为参数是 long double 并且目标表示更窄。添加 0.5 是一种常见的方法 - 但它并没有在这里完成。 IMO,作者在这里没有评论 0.5f 暗示其意图是 "obvious" - 虽然不正确,但并不微妙。

一样,移动 0.5 更接近正确的舍入,但可能会误导一些人认为加法是用 float 数学完成的,(它是long doublelong double 乘积添加到 float 会导致 0.5f 首先升级为 long double

// closer to rounding but may mislead
significand = fnorm * (1LL<<significandbits) + 0.5f;

// better
significand = fnorm * (1LL<<significandbits) + 0.5L; // or 0.5l or simply 0.5

舍入,而不调用首选的 <math.h> 舍入例程,如 rintl(), roundl(), nearbyintl(), llrintl(),添加显式类型 0.5 仍然是舍入的弱尝试。它很弱,因为它在许多情况下不正确地四舍五入。 +0.5 技巧依赖于 精确 的总和。

考虑

long double product = fnorm * (1LL<<significandbits);
long long significand = product + 0.5;  // double rounding?

product + 0.5 本身可能会在 truncation/assignment 到 long long 之前进行四舍五入 - 实际上 double rounding.

最好在标准库函数的 C 库中使用正确的工具。

significand = llrintl(fnorm * (1ULL<<significandbits));

这个四舍五入的一个极端情况是 significand 现在太大了,significand , exp 需要调整。正如 所指出的那样,代码也有其他缺点。此外,它在 -0.0.

上失败