如何编写端不可知的C/ c++代码

How to write endian agnostic C/C++ code?

本文关键字:c++ 代码 何编写 不可知      更新时间:2023-10-16

我在谷歌上搜索了一下,没有找到任何关于这个问题的好文章。当我实现一个应用程序时,我应该注意什么,我想是端不可知的?

唯一需要关心端序的情况是,当您在可能不具有相同端序的系统之间传输端序敏感的二进制数据(即不是文本)时。正常的解决方案是使用"网络字节顺序"。(又名大端)传输数据,然后在必要时在另一端交换字节。

要从主机字节顺序转换为网络字节顺序,使用htons(3)htonl(3)。要转换回来,使用ntohl(3)ntohs(3)。查看手册页,了解您需要知道的一切。对于64位数据,这个问题和答案将有所帮助。

当我实现一个应用程序时,我应该注意什么,我想是端部不可知论?

您首先必须认识到何时端序成为问题。当你必须从外部某处读取或写入数据时,无论是从文件读取数据还是在计算机之间进行网络通信,它都会成为一个问题。

在这种情况下,对于大于一个字节的整数,尾端顺序很重要,因为不同的平台在内存中表示整数的方式不同。这意味着每次需要读取或写入外部数据时,您需要做的不仅仅是转储程序的内存,或者直接将数据读取到您自己的变量中。

。如果你有这样一段代码:

unsigned int var = ...;
write(fd, &var, sizeof var);

你直接写出var的内存内容,这意味着数据被呈现到这个数据去的地方,就像它在你自己的计算机内存中表示的那样。

如果您将这些数据写入文件,那么无论您在大端序机器上运行程序还是在小端序机器上运行程序,文件内容都会有所不同。所以这段代码不是端序不可知的,你会想要避免这样做。

关注数据格式。在读写数据时,总是先决定数据格式,然后再编写处理它的代码。如果您需要读取一些现有的定义良好的文件格式或实现现有的网络协议,这可能已经为您决定了。

一旦你知道了数据格式,你的代码就会这样做,而不是直接转储一个int变量:

uint32_t i = ...;
uint8_t buf[4];
buf[0] = (i&0xff000000) >> 24;
buf[1] = (i&0x00ff0000) >> 16;
buf[2] = (i&0x0000ff00) >> 8;
buf[3] = (i&0x000000ff);
write(fd, buf, sizeof buf);

我们现在已经选择了最高位字节并将其作为缓冲区中的第一个字节,并将最低位字节放置在缓冲区的末尾。该整数在buf中以大端序格式表示,而不考虑主机的端序-因此此代码与端序无关。

该数据的使用者必须知道该数据是以大端格式表示的。不管程序在哪个主机上运行,这段代码都可以很好地读取数据:

uint32_t i;
uint8_t buf[4];
read(fd, buf, sizeof buf);
i  = (uint32_t)buf[0] << 24;
i |= (uint32_t)buf[1] << 16;
i |= (uint32_t)buf[2] << 8;
i |= (uint32_t)buf[3];

相反,如果您需要读取的数据是小端序格式的,那么与端序无关的代码将只执行

uint32_t i ;
uint8_t buf[4];
read(fd, buf, sizeof buf);
i  = (uint32_t)buf[3] << 24;
i |= (uint32_t)buf[2] << 16;
i |= (uint32_t)buf[1] << 8;
i |= (uint32_t)buf[0];

你可以编写一些很好的内联函数或宏来包装和展开你需要的所有2,4,8字节的整数类型,如果你使用这些并且关心数据格式而不是你运行的处理器的端序,你的代码将不依赖于它运行的端序。

这是比许多其他解决方案更多的代码,我还没有写过一个程序,这些额外的工作对性能有任何有意义的影响,即使是在移动1Gbps+的数据时。

它还避免了内存访问的不对齐,您可以通过以下方法轻松获得:

uint32_t i;
uint8_t buf[4];
read(fd, buf, sizeof buf);
i = ntohl(*(uint32_t)buf));

