如何将主机字节序的值转换为小端字节序?

10

我需要将主机字节序中的短整型值转换为小端字节序。如果目标是大端字节序,我可以使用htons()函数,但不幸的是 - 它不是。

我想我可以这样做:

swap(htons(val))

但这可能会导致字节被交换两次,虽然结果正确,但会给我带来性能损失,在我的情况下这是不允许的。


不用担心性能损失。如果您执行冗余操作(例如两次交换一个 int 的值),编译器将检测到并在优化阶段删除该代码。 - Nils Pipenbrinck
尼尔斯所说的没错,但更谨慎的方法是首先检查您可以承受的优化级别生成的代码(如果您被困在调试中,则会出现问题)。如果双重交换被优化掉了,那么您的性能问题将立即得到解决。 - MaR
编译器真的能够优化这个吗?我猜如果swap()和htons()是宏或内联函数,那么它会,但否则呢? - anorm
这要看情况。有时编译器可以自动内联(或通过链接时代码生成间接内联),有时必须使用(强制)内联提示或使用跨模块编译...没有尝试过很难给出确切的建议。 - MaR
7个回答

9

这是一篇关于字节序(endianness)以及如何从IBM确定它的文章:

使用C编写与字节序无关的代码:不要让字节序“咬”你

其中包括一个在运行时确定字节序的示例(您只需要执行一次)。

const int i = 1;
#define is_bigendian() ( (*(char*)&i) == 0 )

