ArraySegment - 返回实际段 C#

19

我一直在寻找返回由ArraySegment保持的偏移量和计数的段的方法。虽然ArraySegment保存完整的原始数组,但它只是用任何对段的更改反映到原始数组来限制它。 ArraySegment的问题或者说局限性在于,它不会作为一个整体返回段,而我必须遍历值。最好的方法是以整体形式返回段吗?

 byte[] input = new byte[5]{1,2,3,4,5};
 ArraySegment<byte> delimited = new ArraySegment<byte>(input,0,2);
 byte[] segment = HERE I NEED SOMETHING THAT WILL RETURN THE SEGMENT i.e. [0,1,2]

最重要的一点是,段必须不是一个副本,而应该引用原始数组。如果对段进行任何更改,它们必须反映在原始数组中。

非常感谢任何提示,谢谢!

任务基准:在得到ThomasdigEmAll的一些答案后

好的,我对digEmAll和Thomas的代码运行了一些基准测试,令我惊讶的是,这个代码比其他代码快得多。这正是我迫切需要的。以下是结果。

Construct             Size    Elements assigned    Iterations       Time
_______________________________________________________________________________

ArraySegmentWrapper   1500        1500              1000000       396.3 ms
Array.Copy            1500        1500              1000000       4389.04 ms

从差异性上可以看出,很明显我将使用ArraySegment的代码。以下是基准测试代码。请注意,这可能有些偏见,因为人们会争论为什么"new"被放在循环内部。我只是试图尽可能地重现我目前手头的情况,并尽量不移动太多代码来解决它。这让我今天过得很愉快!

namespace ArraySegmentWrapped
{
    class Program
    {

        public static Stopwatch stopWatch = new Stopwatch();
        public static TimeSpan span = new TimeSpan();
        public static double totalTime = 0.0;
        public static int iterations = 1000000;

        static void Main(string[] args)
        {
            int size = 1500;
            int startIndex = 0;
            int endIndex = 1499;
            byte[] array1 = new byte[size];
            byte[] array2 = null;

            for (int index = startIndex; index < size; index++)
            {
                array1[index] = (byte)index;
            }

            ArraySegmentWrapper<byte> arraySeg;

            for (int index = 0; index < iterations; index++)
            {
                stopWatch.Start();
                arraySeg = new ArraySegmentWrapper<byte>(array1, startIndex, endIndex);            
                stopWatch.Stop();
                totalTime += stopWatch.Elapsed.TotalMilliseconds;
            }

            Console.WriteLine("ArraySegment:{0:F6}", totalTime / iterations);
            stopWatch.Reset();
            totalTime = 0.0;

            for (int index = 0; index < iterations; index++)
            {
                stopWatch.Start();
                array2 = new byte[endIndex - startIndex + 1];
                Array.Copy(array1, startIndex, array2, 0, endIndex);
                stopWatch.Stop();
                totalTime += stopWatch.Elapsed.TotalMilliseconds;
            }
            Console.WriteLine("Array.Copy:{0:F6}", totalTime / iterations);                        


        }
    }
// Code for ArraySegmentWrapper goes here    

}

访问基准测试(已更新) 所以,Thomas指出基准测试并表示访问简单数组比ArraySegment更快,他完全正确。但是digEmAll指出我应该在Release模式下进行测试(抱歉之前在debug模式下测试的错误),我将代码几乎保持不变(迭代次数减少了两个零-无法等待输出,抱歉),并对访问相同数量的元素进行了一些修改,下面是我得到的结果。

Construct             Size    Elements accessed    Iterations       Time
_______________________________________________________________________________

ArraySegmentWrapper   1500        1500              1000000       5268.3 ms
Array.Copy            1500        1500              1000000       4812.4 ms

结论是尽管通过 ArraySegments 访问非常快,但访问速度较慢。


