在开源计划I 写道,我正在读取文件中的二进制数据(由另一个程序编写)并输出整数,双精度数, 和其他各种数据类型。其中一个挑战是它需要 在两个端点的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版本更明确地表达了我正在尝试做的事情,即使是最天真的编译器也应该工作。

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

有帮助吗?

解决方案

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

其他提示

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

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

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

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

简而言之,改变这一点:

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

进入这个:

((wrongend >> 56) & 0xff)

应该生成相同的结果。

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

仍然如果数据需要从任何endian转换对于大而且从大到主机端,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 /使用指针到重铸-在-C-是-bad.html

证明演员表演很糟糕......不幸的是,我只能强烈反对这个页面的大部分内容。报价和评论:

  

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

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

  

类型惩罚
一种指针形式   别名,两个指针和引用   到了记忆中的同一个位置但是   表示该位置不同   类型。编译器将同时处理两者   &QUOT;双关语&QUOT;作为无关的指针。类型   双关语有可能导致   任何数据的依赖性问题   通过两个指针访问。

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

他所指的是这样的代码:

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大小的变量。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top