【问题标题】:Most efficient way of getting the N last element of an array获取数组最后一个元素的最有效方法
【发布时间】:2014-03-03 13:07:25
【问题描述】:

对于一个项目,我必须经常取包含大量数据的数组的最后 N 个元素。

我试着做

myArray.Skip(myArray.Length - toTake).Take(toTake)

但我发现它很慢。

我把它和这个做了比较:

public static int[] TakeLast(this int[] inputArray, int count)
{
    int[] returnArray = new int[count];
    int startIndex = Math.Max(inputArray.Count() - count, 0);
    unsafe
    {
        fixed (int* itemArrayPtr = &(inputArray[startIndex]))
        {
            fixed (int* arrayPtr = &(returnArray[0]))
            {
                int* itemValuePtr = itemArrayPtr;
                int* valuePtr = arrayPtr;

                for (int i = 0; i < count; i++)
                {
                    *valuePtr++ = *itemValuePtr++;
                }
            }
        }
    }
    return returnArray;
}

这很好用,因为它不能是通用的(我希望这可以适用于任何原始类型(int、float、double、...)。

有没有办法通过 generic/linq/... 方法实现相当的性能?我不需要让它在 IEnumerable 上工作,Array 对我来说就足够了。

编辑 我目前正在测试你给我的所有方法,现在是 Array.Copy 似乎更快:

Generating array for 100000000 elements.
SkipTake: 00:00:00.3009047
Unsafe: 00:00:00.0006289
Array.Copy: 00:00:00.0000012
Buffer.BlockCopy: 00:00:00.0001860
Reverse Linq: 00:00:00.2201143
Finished

【问题讨论】:

  • new T[count] 后跟Array.Copy?顺便说一句,一个演示 Enumerable.Skip / Enumerable.Take 速度慢的简短程序会很棒,这样可以实际测试建议的替代方案,而不是主要猜测。
  • 是的,我原以为 Array.Copy() 会和指针运算一样快,因为它可能在实现中使用 memcpy 等效项。
  • 我认为这不会产生明显的影响,但您无需致电Take()。如果你只使用myArray.Skip(myArray.Length - toTake),性能会提高很多吗?
  • @hvd:请将此作为答案发布。速度之快令人惊讶
  • @BACON 好点,我在没有Take() 的情况下对其进行了测试,但似乎仍然很慢

标签: c# arrays performance linq


【解决方案1】:

来自cmets:

public static T[] TakeLast<T>(this T[] inputArray, int count)
{
    var result = new T[count];
    Array.Copy(inputArray, inputArray.Length - count, result, 0, count);
    return result;
}

似乎表现不错。值得指出的是,根据具体需求,可能完全避免使用新数组,而对原来的inputArray进行迭代。你不能复制比不复制更快。 :)

【讨论】:

  • +1 我的测试表明这是最好的解决方案,IMO。也许您应该在开头添加count = Math.Min(count, inputArray.Length);
  • 谢谢,经过我的所有测试,这是更好的解决方案。您将如何避免新数组? (在我的情况下,我可以直接引用基本数组,原始或目标都不会改变,只读操作)
  • @J4N 您可以创建一个自定义的IEnumerable&lt;T&gt;,它包含对原始数组的引用以及该数组的索引。它类似于Enumerable.Skip 的工作方式,但首先,由于您知道您正在处理一个数组,您可以通过索引访问数组元素,其次,您有足够的额外信息来避免不必要的装箱操作。这是否比复制数据表现更好取决于所涉及的数据。
  • 您也可以使用实现IEnumerable&lt;T&gt;ArraySegment(但我认为IEnumerable 功能需要.Net 4.5)
  • @MatthewWatson Ha,我不知道它存在。这几乎就是我的建议,唯一的区别是根据数据,添加一个返回值类型的GetEnumerator() 方法可能是有益的。 :)
【解决方案2】:

这个怎么样?应该相当快:

public static class ArrayExt
{
    public static T[] TakeLast<T>(this T[] inputArray, int count) where T: struct
    {
        count = Math.Min(count, inputArray.Length);
        int size = Marshal.SizeOf(typeof(T));

        T[] result = new T[count];
        Buffer.BlockCopy(inputArray, (inputArray.Length-count)*size, result, 0, count*size);

        return result;
    }
}