@Thomas,当我创建一个ArraySegmentWrapper实例时,不是也同时创建了segment吗?segment被放入arraySeg中。据我所见,这就是工作正在进行的地方。 - user349026
@Wajih,这个段落的创建成本是:它只存储一个数组的引用、偏移量和计数...它实际上并没有执行任何操作。 - Thomas Levesque
@Thomas,完全正确,正是需要的 :) - user349026
@Thomas 和 digEmAll。现在结果更加合理了。我想这里有一些差异。但并不像我想象的那么大。 - user349026
1
@Wajih:好的,优化器和即时编译器终究还是发挥了作用:D - digEmAll
显示剩余12条评论
4个回答

8

Thomas Levesque的建议开始,我构建了一个简单的ArraySegmentWrapper<T>类以以下方式使用:

static void Main(string[] args)
{
    int[] arr = new int[10];
    for (int i = 0; i < arr.Length; i++)
        arr[i] = i;

    // arr = 0,1,2,3,4,5,6,7,8,9

    var segment = new ArraySegmentWrapper<int>(arr, 2, 7);
    segment[0] = -1;
    segment[6] = -1;
    // now arr = 0,1,-1,3,4,5,6,7,-1,9


    // this prints: -1,3,4,5,6,7,-1
    foreach (var el in segment)
        Console.WriteLine(el);
}

实现:

public class ArraySegmentWrapper<T> : IList<T>
{
    private readonly ArraySegment<T> segment;

    public ArraySegmentWrapper(ArraySegment<T> segment)
    {
        this.segment = segment;
    }

    public ArraySegmentWrapper(T[] array, int offset, int count)
        : this(new ArraySegment<T>(array, offset, count))
    {
    }

    public int IndexOf(T item)
    {
        for (int i = segment.Offset; i < segment.Offset + segment.Count; i++)
            if (Equals(segment.Array[i], item))
                return i;
        return -1;
    }

    public void Insert(int index, T item)
    {
        throw new NotSupportedException();
    }

    public void RemoveAt(int index)
    {
        throw new NotSupportedException();
    }

    public T this[int index]
    {
        get
        {
            if (index >= this.Count)
                throw new IndexOutOfRangeException();
            return this.segment.Array[index + this.segment.Offset];
        }
        set
        {
            if (index >= this.Count)
                throw new IndexOutOfRangeException();
            this.segment.Array[index + this.segment.Offset] = value;
        }
    }

    public void Add(T item)
    {
        throw new NotSupportedException();
    }

    public void Clear()
    {
        throw new NotSupportedException();
    }

    public bool Contains(T item)
    {
        return this.IndexOf(item) != -1;
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        for (int i = segment.Offset; i < segment.Offset + segment.Count; i++)
        {
            array[arrayIndex] = segment.Array[i];
            arrayIndex++;
        }
    }

    public int Count
    {
        get { return this.segment.Count; }
    }

    public bool IsReadOnly
    {
        get { return false; }
    }

    public bool Remove(T item)
    {
        throw new NotSupportedException();
    }

    public IEnumerator<T> GetEnumerator()
    {
        for (int i = segment.Offset; i < segment.Offset + segment.Count; i++)
            yield return segment.Array[i];
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

编辑:

正如评论中@JeppeStigNielsen所指出的那样,自.NET 4.5以来,ArraySegment<T>实现了IList<T>


好的,这对我来说是一个宝藏,我现在会进行一些分析,看看我能得出什么。 - user349026
@RepDbg:那只是一个例子。当然,由于ArraySegment非常简单,您可以轻松地直接在包装器内部实现它。无论如何,我几乎不认为性能会有太大变化,因为编译器和Jitter通常会内联许多方法调用。也许仅考虑相对性能增益,差异会很大,但从绝对角度来看,我们谈论的只是几秒钟的差距... :) - digEmAll
我真的很喜欢这个包装器!我正在尝试找到将ArraySegment传递给Sha-256哈希类的最佳方法。这意味着我可以将其转换为数组或流。我不确定哪个开销更小...或者它们最终是否相同... - makerofthings7
@makerofthings7:将其转换为数组会有数组创建的开销,而如果您为ArraySegmentWrapper构建一个临时流类,则不会有这种情况。另一方面,实现自定义流并不像复制数组那样直截了当... - digEmAll
2
注意:自2012年8月起,.NET 4.5版本中的ArraySegment<>结构体已经实现了自身的IList<> - Jeppe Stig Nielsen
显示剩余2条评论

7

我使用以下的扩展方法来处理数组片段:

