为什么复制对字符串的引用比复制int要慢得多(但反之亦然,对于Array.Copy())?

时间:2009-08-14 18:28:40

标签: .net performance arrays

假设我想将数组的一部分向右移动1.我可以使用Array.Copy或者只是逐个复制元素:

private static void BuiltInCopy<T>(T[] arg, int start) {
    int length = arg.Length - start - 1;
    Array.Copy(arg, start, arg, start + 1, length);
}

private static void ElementByElement<T>(T[] arg, int start) {
    for (int i = arg.Length - 1; i > start; i--) {
        arg[i] = arg[i - 1];
    }
}

private static void ElementByElement2<T>(T[] arg, int start) {
    int i = arg.Length - 1;
    while (i > start)
        arg[i] = arg[--i];
}

Matt Howells建议使用ElementByElement2。)

我使用Minibench对其进行了测试,结果让我感到非常惊讶。

internal class Program {
    private static int smallArraySize = 32;

    public static void Main(string[] args) {
        BenchArrayCopy();
    }

    private static void BenchArrayCopy() {
        var smallArrayInt = new int[smallArraySize];
        for (int i = 0; i < smallArraySize; i++)
            smallArrayInt[i] = i;

        var smallArrayString = new string[smallArraySize];
        for (int i = 0; i < smallArraySize; i++)
            smallArrayString[i] = i.ToString();

        var smallArrayDateTime = new DateTime[smallArraySize];
        for (int i = 0; i < smallArraySize; i++)
            smallArrayDateTime[i] = DateTime.Now;

        var moveInt = new TestSuite<int[], int>("Move part of array right by 1: int")
            .Plus(BuiltInCopy, "Array.Copy()")
            .Plus(ElementByElement, "Element by element (for)")
            .Plus(ElementByElement2, "Element by element (while)")
            .RunTests(smallArrayInt, 0);

        var moveString = new TestSuite<string[], string>("Move part of array right by 1: string")
            .Plus(BuiltInCopy, "Array.Copy()")
            .Plus(ElementByElement, "Element by element (for)")
            .Plus(ElementByElement2, "Element by element (while)")
            .RunTests(smallArrayString, "0");

        moveInt.Display(ResultColumns.All, moveInt.FindBest());
        moveString.Display(ResultColumns.All, moveInt.FindBest());
    }

    private static T ElementByElement<T>(T[] arg) {
        ElementByElement(arg, 1);
        return arg[0];
    }

    private static T ElementByElement2<T>(T[] arg) {
        ElementByElement2(arg, 1);
        return arg[0];
    }

    private static T BuiltInCopy<T>(T[] arg) {
        BuiltInCopy(arg, 1);
        return arg[0];
    }

    private static void BuiltInCopy<T>(T[] arg, int start) {
        int length = arg.Length - start - 1;
        Array.Copy(arg, start, arg, start + 1, length);
    }

    private static void ElementByElement<T>(T[] arg, int start) {
        for (int i = arg.Length - 1; i > start; i--) {
            arg[i] = arg[i - 1];
        }
    }

    private static void ElementByElement2<T>(T[] arg, int start) {
        int i = arg.Length - 1;
        while (i > start)
            arg[i] = arg[--i];
    }
}

请注意,这里没有测量分配。所有方法都只是复制数组元素。由于我在32位操作系统上,intstring引用占用了相同的堆栈空间。

