【问题标题】:Span<T> range operator doesn't make senseSpan<T> 范围运算符没有意义
【发布时间】:2021-01-30 17:18:26
【问题描述】:

我一辈子都无法理解 Span 的 Range 运算符背后的逻辑。

我有一个包含 IP 协议标头的以下字节数组:

---------------------------------------------------------------------------------
|   |   |   |   |   |   |   |   |   |   | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 9 |
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---------------------------------------------------------------------------------
|                   other data                  |    src ip     |    dest ip    |
---------------------------------------------------------------------------------

使用范围运算符,我需要使用以下值来提取正确的 4 字节数组:

Span<byte> ipHeader = ethernetFrame.IPHeader;
byte[] sourceIpAddress = ipHeader[12..16].ToArray();
byte[] destinationIpAddress = ipHeader[16..20].ToArray();

如果我使用 Slice 方法,我会得到:

Span<byte> ipHeader = ethernetFrame.IPHeader;
byte[] sourceIpAddress = ipHeader.Slice(12, 4).ToArray();
byte[] destinationIpAddress = ipHeader.Slice(16, 4).ToArray();

范围运算符背后的逻辑是什么要求我给出一个超过实际结束索引值 1 的结束值?

从性能的角度来看,我也很好奇(因为这个特定的代码块每分钟运行数百万次),无论如何使用 Slice 会更高效吗?

【问题讨论】:

    标签: c# .net .net-5 c#-8.0


    【解决方案1】:

    我一辈子都无法理解 Span 的 Range 运算符背后的逻辑。

    值得注意的是,这并不特定于 Span - C# 中的范围总是预计会有一个排他的上限。 (更多信息请参见tutorial。)

    范围运算符背后的逻辑是什么要求我给出一个超过实际结束索引值 1 的结束值?

    这只是一个排他的上限。上限通常是排他性的,这有很多好处。例如,请注意图片中有两个| 分隔线?这些位于索引 12 和 16 - 这是之前范围的上限,以及之后范围的下限。相同的数字在两个地方都有用,因此您无需开始加减一个。

    请注意,独占上限在 for 循环中也很常见。如果您想手动复制该 src IP 地址,我怀疑您会执行以下操作:

    for (int index = 12; index < 16; index++)
    {
        // Copy item at index
    }
    

    我认为这比使用 inclusive 上限更惯用:

    for (int index = 12; index <= 15; index++)
    {
        // Copy item at index
    }
    

    它也适用于指定为开始和长度的范围:

    for (int index = start; index < start + length; index++)
    {
        // Copy item at index
    }
    

    (或者相反,使用独占上限,您只需从结尾减去开头即可找到长度,无需任何“加一”部分。)

    从性能的角度来看,我也很好奇(因为这个特定的代码块每分钟运行数百万次),无论如何使用 Slice 会更高效吗?

    范围运算符已经对范围进行了切片。可能效率低下的是构造新的字节数组 - 如果您可以避免这种情况并直接使用跨度,那么效率会更高。

    【讨论】:

    • 这需要我一点时间来理解它,现在你已经把它画出来了,我明白了。只是当你从字节数组的角度来看它时,它并不完全直观。在中间阶段,我反编译了为 Range 操作与 Slice 生成的 IL,编译器为 Range 操作与 Slice 生成了大约 6 个以上的操作。至于避免字节数组复制,不幸的是, System.Net.IPAddress 的构造函数不接受 Span 作为参数。 :-/
    • @jscarle:这对我来说似乎很直观,但我想我已经习惯于从排他范围的角度来思考。在性能方面,我真的不会担心那些额外的 6 条指令。我相信他们基本上是在做减法——我怀疑在极少数情况下这种差异会被证明是显着的。
    • 我通常不会尝试在如此微观的层面上进行优化,只是我正在编写的代码将 24/7 运行并处理 TB 的数据,所以我尝试使用代码优化来压缩最后一点性能,同时尝试保持简化和类结构之间的平衡,使最终类可以轻松集成到其他项目中。
    • @jscarle:与以往一样,关键是衡量。尝试使用范围并使用切片尝试。鉴于您正在创建一个数组并复制数据,如果一个减法变得相关,我会感到惊讶。 (而关于网络的任何事情都会使之相形见绌......)
    • @jscarle:您可能想研究调用IPAddress(long) 构造函数而不是传入字节数组的选项。尽管如此,我还是希望 doing 使用 IPAddress 做任何事情都会比构造函数花费更长的时间。
    【解决方案2】:

    可以在here 找到一个有趣的设计决策链接(包括与排他)。

    报告中的结论是:

    • 它允许 a.Length 作为端点而不加/减 1。
    • 它让一个范围的结束成为下一个范围的开始而不重叠
    • 它避免了 x..x-1 形式的丑陋空范围

    【讨论】:

      猜你喜欢
      • 2010-09-09
      • 1970-01-01
      • 2018-04-19
      • 2011-09-05
      • 2014-10-05
      • 1970-01-01
      • 1970-01-01
      • 2011-01-21
      • 1970-01-01
      相关资源
      最近更新 更多