    #region ArraySegment related methods

    public static ArraySegment<T> GetSegment<T>(this T[] array, int from, int count)
    {
        return new ArraySegment<T>(array, from, count);
    }

    public static ArraySegment<T> GetSegment<T>(this T[] array, int from)
    {
        return GetSegment(array, from, array.Length - from);
    }

    public static ArraySegment<T> GetSegment<T>(this T[] array)
    {
        return new ArraySegment<T>(array);
    }

    public static IEnumerable<T> AsEnumerable<T>(this ArraySegment<T> arraySegment)
    {
        return arraySegment.Array.Skip(arraySegment.Offset).Take(arraySegment.Count);
    }

    public static T[] ToArray<T>(this ArraySegment<T> arraySegment)
    {
        T[] array = new T[arraySegment.Count];
        Array.Copy(arraySegment.Array, arraySegment.Offset, array, 0, arraySegment.Count);
        return array;
    }

    #endregion

您可以按照以下方式使用它们:
byte[] input = new byte[5]{1,2,3,4,5};
ArraySegment<byte> delimited = input.GetSegment(0, 2);
byte[] segment = delimited.ToArray();

这不会成为段的副本吗?这意味着对段所做的任何更改都不会反映在原始数组中? - user349026
是的,它将会是一个副本。这不正是你想要的吗?没有办法拥有一个指向相同数据的不同数组... 但是,你可以创建一个访问数组段的IList实现。 - Thomas Levesque
1
嗯,我想我将会非常怀念 C !无论如何还是谢谢! - user349026
@Wajih:C#与C不同,您不能拥有一个实际上是另一个子数组的数组,因为您不能(实际上不应该)使用指针。您可以创建一个自定义的IList<>实现,将原始数组实例保留在内部,正如Thomas所建议的那样(注意:所有T[]数组都实现了IList<T>)。 - digEmAll
@Wajih:看一下我的答案,这是一个例子 ;) - digEmAll
@digEmAll 和 @Thomas,非常感谢你们的回答。我会尝试这些答案。非常感谢你们的贡献,谢谢! - user349026

2
C#(以及.NET总体)不允许您创建指向另一个数组内部的标准数组引用。因此,您需要更改使用的API,使其能够处理ArraySegment实例,或者在操作副本后将更改复制回去。这通常是更安全的方法,因为传递对数组的引用会破坏隔离性,并且随着数组使用者数量的增加,跟踪错误变得更加困难。在.NET中构建新的数组实例并复制值相对便宜,只要数组不是非常大,因此性能影响通常可以忽略不计。
如果遇到性能问题并且需要微调,我建议使用不安全的C#代码(在其中可以修复数组引用并传递指针),或者将关键性能代码提取到C++/CLI程序集中,在那里可以使用非托管内存进行计算。我建议首先对代码进行分析,以验证这是否真正是瓶颈。我无法强调足够,在.NET中不必担心分配新内存,因为紧凑GC堆的性质意味着频繁的小型分配比在C中更便宜(在C中,内存分配必须考虑可能的堆碎片)。

我进行了一些分析,发现ArraySegment的性能比Array.Copy高出2倍。我使用了Array.Copy来创建一个段。我需要一些快速代码来参考原始数据。这就是为什么我探索ArraySegment的原因。唯一的问题是段引用,由于其缓慢和非引用数组切片,必须将Array.Copy从代码中移除。尽管如此,感谢您的答复。我会继续探索其他东西。 - user349026

1

请查看我在这个话题上发布的答案这里

基本上,你只需要将ArraySegment转换为IList即可获得你所期望的功能。


注意:本主题来自2011年,早于.NET 4.5发布之前。在那些日子里,ArraySegment<>结构体没有实现任何接口。这在2012年8月随着.NET 4.5的发布而改变了。 - Jeppe Stig Nielsen

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