11

在我编写的开源程序中,我正在从文件中读取二进制数据(由另一个程序编写)并输出整数、双精度和其他各种数据类型。挑战之一是它需要在两种字节序的 32 位和 64 位机器上运行,这意味着我最终不得不做相当多的低级位旋转。我对类型双关语和严格别名了解(非常)一点,并希望确保我以正确的方式做事。

基本上,很容易从 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;
}

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

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));
}

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

int64_t (*slittleint64_t)(const char *);
if(littleendian) {
    slittleint64_t = snativeint64_t;
} else {
    slittleint64_t = sswappedint64_t;
}

现在,当我尝试将 char* 转换为 double 时,棘手的部分就来了。我想像这样重新使用字节序交换代码:

union 
{
    double  d;
    int64_t i;
} int64todouble;

int64todouble.i = slittleint64_t(bufoffset);
printf("%lf", int64todouble.d);

然而,一些编译器可能会优化掉“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;
}

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

snativeint64_t:
        movq    (%rdi), %rax
        ret

在这两者中,memcpy 版本更明确地表达了我正在尝试做的事情,并且应该适用于即使是最天真的编译器。

亚当,你的回答也很棒,我从中学到了很多。感谢您的发布!

4

5 回答 5

12

我强烈建议您阅读Understanding Strict Aliasing。具体来说,请参阅标有“通过联合铸造”的部分。它有很多很好的例子。虽然这篇文章在一个关于 Cell 处理器的网站上并使用了 PPC 组装示例,但几乎所有这些都同样适用于其他架构,包括 x86。

于 2008-10-21T15:24:51.060 回答
2

该标准说,写入联合的一个字段并立即从中读取是未定义的行为。因此,如果您按照规则手册进行操作,则基于联合的方法将不起作用。

宏通常是一个坏主意,但这可能是规则的一个例外。使用输入和输出类型作为参数的一组宏应该可以在 C 中获得类似模板的行为。

于 2008-10-21T15:28:40.413 回答
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);

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

但是,总的来说,对字节交换做任何过于聪明的事情都会导致可移植性问题。存在具有中端双精度的 ABI,其中每个单词都是 little-endian,但 big word 先出现。

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

于 2008-10-21T16:29:08.573 回答
0

作为一个非常小的子建议,我建议您调查是否可以在 64 位情况下交换掩码和移位。由于该操作是交换字节,因此您应该始终可以使用0xff. 这应该会导致更快、更紧凑的代码,除非编译器足够聪明,可以自己解决这个问题。

简而言之,改变这个:

(((wrongend & 0xff00000000000000LL) >> 56)

进入这个:

((wrongend >> 56) & 0xff)

应该产生相同的结果。

于 2008-10-21T15:38:59.190 回答
-2

编辑:
删除了关于如何有效地存储数据总是大端和交换到机器端的评论,因为提问者没有提到另一个程序写入他的数据(这是重要信息)。

尽管如此,如果数据需要从任何字节序转换为大字节序,从大字节序转换为主机字节序,ntohs/ntohl/htons/htonl 是最好的方法,最优雅且速度无与伦比(因为如果 CPU 支持,它们将在硬件中执行任务,你不能打败那个)。


关于双精度/浮点数,只需通过内存转换将它们存储到整数:

double d = 3.1234;
printf("Double %f\n", d);
int64_t i = *(int64_t *)&d;
// Now i contains the double value as int
double d2 = *(double *)&i;
printf("Double2 %f\n", d2);

将其包装成一个函数

int64_t doubleToInt64(double d)
{
    return *(int64_t *)&d;
}

double int64ToDouble(int64_t i)
{
    return *(double *)&i;
}

发问者提供了这个链接:

http://cocoawithlove.com/2008/04/using-pointers-to-recast-in-c-is-bad.html

作为铸造不好的证明......不幸的是,我只能强烈反对这一页的大部分内容。引用和评论:

与通过指针进行强制转换一样普遍,这实际上是一种不好的做法,并且有潜在的风险代码。由于类型双关语,通过指针进行转换可能会产生错误。

这根本没有风险,也不是不好的做法。如果你做错了,它只有可能导致错误,就像用 C 编程如果你做错了有可能导致错误一样,任何语言的任何编程也是如此。根据这个论点,您必须完全停止编程。

类型双关语
一种指针别名形式,其中两个指针 和 引用内存中的相同位置,但将该位置表示为不同的类型。编译器会将这两个“双关语”视为不相关的指针。类型双关有可能导致通过两个指针访问的任何数据的依赖性问题。

这是真的,但不幸的是与我的代码完全无关

他指的是这样的代码:

int64_t * intPointer;
:
// Init intPointer somehow
:
double * doublePointer = (double *)intPointer;

现在 doublePointer 和 intPointer 都指向同一个内存位置,但是把它当作同一个类型。这确实是您应该通过工会解决的情况,其他任何事情都非常糟糕。糟糕的是,这不是我的代码所做的!

我的代码按复制,而不是按引用复制。我将一个 double 转换为 int64 指针(或相反)并立即遵从它。一旦函数返回,就没有指向任何东西的指针。有一个 int64 和一个 double ,它们与函数的输入参数完全无关。我从不将任何指针复制到不同类型的指针(如果您在我的代码示例中看到这一点,您严重误读了我编写的 C 代码),我只是将值传输到不同类型的变量(在自己的内存位置) . 所以类型双关的定义根本不适用,因为它说“引用内存中的相同位置”,这里没有任何东西指的是相同的内存位置。

int64_t intValue = 12345;
double doubleValue = int64ToDouble(intValue);
// The statement below will not change the value of doubleValue!
// Both are not pointing to the same memory location, both have their
// own storage space on stack and are totally unreleated.
intValue = 5678;

我的代码只不过是一个内存副本,只是用 C 语言编写的,没有外部函数。

int64_t doubleToInt64(double d)
{
    return *(int64_t *)&d;
}

可以写成

int64_t doubleToInt64(double d)
{
    int64_t result;
    memcpy(&result, &d, sizeof(d));
    return result;
}

仅此而已,因此即使在任何地方都看不到类型双关语。而且这个操作也是完全安全的,就像在 C 中的操作一样安全。 double 被定义为始终为 64 位(与 int 不同,它的大小不变,它固定为 64 位),因此它总是适合成一个 int64_t 大小的变量。

于 2008-10-21T15:55:04.883 回答