在最好的情况下也会导致性能下降(在某些情况下微不足道,在其他情况下则是很多很多个数量级),在不能对整数进行非对齐访问的平台上则会导致崩溃。

这可能是一篇适合您阅读的文章:字节顺序错误

计算机的字节顺序根本不重要,除了编译器编写者之类的人,他们为分配映射到寄存器块的内存字节而烦恼。很有可能你不是编译器的编写者,所以计算机的字节顺序对你来说一点也不重要。

注意短语"计算机的字节顺序"。重要的是外设或编码数据流的字节顺序,但是——这是关键点——计算机进行处理的字节顺序与数据本身的处理无关。如果数据流以字节序B编码值,那么在计算机上以字节序C解码值的算法应该是关于B的,而不是关于B和C之间的关系。

有几个答案涉及到文件IO,这当然是最常见的端到端问题。我将涉及一个尚未提到的:联合

下面的联合是SIMD/SSE编程中的常用工具,并且不支持结尾:

union uint128_t {
    _m128i      dq;
    uint64_t    dd[2];
    uint32_t    dw[4];
    uint16_t    dh[8];
    uint8_t     db[16];
};

任何访问dd/dw/dh/db表单的代码都将以特定于端序的方式进行。在32位cpu上,也经常看到更简单的联合,它允许更容易地将64位算术分解为32位部分:

union u64_parts {
    uint64_t    dd;
    uint32_t    dw[2];
};

由于在这种使用情况下很少(如果有的话)需要遍历联合的每个元素,所以我更喜欢这样写这样的联合:

union u64_parts {
    uint64_t dd;
    struct {
#ifdef BIG_ENDIAN
        uint32_t dw2, dw1;
#else
        uint32_t dw1, dw2;
#endif
    }
};

对于直接访问dw1/dw2的代码,结果是隐式的端交换。同样的设计方法也可以用于上面的128位SIMD数据类型,尽管它最终会变得相当冗长。

免责声明:由于关于结构填充和对齐的松散标准定义,使用联合通常是不受欢迎的。我发现联合非常有用,并且已经广泛地使用了它们,而且我在很长一段时间(15年以上)内没有遇到任何交叉兼容性问题。对于任何针对x86、ARM或PowerPC的当前编译器,联合填充/对齐都将以预期和一致的方式运行。

在你的代码中,你几乎可以忽略它——所有的东西都抵消了。

当你读/写数据到磁盘或网络使用按钮

这显然是一个相当有争议的话题。

一般的方法是设计你的应用程序,这样你只关心一小部分的字节顺序:代码的输入和输出部分。

在其他地方,您应该使用本机字节顺序。

请注意,尽管大多数机器都以相同的方式执行此操作,但不能保证浮点数和整数数据以相同的方式存储,因此为了完全确保工作正常,您不仅需要知道大小,还需要知道它是整数还是浮点数。

另一种选择是只使用和生成文本格式的数据。这可能几乎很容易实现,除非您在应用程序中有非常高的数据进出率,而处理很少,否则性能上的差异可能很小。这样做的好处是(对某些人来说),您可以在文本编辑器中读取输入和输出数据,而不是在代码中出现错误时试图解码输出中51213498-51213501字节的实际值。

如果需要在2、4或8字节整数类型和字节索引数组(或相反)之间重新解释,则需要知道端序。

这经常出现在加密算法实现、序列化应用程序(如网络协议、文件系统或数据库后端),当然还有操作系统内核和驱动程序中。

通常由像ENDIAN…这样的宏检测到。什么。

例如:

uint32 x = ...;
uint8* p = (uint8*) &x;

p在BE机器上指向高字节,在LE机器上指向低字节。

使用宏你可以这样写:

uint32 x = ...;
#ifdef LITTLE_ENDIAN
    uint8* p = (uint8*) &x + 3;
#else // BIG_ENDIAN
    uint8* p = (uint8*) &x;
#endif
例如,

总是得到高字节。

这里有定义宏的方法:C宏定义确定大端序还是小端序机器?