这是我期望看到的:

  1. BuiltInCopy应该是最快的,原因有两个:1)它可以做内存复制; 2)List<T>.Insert使用Array.Copy。另一方面,它是非泛型的,当数组有不同的类型时它可以做很多额外的工作,所以也许它没有充分利用1)。
  2. 对于ElementByElementint
  3. string应该同样快。
  4. 对于BuiltInCopyint
  5. string要么同样快,要么int要慢一些(如果必须做一些拳击)。
  6. 但是,所有这些假设都是错误的(至少在我的.NET 3.5 SP1机器上)!

      对于32个元素的数组,
    1. BuiltInCopy<int>明显慢于ElementByElement<int>。当尺寸增加时,BuiltInCopy<int>变得更快。
    2. ElementByElement<string> ElementByElement<int>慢4倍
    3. BuiltInCopy<int>BuiltInCopy<string>快。
    4. 有人可以解释这些结果吗?

      更新:来自CLR代码生成小组blog post on array bounds check elimination

        

      建议4:当您复制中型到大型数组时,请使用Array.Copy,而不是显式复制循环。首先,所有范围检查将被“提升”到循环外的单个检查。 如果数组包含对象引用,您还将有效地“提升”与存储到对象类型数组相关的两个额外费用:通常可以通过检查来消除与数组协方差相关的每元素“存储检查”关于数组的动态类型,垃圾收集相关的写屏障将被聚合并变得更有效。最后,我们将能够使用更高效的“memcpy”式复制循环。 (在即将到来的多核世界中,如果阵列足够大,甚至可能采用并行性!)

      最后一列是分数(以刻度/迭代次数计算的总持续时间,按最佳结果标准化)。

      两次smallArraySize = 32

      f:\MyProgramming\TimSort\Benchmarks\bin\Release>Benchmarks.exe
      ============ Move part of array right by 1: int ============
      Array.Copy()               468791028 0:30.350 1,46
      Element by element (for)   637091585 0:29.895 1,06
      Element by element (while) 667595468 0:29.549 1,00
      
      ============ Move part of array right by 1: string ============
      Array.Copy()               432459039 0:30.929 1,62
      Element by element (for)   165344842 0:30.407 4,15
      Element by element (while) 150996286 0:28.399 4,25
      
      
      f:\MyProgramming\TimSort\Benchmarks\bin\Release>Benchmarks.exe
      ============ Move part of array right by 1: int ============
      Array.Copy()               459040445 0:29.262 1,38
      Element by element (for)   645863535 0:30.929 1,04
      Element by element (while) 651068500 0:30.064 1,00
      
      ============ Move part of array right by 1: string ============
      Array.Copy()               403684808 0:30.191 1,62
      Element by element (for)   162646202 0:30.051 4,00
      Element by element (while) 160947492 0:30.945 4,16
      

      两次smallArraySize = 256

      f:\MyProgramming\TimSort\Benchmarks\bin\Release>Benchmarks.exe
      ============ Move part of array right by 1: int ============
      Array.Copy()               172632756 0:30.128 1,00
      Element by element (for)    91403951 0:30.253 1,90
      Element by element (while)  65352624 0:29.141 2,56
      
      ============ Move part of array right by 1: string ============
      Array.Copy()               153426720 0:28.964 1,08
      Element by element (for)    19518483 0:30.353 8,91
      Element by element (while)  19399180 0:29.793 8,80
      
      
      f:\MyProgramming\TimSort\Benchmarks\bin\Release>Benchmarks.exe
      ============ Move part of array right by 1: int ============
      Array.Copy()               184710866 0:30.456 1,00
      Element by element (for)    92878947 0:29.959 1,96
      Element by element (while)  73588500 0:30.331 2,50
      
      ============ Move part of array right by 1: string ============
      Array.Copy()               157998697 0:30.336 1,16
      Element by element (for)    19905046 0:29.995 9,14
      Element by element (while)  18838572 0:29.382 9,46
      

3 个答案:

答案 0 :(得分:5)

需要注意几点:

  • 对于BuiltInCopy,每次迭代还有一个方法调用 - 第一个方法调用另一个然后调用Array.Copy的重载。这是一点开销。
  • 您的实施不会确切地检查他们必须为重叠副本做什么。根据它们是“向上”还是“向下”移动(当目标数组与源相同时),它们应该从开始或结束起作用,以避免损坏。 Array.Copy将得到正确的结果 - 这是开销。
  • Array.Copy采用一般Array引用,可以是多维,不同类型等。您的方法只能在单个数组上工作。
  • Array.Copy会对排名,类型兼容性等进行大量检查。您的方法不会。
  • 您的方法占用的参数较少,这意味着需要在方法调用中复制较少的数据。