int main(void) {
    int val;
    char *ptr;
    ptr = (char*) &val;
    val = 0x12345678;
    if (is_bigendian()) {
        printf(“%X.%X.%X.%X\n", u.c[0], u.c[1], u.c[2], u.c[3]);
    } else {
        printf(“%X.%X.%X.%X\n", u.c[3], u.c[2], u.c[1], u.c[0]);
    }
    exit(0);
}

该页面还有一个关于反转字节顺序的方法部分:

short reverseShort (short s) {
    unsigned char c1, c2;

    if (is_bigendian()) {
        return s;
    } else {
        c1 = s & 255;
        c2 = (s >> 8) & 255;

        return (c1 << 8) + c2;
    }
}

;

short reverseShort (char *c) {
    short s;
    char *p = (char *)&s;

    if (is_bigendian()) {
        p[0] = c[0];
        p[1] = c[1];
    } else {
        p[0] = c[1];
        p[1] = c[0];
    }

    return s;
}

1
检查条件比执行额外交换更好吗? - Michael Krelin - hacker
这样做的目的是:1. 使代码可移植,2. 仅在不是小端平台上执行交换。如果系统已经是小端,则最终只需进行一次测试和跳转,而不是进行4个字节的交换。 - Robert S. Barnes
另外一件事,由于条件永远不会改变 #define is_bigendian() ( (*(char*)&i) == 0 ),我猜测CPU上的分支预测器可能会消除它,从而使得当系统已经是小端字节序时,这实际上成为一个无操作。 - Robert S. Barnes
这种技术的好处是,它可以在没有ntohs()函数的系统上工作。另外,交换通常只使用两个参数。该方法允许扩展到具有超过2个八位字节的整数宽度。 - Thomas Matthews
在reverseShort()的第一次实现中,在第二个return语句中,在移位之前应将c1强制转换为short。否则,它们最终只会进入天空中的大位桶。 - RobH

6
那么你应该知道你的字节序并有条件地调用htons()。实际上,甚至不需要使用htons,只需要有条件地交换字节即可。当然,在编译时进行。

2
如果你在为性能而苦恼,#ifdef是你的好伙伴。 - Adam Luchjenbroers

4

类似以下内容:

unsigned short swaps( unsigned short val)
{
    return ((val & 0xff) << 8) | ((val & 0xff00) >> 8);
}

/* host to little endian */

#define PLATFORM_IS_BIG_ENDIAN 1
#if PLATFORM_IS_LITTLE_ENDIAN
unsigned short htoles( unsigned short val)
{
    /* no-op on a little endian platform */
    return val;
}
#elif PLATFORM_IS_BIG_ENDIAN
unsigned short htoles( unsigned short val)
{
    /* need to swap bytes on a big endian platform */
    return swaps( val);
}
#else
unsigned short htoles( unsigned short val)
{
    /* the platform hasn't been properly configured for the */
    /* preprocessor to know if it's little or big endian    */

    /* use potentially less-performant, but always works option */

    return swaps( htons(val));
}
#endif

如果您的系统已经正确配置(这样预处理器就知道目标ID是小端还是大端),您将得到一个“优化”的htoles()版本。否则,您将得到取决于htons()的潜在非优化版本。无论如何,您都会得到能够工作的东西。
没有什么特别棘手的问题,而且更或多或少是可移植的。
当然,您可以通过实现inline或适当的宏来进一步改善优化可能性。
您可能需要查看类似“Portable Open Source Harness (POSH)”之类的东西,以获取定义各种编译器的字节序的实际实现。请注意,要访问该库,需要通过伪身份验证页面(尽管您不需要注册或提供任何个人信息):http://hookatooka.com/poshlib/

正是我所需要的。我在Linux/gcc环境下,因此通过包含<endian.h>,__BYTE_ORDER被定义为__LITTLE_ENDIAN或__BIG_ENDIAN(或__PDP_ENDIAN)。 - anorm

0

我的经验法则是,这取决于你是否一次性地对大块数据进行小端字节序转换,还是只转换一个值:

如果只转换一个值,那么函数调用开销可能会压倒不必要的字节交换开销,即使编译器没有优化掉不必要的字节交换。然后,您可能会将该值写为套接字连接的端口号,并尝试打开或绑定套接字,这与任何类型的位操作相比都需要更长的时间。所以不要担心它。

如果是大块数据,则您可能会担心编译器无法处理。因此,请执行以下操作:

if (!is_little_endian()) {
    for (int i = 0; i < size; ++i) {
        vals[i] = swap_short(vals[i]);
    }
}

或者在您的架构上查看SIMD指令,这可以使其运行速度更快。

使用任何技巧编写is_little_endian()。我认为Robert S. Barnes提供的方法是可靠的,但由于通常对于给定的目标,您知道它是大端还是小端,因此您应该有一个特定于平台的头文件,将其定义为评估为1或0的宏。

一如既往,如果您真的关心性能,请查看生成的汇编代码,以查看是否已删除无意义的代码,并将各种替代方案相互比较以确定哪个实际上运行最快。


0

很遗憾,使用标准C编译时没有真正的跨平台方法来确定系统的字节顺序。我建议在您的config.h(或任何其他用于构建配置的内容)中添加#define

检查LITTLE_ENDIANBIG_ENDIAN定义是否正确的单元测试可能如下所示:

#include <assert.h>
#include <limits.h>
#include <stdint.h>

void check_bits_per_byte(void)
{ assert(CHAR_BIT == 8); }

void check_sizeof_uint32(void)
{ assert(sizeof (uint32_t) == 4); }

void check_byte_order(void)
{
    static const union { unsigned char bytes[4]; uint32_t value; } byte_order =
        { { 1, 2, 3, 4 } };

    static const uint32_t little_endian = 0x04030201ul;
    static const uint32_t big_endian = 0x01020304ul;

    #ifdef LITTLE_ENDIAN
    assert(byte_order.value == little_endian);
    #endif

    #ifdef BIG_ENDIAN
    assert(byte_order.value == big_endian);
    #endif

    #if !defined LITTLE_ENDIAN && !defined BIG_ENDIAN
    assert(!"byte order unknown or unsupported");
    #endif
}

int main(void)
{
    check_bits_per_byte();
    check_sizeof_uint32();
    check_byte_order();
}

0
这个技巧应该会起作用:在启动时,使用虚拟值调用ntohs,然后将结果值与原始值进行比较。如果两个值相同,则机器使用大端字节序,否则它是小端字节序。

然后,使用一个ToLittleEndian方法,根据初始测试的结果不执行任何操作或调用ntohs

(根据评论中提供的信息进行编辑)


你有没有注意到OP关心性能惩罚?;-) - Michael Krelin - hacker
OP只需要在启动时进行一次此检查。 - Jeff Paquette
1
将以下与编程有关的内容从英语翻译成中文。仅返回翻译后的文本:flyfishr64,并每次检查结果。不要拖延到运行时可以在编译时完成的事情。 - Michael Krelin - hacker

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接