我做了一些谷歌搜索,找不到任何关于这个问题的好文章。在实现一个我想要与字节序无关的应用程序时,我应该注意什么?
7 回答
在实现一个我想要与字节序无关的应用程序时,我应该注意什么?
您首先必须认识到字节序何时成为问题。当您必须从外部某个地方读取或写入数据时,它通常会成为一个问题,无论是从文件中读取数据还是在计算机之间进行网络通信。
在这种情况下,字节顺序对于大于字节的整数很重要,因为不同平台在内存中表示的整数不同。这意味着每次您需要读取或写入外部数据时,您需要做的不仅仅是转储程序的内存,或者直接将数据读取到您自己的变量中。
例如,如果您有这段代码:
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,这当然是最常见的字节序问题。我将谈到一个尚未提及的:Unions。
以下联合是 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 的编译器,联合填充/对齐将以预期和一致的方式运行。
在您的代码中,您几乎可以忽略它 - 一切都取消了。
当您将数据读/写到磁盘或网络时,请使用htons
这显然是一个颇具争议的话题。
一般的方法是设计您的应用程序,以便您只关心一小部分的字节顺序:代码的输入和输出部分。
在其他任何地方,您都应该使用本机字节顺序。
请注意,尽管大多数机器以相同的方式执行此操作,但不能保证浮点和整数数据的存储方式相同,因此要完全确定一切正常,您不仅需要知道大小,还需要知道它是否是整数或浮点数。
另一种选择是仅使用和生成文本格式的数据。这可能几乎同样容易实现,除非您的应用程序的数据输入/输出率非常高且处理量很少,否则性能差异可能很小。并且(对某些人)的好处是,您可以在文本编辑器中读取输入和输出数据,而不是尝试解码输出中字节 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 宏定义确定大端还是小端机器?如果您的工具链没有提供它们。