(对于原始类型,我认为这比 Array.Copy() 快一点。但我不会认为这是理所当然的 - 5 分钟后返回。;)


[EDIT] Timings 显示 Array.Copy() 的速度相似,但结果因运行而异,具体取决于数组大小。

这里有一些示例代码:

using System;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;

namespace Demo
{
    internal class Program
    {
        private void run()
        {
            const int ARRAY_SIZE = 10000;
            var array = Enumerable.Range(0, ARRAY_SIZE).Select(x => x).ToArray();
            Stopwatch sw = new Stopwatch();
            const int COUNT = 100000;

            for (int i = 0; i < 8; ++i)
            {
                sw.Restart();

                for (int j = 0; j < COUNT; ++j)
                    array.TakeLastViaArrayCopy(ARRAY_SIZE/2);

                Console.WriteLine("TakeLastViaArrayCopy took " + sw.Elapsed);

                sw.Restart();

                for (int j = 0; j < COUNT; ++j)
                    array.TakeLastViaBlockCopy(ARRAY_SIZE/2);

                Console.WriteLine("TakeLastViaBlockCopy took " + sw.Elapsed);
                Console.WriteLine();
            }
        }

        private static void Main()
        {
            new Program().run();
        }
    }

    public static class ArrayExt
    {
        public static T[] TakeLastViaBlockCopy<T>(this T[] inputArray, int count) where T: struct
        {
            count = Math.Min(count, inputArray.Length);
            int size = Marshal.SizeOf(typeof(T));

            T[] result = new T[count];
            Buffer.BlockCopy(inputArray, (inputArray.Length-count)*size, result, 0, count*size);

            return result;
        }

        public static T[] TakeLastViaArrayCopy<T>(this T[] inputArray, int count) where T: struct
        {
            count = Math.Min(count, inputArray.Length);

            T[] result = new T[count];
            Array.Copy(inputArray, inputArray.Length-count, result, 0, count);

            return result;
        }
    }
}

结果(照常发布构建):

TakeLastViaArrayCopy took 00:00:00.3028503
TakeLastViaBlockCopy took 00:00:00.3052196

TakeLastViaArrayCopy took 00:00:00.2969425
TakeLastViaBlockCopy took 00:00:00.3000117

TakeLastViaArrayCopy took 00:00:00.2906120
TakeLastViaBlockCopy took 00:00:00.2987753

TakeLastViaArrayCopy took 00:00:00.2954674
TakeLastViaBlockCopy took 00:00:00.3005010

TakeLastViaArrayCopy took 00:00:00.2944490
TakeLastViaBlockCopy took 00:00:00.3006893

TakeLastViaArrayCopy took 00:00:00.3041998
TakeLastViaBlockCopy took 00:00:00.2920206

TakeLastViaArrayCopy took 00:00:00.3115137
TakeLastViaBlockCopy took 00:00:00.2996884

TakeLastViaArrayCopy took 00:00:00.2906820
TakeLastViaBlockCopy took 00:00:00.2985933

Array.Copy() 比较简单,所以就用这个吧。

【讨论】:

  • 很好,但是为什么这会比 Array.Copy 快呢?目前我的测试并没有显示得更快。
  • @J4N 这不是更快 - 我只是尝试过。 Array.Copy() 更快。我将在我的答案中加入免责声明 - 您需要发布您的 Array.Copy() 答案。 :)
  • 是的,但我想奖励这个解决方案附带的 hvc。所以,如果他不发布,我会的。
  • @J4N 哦,对不起,是的,我的意思是 hvd 应该发布它(我说错了“你”)
【解决方案3】:

C#8 开始,您可以像这样使用Range

var lastN = array[^n..];

【讨论】:

    【解决方案4】:
    myArray.Reverse().Take(toTake).Reverse();
    

    【讨论】:

    • 似乎比Skip.Take快一点点,但还是要花很多时间
    猜你喜欢
    • 1970-01-01
    • 2022-09-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-09-29
    • 2015-10-12
    • 2014-01-26
    • 2010-10-16
    相关资源
    最近更新 更多