在C中安全地将char *加倍

在我写的一个开源程序中,我正在从文件中读取二进制数据(由另一个程序编写)并输出整数,双精度数和其他各种数据类型。 其中一个挑战是它需要在两个端点的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; } 

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

 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时,棘手的部分就出现了。 我想重新使用endian-swapping代码,如下所示:

 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版本更明确地表达了我正在尝试做的事情,甚至应该对最天真的编译器起作用。

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

由于您似乎对实现有足够的了解,以确保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); 

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

但是,一般情况下,使用byteswapping做一些太聪明的事情会导致可移植性问题。 存在具有中端双精度的ABI,其中每个单词都是小尾数,但是大词首先出现。

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

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

写这个标准说,写一个联盟的一个领域并立即从中读取是不明确的行为。 因此,如果按规则书进行操作,基于联合的方法将无效。

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

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

简而言之,改变这个:

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

进入这个:

 ((wrongend >> 56) & 0xff) 

应该产生相同的结果。

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

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


关于double / float,只需通过内存转换将它们存储到int中:

 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

作为一个certificate铸造是坏的…不幸的是,我只能强烈反对这个页面的大部分内容。 报价和评论:

像通过指针进行投射一样常见,它实际上是不好的做法和潜在的风险代码。 通过指针进行转换有可能因类型惩罚而产生错误。

它根本没有风险,也是不错的做法。 如果你不正确地做错,它只有可能导致错误,就像在C中编程有可能导致错误,如果你做错了,所以任何语言的编程也是如此。 通过这个论点,你必须完全停止编程。

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

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

他所指的是这样的代码:

 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中安全。双倍被定义为总是64位(与int不同,它的大小不变,它固定为64位),因此它总是适合到一个int64_t大小的变量。