如果您有与系统具有相同端序的数据,可以简单地将字节1-1映射为整数类型

本文关键字:简单 字节 映射 类型 整数 数据 系统 如果 | 更新日期: 2023-09-27 18:09:02

我正在编写一个类,它允许我在字节和各种整数数据类型之间进行转换。我选择确定系统的端序是否与数据相同,而不是反转数组然后转换数据。如果是,我只需将数据映射到整数,就像64位整数的情况一样:

result = (long)(
    (buffer[index] << 56) |
    (buffer[index + 1] << 48) |
    (buffer[index + 2] << 40) |
    (buffer[index + 3] << 32) |
    (buffer[index + 4] << 24) |
    (buffer[index + 5] << 16) |
    (buffer[index + 6] << 8) |
    (buffer[index + 7]));

如果系统的端序和数据的端序不同,则颠倒为:

result = (long)(
    (buffer[index]) |
    (buffer[index + 1] << 8) |
    (buffer[index + 2] << 16) |
    (buffer[index + 3] << 24) |
    (buffer[index + 4] << 32) |
    (buffer[index + 5] << 40) |
    (buffer[index + 6] << 48) |
    (buffer[index + 7] << 56));

result是64位有符号整数

buffer是一个字节数组

index是一个32位有符号整数,表示缓冲区中开始读取

的位置。我的问题是……我这样做错了吗?或者这只是一种非常简单的方法来进行转换,而不必在原地反转数组或进行复制?

这似乎应该适用于所有系统和数据端序的组合,并在两者之间正确转换。

是否有一种更容易阅读或更简单的方法?

如果您有与系统具有相同端序的数据,可以简单地将字节1-1映射为整数类型

在较新的c#版本中。. NET 5+, Core 2。x和Core 3.x),您可以使用System.Buffers.Binary.BinaryPrimitives来反转整数字节(ReverseEndianness),也可以以选定的端序从内存中读取/写入整数。

目前看来,Core CLR 7.0.222.60605确实使用movbe指令来实现内存的大端读取(即使c#代码将其表达为小端读取后跟ReverseEndianness),但使用bswap和正常的mov来实现大端存储。bswap + mov并不是,特别是在当前的英特尔处理器上,movbe似乎是这样实现的(64位bswap需要2µops,例如Ice Lake实现的movbe m64, r64比普通存储多2µops)。


这部分仍然适用于旧版本的c#,并且可能是普遍感兴趣的,但对于新版本的c#来说已经过时了。

您可以将long反转,而不是反转数组(使用ulong会稍微容易一些):

ulong raw = BitConverter.ToUInt64(array, pos);
if (wrong_endian)
{
    // swap groups of 4
    raw = (raw >> 32) | (raw << 32);
    // swap groups of 2
    raw = ((raw >> 16) & 0x0000FFFF0000FFFF) | ((raw << 16) &0xFFFF0000FFFF0000);
    // swap groups of 1
    raw = ((raw >> 8) & 0x00FF00FF00FF00FF) | ((raw << 8) & 0xFF00FF00FF00FF00);
}

代码没有经过测试,但是您可以理解。

在整数和它们的字节表示之间进行转换时,主要有两种情况:

本机字节顺序

在与本机代码进行互操作时,通常会出现这种情况。使用自然使用本地端序的代码,如Buffer.BlockCopyBitConverter.ToBytes/ToInt64和不安全代码。在某些情况下,p/invoke编组程序可以为您完成大部分工作。

固定字节顺序

这是解析文件或网络协议时的典型情况。在这种情况下,您的代码片段(减去类型转换错误)是处理它的理想方法。给它们起一个提到端序的名字,比如ToInt64BitEndian

它们易于理解,易于测试(不依赖于系统端序),并且相当快。

偶尔使用Buffer.BlockCopy或不安全的重新解释强制转换可以提高性能,但我只会在分析之后使用那些表明代码中存在瓶颈的方法。在我的程序中,这从来都不是瓶颈,所以我使用的代码与您的示例非常相似。

我不喜欢反转基于此的代码,因为大端系统的代码路径不会在典型的小端系统上执行。


ErrataRob对silent circle的代码审查也提出了类似的观点,并做了更多的阐述:

协议解析与CPU无关。没有理由根据不同的CPU做不同的事情。

类型转换和字节交换

执行上面的#if条件的错误来自于试图修复在char*int*之间进行强制转换的潜在错误。这是在"UNIX网络编程"课程中教授的一种常见技术。这也是错误的。在解析数据包时不应该这样做。

避免这种情况有两个原因。第一个问题是(如上所述)一些cpu,如SPARC和某些版本的ARM在引用未对齐整数时崩溃。这使得RISC系统上的网络代码不稳定,因为大多数整数通常都是对齐的,这意味着许多对齐问题在未被发现的情况下逃到了传输代码中。编写稳定代码的唯一方法是停止在网络(或文件)解析器中强制转换整数。

第二个问题是,它会引起字节顺序/端序的混淆,如果不强制转换整数,就不会发生这种情况。考虑IP地址"10.1.2.3"。这个数字只有两种形式,要么是值为0x0a010203的整数,要么是值为0a01 02 03的字节数组。问题是小端序机器很奇怪。整数0x0a010203在x86处理器上内部表示为03 02 01 0a,字节顺序为"交换"。

但这只是一个内部细节,你永远不需要担心。只要不跨流并从char*转换为int*(或相反),那么字节顺序/端序就无关紧要。

您可以随时使用BitConverter类。

这里有一个直接来自这里的例子。

byte[] bytes = { 0, 0, 0, 25 };
// If the system architecture is little-endian (that is, little end first), 
// reverse the byte array. 
if (BitConverter.IsLittleEndian)
    Array.Reverse(bytes);
int i = BitConverter.ToInt32(bytes, 0);
Console.WriteLine("int: {0}", i);
// Output: int: 25

是的,你做得对。(除了注释中提到的bug)

代码很简单,但可能不短。如果您想要更少的行数,可以这样做:

result = 0;
for(var i = 0; i < 8; i++)
    result |= (long)buffer[index + i] << (8*i);

并希望编译器能进行循环展开。其他代码类似:

result = 0;
for(var i = 0; i < 8; i++)
    result |= (long)buffer[index + i] << (56 - 8*i);