在C中安全地将char*加倍

Kir*_*ser 11 c endianness type-punning

我写的一个开源程序中,我正在读取文件中的二进制数据(由另一个程序编写)并输出整数,双精度和其他各种数据类型.其中一个挑战是它需要在两个端点的32位和64位机器上运行,这意味着我最终不得不做一些低级别的bit-twiddling.我知道(非常)关于类型惩罚和严格别名的一点点,并且想要确保我正确地做事.

基本上,很容易从char*转换为各种大小的int:

int64_t snativeint64_t(const char *buf) 
{
    /* Interpret the first 8 bytes of buf as a 64-bit int */
    return *(int64_t *) buf;
}
Run Code Online (Sandbox Code Playgroud)

我有一组支持函数来根据需要交换字节顺序,例如:

int64_t swappedint64_t(const int64_t wrongend)
{
    /* Change the endianness of a 64-bit integer */
    return (((wrongend & 0xff00000000000000LL) >> 56) |
            ((wrongend & 0x00ff000000000000LL) >> 40) |
            ((wrongend & 0x0000ff0000000000LL) >> 24) |
            ((wrongend & 0x000000ff00000000LL) >> 8)  |
            ((wrongend & 0x00000000ff000000LL) << 8)  |
            ((wrongend & 0x0000000000ff0000LL) << 24) |
            ((wrongend & 0x000000000000ff00LL) << 40) |
            ((wrongend & 0x00000000000000ffLL) << 56));
}
Run Code Online (Sandbox Code Playgroud)

在运行时,程序检测机器的字节顺序,并将上述之一分配给函数指针:

int64_t (*slittleint64_t)(const char *);
if(littleendian) {
    slittleint64_t = snativeint64_t;
} else {
    slittleint64_t = sswappedint64_t;
}
Run Code Online (Sandbox Code Playgroud)

现在,当我试图将char*转换为double时,棘手的部分就出现了.我想重新使用endian-swapping代码,如下所示:

union 
{
    double  d;
    int64_t i;
} int64todouble;

int64todouble.i = slittleint64_t(bufoffset);
printf("%lf", int64todouble.d);
Run Code Online (Sandbox Code Playgroud)

但是,一些编译器可以优化掉"int64todouble.i"赋值并打破程序.有没有更安全的方法来做到这一点,同时考虑到这个程序必须保持性能优化,而且我更愿意不编写一组并行的转换来直接将char*转换为double?如果双关语的联合方法是安全的,我应该重写我的函数,如snativeint64_t来使用它吗?


我最终使用了Steve Jessop的答案,因为转换函数重写为使用memcpy,如下所示:

int64_t snativeint64_t(const char *buf) 
{
    /* Interpret the first 8 bytes of buf as a 64-bit int */
    int64_t output;
    memcpy(&output, buf, 8);
    return output;
}
Run Code Online (Sandbox Code Playgroud)

编译成与原始代码完全相同的汇编程序:

snativeint64_t:
        movq    (%rdi), %rax
        ret
Run Code Online (Sandbox Code Playgroud)

在这两个中,memcpy版本更明确地表达了我正在尝试做的事情,甚至应该对最天真的编译器起作用.

亚当,你的答案也很精彩,我从中学到了很多东西.谢谢发帖!

Ada*_*eld 12

我强烈建议你阅读Understanding Strict Aliasing.具体来说,请参阅标记为"通过联合进行转换"的部分.它有很多很好的例子.虽然该文章位于关于Cell处理器的网站上并使用PPC汇编示例,但几乎所有这些都适用于其他架构,包括x86.


Ste*_*sop 2

由于您似乎对您的实现有足够的了解,可以确保 int64_t 和 double 具有相同的大小,并且具有合适的存储表示形式,因此您可能会冒险使用 memcpy。那么您甚至不必考虑别名。

由于您使用的是函数指针,如果您愿意释放多个二进制文件,则可以轻松内联该函数,因此性能无论如何都不会是一个大问题,但您可能想知道某些编译器可能会非常疯狂地优化 memcpy -对于小整数大小,可以内联一组加载和存储,您甚至可能会发现变量完全被优化掉,编译器的“复制”只是重新分配用于变量的堆栈槽,就像联合一样。

int64_t i = slittleint64_t(buffoffset);
double d;
memcpy(&d,&i,8); /* might emit no code if you're lucky */
printf("%lf", d);
Run Code Online (Sandbox Code Playgroud)

检查生成的代码,或者只是分析它。即使在最坏的情况下,速度也不会很慢。

不过,总的来说,对字节交换做任何过于聪明的事情都会导致可移植性问题。存在具有中端双精度的 ABI,其中每个单词都是小端,但大单词在前。

通常,您可以考虑使用 sprintf 和 sscanf 存储双精度,但对于您的项目,文件格式不受您的控制。但是,如果您的应用程序只是将 IEEE 双精度数从一种格式的输入文件铲到另一种格式的输出文件(不确定是否是这样,因为我不知道有问题的数据库格式,但如果是这样),那么也许您可以忘记它是双精度的事实,因为无论如何你都没有将它用于算术。只需将其视为不透明的 char[8],仅当文件格式不同时才需要字节交换。