问题 在C中安全地将char *加倍


在开源中 程序我 写,我正在读取文件中的二进制数据(由另一个程序编写)并输出整数,双精度数, 和其他各种数据类型。其中一个挑战是它需要 在两个端点的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时,棘手的部分就出现了。 ID 喜欢重复使用endian-swapping代码,如下所示:

union 
{
    double  d;
    int64_t i;
} int64todouble;

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

但是,一些编译器可以优化掉“int64todouble.i”赋值 并打破程序。在考虑时,是否有更安全的方法来做到这一点 这个程序必须保持优化性能,而且我也是 不想写一组并行的转换来转换char * 直接加倍?如果双关语的结合方法是安全的,我应该是 重写我的函数如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版本更明确地表达了我正在尝试做的事情,甚至应该对最天真的编译器起作用。

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


4861
2017-10-21 15:18


起源

只有char指针充分对齐时,整数转换才是安全的。 - Jonathan Leffler
在这个程序中,它总是如此。 - Kirk Strauser
为什么要在运行时检测endian-ness?我想这个程序只能在它编译的arch上工作,它将具有特定的endian-ness(我知道有些让你选择),那么为什么不像ntohl之类的那样做并使它成为编译时的决定? - Evan Teran
除了我最后的评论之外,我还建议在单个端序上标准化实际文件格式,这样它们可以在机器之间互换并且仍然可用。 - Evan Teran
该程序适用于不同的闭源程序的输出,所以我无法控制它。另一个问题是数据文件包含大端和小端值,所以我还需要一个“not_ntohl”函数。如果我走得那么远,我不妨处理所有情况。 - Kirk Strauser


答案:


由于您似乎对实现有足够的了解,以确保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],只有在文件格式不同时才需要字节翻转。


2
2017-10-21 16:29



伟大的memcpy提示 - 谢谢!我确实需要以文本形式输出双打输出,或者我只是将原始字节放在一边。此外,我在有和没有函数指针的情况下对它进行了大量的分析(因为如果它有很大的影响我愿意跳过大端)但是没有可测量的差异。 - Kirk Strauser


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


12
2017-10-21 15:24



谢谢!这就是我一直在寻找的东西。我现在要读了。 - Kirk Strauser
@ryan_s:谢谢,修好 - Adam Rosenfield


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

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


2
2017-10-21 15:28



GCC手册说“即使使用-fstrict-aliasing,也允许类型 - 双关,只要通过联合类型访问内存。”它的 所以 很容易称之为好,但我讨厌编写特定于编译器的代码。有指向宏示例的指针? - Kirk Strauser


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

简而言之,改变这个:

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

进入这个:

((wrongend >> 56) & 0xff)

应该产生相同的结果。


0
2017-10-21 15:38



这只适用于第一次掩码和移位操作,因为所有其他操作都将位移动到输出的中间。 - Kirk Strauser
没错,那么你必须在掩盖之后将其重新调整。我可能更喜欢这样做,因为避免巨大的常数(对我来说)很好。当你提取字节并用字节换字节做其他事情时,只需交换顺序就可以了。 - unwind


编辑:
删除了关于如何有效地存储数据总是大端和交换到机器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

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

像通过指针一样常见   是的,这实际上是不好的做法   潜在风险的代码。铸件   通过指针有潜力   由于类型惩罚而产生错误。

它根本没有风险,也是不错的做法。如果你不正确地做错,它只有可能导致错误,就像在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中安全。双倍被定义为总是64位(与int不同,它的大小不变,它固定为64位),因此它总是适合到一个int64_t大小的变量。


-1
2017-10-21 15:55



在您的第一点,程序读取由另一个程序生成的数据。在第二点,这似乎是不赞成的: cocoawithlove.com/2008/04/... 而我所要问的部分是我是否应该完全摆脱它。 - Kirk Strauser
见上面的更新。您的链接页面声称没有涉及类型惩罚,甚至不接近。并且与你的代码不同,我也从不对任何东西施加一个char指针(因为这绝对不安全!),我按值传递所有数据(从不通过引用!)并且我只在保证具有相同大小的类型之间进行转换 - Mecki
出于性能原因,我的代码中无法通过值传递。我从不投出一个char指针;我投了它的内容。最后,ntoh *仅适用于转换big-endian值。 little-endian值没有相应的函数。 - Kirk Strauser