我不知道如何解释引用类型和值类型之间的区别,但上面应该解释为什么内置副本和例程之间的公平比较并不公平。

答案 1 :(得分:1)

System.Buffer.BlockCopy更接近C的memcpy但仍有开销。对于小案例,您自己的方法通常会更快,而对于大型案例,BlockCopy会更快。

复制引用比复制int更慢,因为在分配引用时,.NET在大多数情况下必须做一些额外的工作 - 这个额外的工作与垃圾收集有关。

为了演示这一事实,请查看下面的代码,其中包含用于复制每个字符串元素的本机代码与复制每个int元素(本机代码在注释中)。请注意,它实际上进行函数调用以将字符串引用赋值给src [i],而int是内联完成的:

    static void TestStrings()
    {
        string[] src = new string[5];
        for (int i = 0; i < src.Length; i++)
            src[i] = i.ToString();
        string[] dst = new string[src.Length];
        // Loop forever so we can break into the debugger when run
        // without debugger.
        while (true)
        {
            for (int i = 0; i < src.Length; i++)
                /*
                 * 0000006f  push        dword ptr [ebx+esi*4+0Ch] 
                 * 00000073  mov         edx,esi 
                 * 00000075  mov         ecx,dword ptr [ebp-14h] 
                 * 00000078  call        6E9EC15C 
                 */
                dst[i] = src[i];
        }
    }
    static void TestInts()
    {
        int[] src = new int[5];
        for (int i = 0; i < src.Length; i++)
            src[i] = i;
        int[] dst = new int[src.Length];
        // Loop forever so we can break into the debugger when run
        // without debugger.
        while (true)
        {
            for (int i = 0; i < src.Length; i++)
                /*
                 * 0000003d  mov         ecx,dword ptr [edi+edx*4+8] 
                 * 00000041  cmp         edx,dword ptr [ebx+4] 
                 * 00000044  jae         00000051 
                 * 00000046  mov         dword ptr [ebx+edx*4+8],ecx 
                 */
                dst[i] = src[i];
        }
    }

答案 2 :(得分:0)

为了删除一些变量,我在VB 2008中尝试了相同的测试,没有使用函数调用,而是使用StopWatch对象而不是Minibench。

数组大小32

1529 - 整数,逐元素复制
2613 - 字符串,逐个元素复制
3619 - 整数,array.copy
3649 - string,array.copy

然而,当我尝试数组大小为3,200,000时,array.copy仍然较慢!也许array.copy不使用memcopy等价物。 vb与c ++之间可能存在一些差异,函数调用可能会在32个成员的测试中发挥重要作用。

阵列大小3,200,000

55,750,010 - 整数,逐元素复制
55,462,881 - 字符串,逐元素复制
69,500,804 - 整数,array.copy
81,102,288 - string,array.copy

来源:

Dim clock As New Stopwatch
Dim t(4) As Integer
Dim iSize As Integer = 3200000
Dim smallArrayInt(iSize) As Integer
Dim smallArrayString(iSize) As String

For i = LBound(smallArrayInt) To UBound(smallArrayInt)
  smallArrayInt(i) = i
Next i

For i = LBound(smallArrayString) To UBound(smallArrayString)
  smallArrayString(i) = Str(i)
Next i

clock.Reset() : clock.Start()

t(0) = clock.ElapsedTicks
For i = 1 To iSize
  smallArrayInt(i - 1) = smallArrayInt(i)
  Next i
t(1) = clock.ElapsedTicks - t(0)

For i = 1 To iSize
  smallArrayInt(i - 1) = smallArrayInt(i)
  Next i
t(2) = clock.ElapsedTicks - t(1)

Array.Copy(smallArrayInt, 1, smallArrayInt, 0, iSize - 1)
t(3) = clock.ElapsedTicks - t(2)

Array.Copy(smallArrayString, 1, smallArrayString, 0, iSize - 1)
t(4) = clock.ElapsedTicks - t(3)

MsgBox(t(1) & ", " & t(2) & ", " & t(3) & ", " & t(4))