C - 浮点数(floats,doubles)的序列化

15

如何将浮点数转换成一个字节序列,以便能够保存在文件中?这个算法必须快速、高度可移植。它还必须允许反向操作,即反序列化。最好只需要很小的位数作为每个值的过量(持久空间)。


你想要哪些系统具备可移植性? - Mark Elliot
它必须独立于底层架构,例如ARM-7、PowerPC、Microblaze、OpenRISC或仅限x86。 - psihodelia
这是作业吗?从你的评论来看似乎是这样。 - David Harris
3
有些问题即使是从作业中出现也很有趣。如果将该网站上任何与某人作业相关的事实和话题都禁止,那么我想这将意味着删除其中一半的内容... - yeoman
10个回答

14

假设您使用主流编译器,在C和C++中,浮点数遵循IEEE标准,并且在以二进制形式写入文件时可以在任何其他平台上恢复,前提是使用相同的字节序写入和读取。因此,我的建议是:选择一个字节序,并在写入之前或读取之后检查该字节序是否与当前平台相同;如果不同,只需交换字节即可。


根据C99规范,附录F,符合规范的实现应该定义__STDC_IEC_559__,原则上可以用作编译时检查,但在实践中是无用的,因为gcc存在问题(http://gcc.gnu.org/c99status.html,请向下滚动至“Further Issues”)。 - Christoph
编译器不一定决定 IEEE 浮点数格式。不幸的是,仍有一些计算机使用其他格式(如 VAX/Alpha、IBM)。但要确保正确设置字节序。 - user7116
1
正确,但他们必须要知道平台使用的格式以支持RTL。此外,许多平台(现在尤其是嵌入式)没有数学协处理器,因此它们确实会在相应的仿真库中指定格式。所以我认为参考编译器会更容易。 - Fabio Ceconello
2
难道不应该将那些不支持IEEE标准的平台视为例外情况,只有在需要(罕见的)适用于它们的版本时才进行必要的转换吗?这里有一篇关于差异的好文章:http://www.codeproject.com/KB/applications/libnumber.aspx - Fabio Ceconello

3

以下翻译可能会为您提供一个良好的开端 - 它将一个浮点值打包成一个intlong long对,然后您可以像往常一样对它进行序列化。

#define FRAC_MAX 9223372036854775807LL /* 2**63 - 1 */

struct dbl_packed
{
    int exp;
    long long frac;
};

void pack(double x, struct dbl_packed *r)
{
    double xf = fabs(frexp(x, &r->exp)) - 0.5;

    if (xf < 0.0)
    {
        r->frac = 0;
        return;
    }

    r->frac = 1 + (long long)(xf * 2.0 * (FRAC_MAX - 1));

    if (x < 0.0)
        r->frac = -r->frac;
}

double unpack(const struct dbl_packed *p)
{
    double xf, x;

    if (p->frac == 0)
        return 0.0;

    xf = ((double)(llabs(p->frac) - 1) / (FRAC_MAX - 1)) / 2.0;

    x = ldexp(xf + 0.5, p->exp);

    if (p->frac < 0)
        x = -x;

    return x;
}

3

您可以始终将数据转换为IEEE-754格式,以固定的字节顺序(小端或大端)进行存储。对于大多数计算机来说,这不需要任何操作或仅需简单的字节交换即可进行序列化和反序列化。如果一台计算机不支持IEEE-754,则需要编写一个转换器,但使用标准C库函数ldexpfrexp以及位移操作并不太困难。


问题在于FP标准缺少IEEE的一些“特性”。即VAX和IBM浮点格式...你将面临大量关于边角情况的痛苦。值得庆幸的是,人们编写了出色的转换器,可以优雅地处理这些情况(我在看着你USGS!我欠你一杯啤酒)。 - user7116
一个符合ANSI标准的frexp函数可以为您隐藏大部分内容。当然,您可能会遇到序列化和反序列化导致您得到一个(接近但)不同的值的情况。 - Chris Dodd

2

“便携”是什么意思?

为了实现便携性,请记得将数字保持在标准定义的限制范围内:如果使用超出这些限制的单个数字,那么所有的便携性都将荡然无存。

double planck_time = 5.39124E-44; /* second */

5.2.4.2.2 浮点类型的特征 <float.h>

[...]
10   下面列表中的值应该被实现定义的常量表达式所替换 [...]
11   下面列表中的值应该被实现定义的常量表达式所替换 [...]
12   下面列表中的值应该被实现定义的正常量表达式所替换 [...]
[...]

注意,所有这些条款都有实现定义


1

转换为ASCII表示法可能是最简单的方法,但如果您需要处理大量浮点数,那么当然应该使用二进制。但是,如果您关心可移植性,这可能是一个棘手的问题。不同机器中的浮点数表示方式不同。

如果您不想使用预先编写好的库,那么您的浮点数二进制序列化程序/反序列化程序将必须在每个位落在哪里以及代表什么方面有“契约”。

这是一个有趣的网站,可以帮助您解决这个问题:link


0

开始吧。

可移植的IEEE 754序列化/反序列化,应该可以在不考虑机器内部浮点表示的情况下工作。

https://github.com/MalcolmMcLean/ieee754

/*
* read a double from a stream in ieee754 format regardless of host
*  encoding.
*  fp - the stream
*  bigendian - set to if big bytes first, clear for little bytes
*              first
*
*/
double freadieee754(FILE *fp, int bigendian)
{
    unsigned char buff[8];
    int i;
    double fnorm = 0.0;
    unsigned char temp;
    int sign;
    int exponent;
    double bitval;
    int maski, mask;
    int expbits = 11;
    int significandbits = 52;
    int shift;
    double answer;

    /* read the data */
    for (i = 0; i < 8; i++)
        buff[i] = fgetc(fp);
    /* just reverse if not big-endian*/
    if (!bigendian)
    {
        for (i = 0; i < 4; i++)
        {
            temp = buff[i];
            buff[i] = buff[8 - i - 1];
            buff[8 - i - 1] = temp;
        }
    }
    sign = buff[0] & 0x80 ? -1 : 1;
    /* exponet in raw format*/
    exponent = ((buff[0] & 0x7F) << 4) | ((buff[1] & 0xF0) >> 4);

    /* read inthe mantissa. Top bit is 0.5, the successive bits half*/
    bitval = 0.5;
    maski = 1;
    mask = 0x08;
    for (i = 0; i < significandbits; i++)
    {
        if (buff[maski] & mask)
            fnorm += bitval;

        bitval /= 2.0;
        mask >>= 1;
        if (mask == 0)
        {
            mask = 0x80;
            maski++;
        }
    }
    /* handle zero specially */
    if (exponent == 0 && fnorm == 0)
        return 0.0;

    shift = exponent - ((1 << (expbits - 1)) - 1); /* exponent = shift + bias */
    /* nans have exp 1024 and non-zero mantissa */
    if (shift == 1024 && fnorm != 0)
        return sqrt(-1.0);
    /*infinity*/
    if (shift == 1024 && fnorm == 0)
    {

#ifdef INFINITY
        return sign == 1 ? INFINITY : -INFINITY;
#endif
        return  (sign * 1.0) / 0.0;
    }
    if (shift > -1023)
    {
        answer = ldexp(fnorm + 1.0, shift);
        return answer * sign;
    }
    else
    {
        /* denormalised numbers */
        if (fnorm == 0.0)
            return 0.0;
        shift = -1022;
        while (fnorm < 1.0)
        {
            fnorm *= 2;
            shift--;
        }
        answer = ldexp(fnorm, shift);
        return answer * sign;
    }
}


/*
* write a double to a stream in ieee754 format regardless of host
*  encoding.
*  x - number to write
*  fp - the stream
*  bigendian - set to write big bytes first, elee write litle bytes
*              first
*  Returns: 0 or EOF on error
*  Notes: different NaN types and negative zero not preserved.
*         if the number is too big to represent it will become infinity
*         if it is too small to represent it will become zero.
*/
int fwriteieee754(double x, FILE *fp, int bigendian)
{
    int shift;
    unsigned long sign, exp, hibits, hilong, lowlong;
    double fnorm, significand;
    int expbits = 11;
    int significandbits = 52;

    /* zero (can't handle signed zero) */
    if (x == 0)
    {
        hilong = 0;
        lowlong = 0;
        goto writedata;
    }
    /* infinity */
    if (x > DBL_MAX)
    {
        hilong = 1024 + ((1 << (expbits - 1)) - 1);
        hilong <<= (31 - expbits);
        lowlong = 0;
        goto writedata;
    }
    /* -infinity */
    if (x < -DBL_MAX)
    {
        hilong = 1024 + ((1 << (expbits - 1)) - 1);
        hilong <<= (31 - expbits);
        hilong |= (1 << 31);
        lowlong = 0;
        goto writedata;
    }
    /* NaN - dodgy because many compilers optimise out this test, but
    *there is no portable isnan() */
    if (x != x)
    {
        hilong = 1024 + ((1 << (expbits - 1)) - 1);
        hilong <<= (31 - expbits);
        lowlong = 1234;
        goto writedata;
    }

    /* get the sign */
    if (x < 0) { sign = 1; fnorm = -x; }
    else { sign = 0; fnorm = x; }

    /* 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--; }

    /* check for denormalized numbers */
    if (shift < -1022)
    {
        while (shift < -1022) { fnorm /= 2.0; shift++; }
        shift = -1023;
    }
    /* out of range. Set to infinity */
    else if (shift > 1023)
    {
        hilong = 1024 + ((1 << (expbits - 1)) - 1);
        hilong <<= (31 - expbits);
        hilong |= (sign << 31);
        lowlong = 0;
        goto writedata;
    }
    else
        fnorm = fnorm - 1.0; /* take the significant bit off mantissa */

    /* calculate the integer form of the significand */
    /* hold it in a  double for now */

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


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

    /* put the data into two longs (for convenience) */
    hibits = (long)(significand / 4294967296);
    hilong = (sign << 31) | (exp << (31 - expbits)) | hibits;
    x = significand - hibits * 4294967296;
    lowlong = (unsigned long)(significand - hibits * 4294967296);

writedata:
    /* write the bytes out to the stream */
    if (bigendian)
    {
        fputc((hilong >> 24) & 0xFF, fp);
        fputc((hilong >> 16) & 0xFF, fp);
        fputc((hilong >> 8) & 0xFF, fp);
        fputc(hilong & 0xFF, fp);

        fputc((lowlong >> 24) & 0xFF, fp);
        fputc((lowlong >> 16) & 0xFF, fp);
        fputc((lowlong >> 8) & 0xFF, fp);
        fputc(lowlong & 0xFF, fp);
    }
    else
    {
        fputc(lowlong & 0xFF, fp);
        fputc((lowlong >> 8) & 0xFF, fp);
        fputc((lowlong >> 16) & 0xFF, fp);
        fputc((lowlong >> 24) & 0xFF, fp);

        fputc(hilong & 0xFF, fp);
        fputc((hilong >> 8) & 0xFF, fp);
        fputc((hilong >> 16) & 0xFF, fp);
        fputc((hilong >> 24) & 0xFF, fp);
    }
    return ferror(fp);
}

已解决。代码现在已经上传。(链接中也有单精度,但它很直接) - Malcolm McLean

0

这个版本每个浮点数有多余的一个字节来指示字节序。但是我认为,它仍然不够可移植。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>

#define LITEND      'L'
#define BIGEND      'B'

typedef short               INT16;
typedef int                 INT32;
typedef double              vec1_t;

 typedef struct {
    FILE            *fp;
} WFILE, RFILE;

#define w_byte(c, p)    putc((c), (p)->fp)
#define r_byte(p)       getc((p)->fp)

static void w_vec1(vec1_t v1_Val, WFILE *p)
{
    INT32   i;
    char    *pc_Val;

    pc_Val = (char *)&v1_Val;

    w_byte(LITEND, p);
    for (i = 0; i<sizeof(vec1_t); i++)
    {
        w_byte(pc_Val[i], p);
    }
}


static vec1_t r_vec1(RFILE *p)
{
    INT32   i;
    vec1_t  v1_Val;
    char    c_Type,
            *pc_Val;

    pc_Val = (char *)&v1_Val;

    c_Type = r_byte(p);
    if (c_Type==LITEND)
    {
        for (i = 0; i<sizeof(vec1_t); i++)
        {
            pc_Val[i] = r_byte(p);
        }
    }
    return v1_Val;
}

int main(void)
{
    WFILE   x_FileW,
            *px_FileW = &x_FileW;
    RFILE   x_FileR,
            *px_FileR = &x_FileR;

    vec1_t  v1_Val;
    INT32   l_Val;
    char    *pc_Val = (char *)&v1_Val;
    INT32   i;

    px_FileW->fp = fopen("test.bin", "w");
    v1_Val = 1234567890.0987654321;
    printf("v1_Val before write = %.20f \n", v1_Val);
    w_vec1(v1_Val, px_FileW);
    fclose(px_FileW->fp);

    px_FileR->fp = fopen("test.bin", "r");
    v1_Val = r_vec1(px_FileR);
    printf("v1_Val after read = %.20f \n", v1_Val);
    fclose(px_FileR->fp);
    return 0;
}

5
它只能在使用相同浮点格式的机器上移植。因为我曾有过这样的经历,所以我要给你以下建议:采用小端IEEE-754标准,并让其他人在必要时进行转换。最终你会更加开心,因为你将拥有通过严格标准实现的可移植性。 - user7116

0

sprintf和fprintf?再也没有比这更具可移植性的了。


2
这不是有效的解决方案,相比于在RAM中表示相同数字,它需要更多的持久空间。 - psihodelia
3
那你为什么不这样做呢? - Steve Jessop
5
可能需要更多的空间,但它既可读性强又适合机器读取,不受大小端影响,并且在所需精度方面理论上是无限制的。 - dreamlax
2
更重要的是,@dreamlax,它与浮点格式无关。 - user7116
4
这看起来一开始很不错,但有一个含义可能会很严重(或者根据使用情况可能不那么严重):你并不能总是以十进制格式存储浮点数。这意味着,无论你添加多少小数位,在ASCII十进制格式中存储,你不能保证读回的数字与存储的数字相同。 - Gerasimos R
显示剩余2条评论

0
你需要多高的可移植性?如果文件将在与生成它的操作系统相同的计算机上读取,则使用二进制文件并保存和恢复位模式应该可以工作。否则,正如boytheo所说,ASCII是你的好朋友。

-1
fwrite(),fread()?您可能需要使用二进制,并且除非您想在程序中牺牲精度然后再使用fwrite() fread(),否则无法将字节压缩得更紧密;float a; double b; a =(float)b; fwrite(&a,1,sizeof(a),fp);
如果您要处理不同的浮点格式,则它们可能无法以直接二进制方式进行转换,因此您可能需要拆分位并执行数学运算,例如此次幂加上此次等等。 IEEE 754是一个糟糕的标准,但广泛使用,因此它可以最小化工作量。

这个问题明显是在询问一种便携式的方法,而这显然不是。 - Kevin Cox
“浮点数”本质上是不可移植的,因为有许多格式且具体格式未指定。C语言也不是非常可移植,这个问题本来就存在缺陷。 - old_timer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接