在 C# 中复制数组的任何更快的方法?

Posted

技术标签:

【中文标题】在 C# 中复制数组的任何更快的方法?【英文标题】:Any faster way of copying arrays in C#? 【发布时间】:2011-07-03 05:53:08 【问题描述】:

我有三个数组需要组合成一个三维数组。以下代码显示性能资源管理器中的低性能。有更快的解决方案吗?

for (int i = 0; i < sortedIndex.Length; i++) 
    if (i < num_in_left)
        
        // add instance to the left child
        leftnode[i, 0] = sortedIndex[i];
        leftnode[i, 1] = sortedInstances[i];
        leftnode[i, 2] = sortedLabels[i];
    
    else
     
        // add instance to the right child
        rightnode[i-num_in_left, 0] = sortedIndex[i];
        rightnode[i-num_in_left, 1] = sortedInstances[i];
        rightnode[i-num_in_left, 2] = sortedLabels[i];
                        

更新:

我实际上正在尝试执行以下操作:

//given three 1d arrays
double[] sortedIndex, sortedInstances, sortedLabels;
// copy them over to a 3d array (forget about the rightnode for now)
double[] leftnode = new double[sortedIndex.Length, 3];
// some magic happens here so that
leftnode = sortedIndex, sortedInstances, sortedLabels;

【问题讨论】:

我本来打算建议不安全的代码,但后来发现:***.com/questions/85479/c-unsafe-fixed-code。 Array.Copy,正如马龙所说,这可能是最好的选择。 【参考方案1】:

使用Buffer.BlockCopy。它的全部目的是快速执行(参见Buffer):

与 System.Array 类中的类似方法相比,此类在操作原始类型方面提供了更好的性能。

诚然,我没有做过任何基准测试,但这就是文档。它也适用于多维数组;只需确保您始终指定要复制多少 字节,而不是多少元素,并且您正在处理原始数组。

此外,我还没有对此进行测试,但是如果您将委托绑定到 System.Buffer.memcpyimpl 并直接调用它,您可能能够从系统中挤出更多性能。签名是:

internal static unsafe void memcpyimpl(byte* src, byte* dest, int len)

它确实需要指针,但我相信它已针对可能的最高速度进行了优化,因此我认为没有任何方法可以比这更快,即使您手头有组装。


更新

由于请求(并满足我的好奇心),我对此进行了测试:

using System;
using System.Diagnostics;
using System.Reflection;

unsafe delegate void MemCpyImpl(byte* src, byte* dest, int len);

static class Temp

    //There really should be a generic CreateDelegate<T>() method... -___-
    static MemCpyImpl memcpyimpl = (MemCpyImpl)Delegate.CreateDelegate(
        typeof(MemCpyImpl), typeof(Buffer).GetMethod("memcpyimpl",
            BindingFlags.Static | BindingFlags.NonPublic));
    const int COUNT = 32, SIZE = 32 << 20;

    //Use different buffers to help avoid CPU cache effects
    static byte[]
        aSource = new byte[SIZE], aTarget = new byte[SIZE],
        bSource = new byte[SIZE], bTarget = new byte[SIZE],
        cSource = new byte[SIZE], cTarget = new byte[SIZE];


    static unsafe void TestUnsafe()
    
        Stopwatch sw = Stopwatch.StartNew();
        fixed (byte* pSrc = aSource)
        fixed (byte* pDest = aTarget)
            for (int i = 0; i < COUNT; i++)
                memcpyimpl(pSrc, pDest, SIZE);
        sw.Stop();
        Console.WriteLine("Buffer.memcpyimpl: 0:N0 ticks", sw.ElapsedTicks);
    

    static void TestBlockCopy()
    
        Stopwatch sw = Stopwatch.StartNew();
        sw.Start();
        for (int i = 0; i < COUNT; i++)
            Buffer.BlockCopy(bSource, 0, bTarget, 0, SIZE);
        sw.Stop();
        Console.WriteLine("Buffer.BlockCopy: 0:N0 ticks",
            sw.ElapsedTicks);
    

    static void TestArrayCopy()
    
        Stopwatch sw = Stopwatch.StartNew();
        sw.Start();
        for (int i = 0; i < COUNT; i++)
            Array.Copy(cSource, 0, cTarget, 0, SIZE);
        sw.Stop();
        Console.WriteLine("Array.Copy: 0:N0 ticks", sw.ElapsedTicks);
    

    static void Main(string[] args)
    
        for (int i = 0; i < 10; i++)
        
            TestArrayCopy();
            TestBlockCopy();
            TestUnsafe();
            Console.WriteLine();
        
    

结果:

Buffer.BlockCopy: 469,151 ticks
Array.Copy: 469,972 ticks
Buffer.memcpyimpl: 496,541 ticks

Buffer.BlockCopy: 421,011 ticks
Array.Copy: 430,694 ticks
Buffer.memcpyimpl: 410,933 ticks

Buffer.BlockCopy: 425,112 ticks
Array.Copy: 420,839 ticks
Buffer.memcpyimpl: 411,520 ticks

Buffer.BlockCopy: 424,329 ticks
Array.Copy: 420,288 ticks
Buffer.memcpyimpl: 405,598 ticks

Buffer.BlockCopy: 422,410 ticks
Array.Copy: 427,826 ticks
Buffer.memcpyimpl: 414,394 ticks

现在改变顺序:

Array.Copy: 419,750 ticks
Buffer.memcpyimpl: 408,919 ticks
Buffer.BlockCopy: 419,774 ticks

Array.Copy: 430,529 ticks
Buffer.memcpyimpl: 412,148 ticks
Buffer.BlockCopy: 424,900 ticks

Array.Copy: 424,706 ticks
Buffer.memcpyimpl: 427,861 ticks
Buffer.BlockCopy: 421,929 ticks

Array.Copy: 420,556 ticks
Buffer.memcpyimpl: 421,541 ticks
Buffer.BlockCopy: 436,430 ticks

Array.Copy: 435,297 ticks
Buffer.memcpyimpl: 432,505 ticks
Buffer.BlockCopy: 441,493 ticks

现在再次更改顺序:

Buffer.memcpyimpl: 430,874 ticks
Buffer.BlockCopy: 429,730 ticks
Array.Copy: 432,746 ticks

Buffer.memcpyimpl: 415,943 ticks
Buffer.BlockCopy: 423,809 ticks
Array.Copy: 428,703 ticks

Buffer.memcpyimpl: 421,270 ticks
Buffer.BlockCopy: 428,262 ticks
Array.Copy: 434,940 ticks

Buffer.memcpyimpl: 423,506 ticks
Buffer.BlockCopy: 427,220 ticks
Array.Copy: 431,606 ticks

Buffer.memcpyimpl: 422,900 ticks
Buffer.BlockCopy: 439,280 ticks
Array.Copy: 432,649 ticks

或者,换句话说:他们非常有竞争力;一般来说,memcpyimpl 最快,但不一定值得担心。

【讨论】:

来吧,伙计,对它进行基准测试!我一直认为 Buffer.BlockCopy 更快,但我不确定了。 Hans Passant(在页面下方)声称两者执行完全相同的 CLR 代码:social.msdn.microsoft.com/Forums/en-US/netfxbcl/thread/… 我很想知道您最后的建议是否有效,如果有效,效果如何。 @MusiGenesis:我猜memcpyimpl 是要走的路吗? (虽然我以前使用过它,但我也没有对其进行基准测试。我现在就对其进行基准测试。) Hans Passant: 在 *** 还没有之前就回答了 *** 的问题!我认为这正式让他进入了 Jon Skeet 的领域。 应该注意memcpyimpl 方法至少在 .NET 4.5.1 中不再存在,它现在被称为 Memcpy 并且它有各种重载,所以你需要传入参数类型来解析你想要的方法。【参考方案2】:

您可以使用Array.Copy

编辑

Array.Copy 确实适用于多维数组:请参阅this topic。

【讨论】:

我查看了您的链接,但我的情况有所不同。源来自三个不同的一维数组。 dest 数组是一个 N x 3 数组,其中每个维度都包含一个源数组。【参考方案3】:

如果在 .NET Core 上运行,您可以考虑使用 source.AsSpan().CopyTo(destination)(但要注意 Mono)。

          Method |  Job | Runtime |      Mean |     Error |    StdDev | Ratio | RatiosD |
---------------- |----- |-------- |----------:|----------:|----------:|------:|--------:|
       ArrayCopy |  Clr |     Clr |  60.08 ns | 0.8231 ns | 0.7699 ns |  1.00 |    0.00 |
        SpanCopy |  Clr |     Clr |  99.31 ns | 0.4895 ns | 0.4339 ns |  1.65 |    0.02 |
 BufferBlockCopy |  Clr |     Clr |  61.34 ns | 0.5963 ns | 0.5578 ns |  1.02 |    0.01 |
                 |      |         |           |           |           |       |         |
       ArrayCopy | Core |    Core |  63.33 ns | 0.6843 ns | 0.6066 ns |  1.00 |    0.00 |
        SpanCopy | Core |    Core |  47.41 ns | 0.5399 ns | 0.5050 ns |  0.75 |    0.01 |
 BufferBlockCopy | Core |    Core |  59.89 ns | 0.4713 ns | 0.3936 ns |  0.94 |    0.01 |
                 |      |         |           |           |           |       |         |
       ArrayCopy | Mono |    Mono | 149.82 ns | 1.6466 ns | 1.4596 ns |  1.00 |    0.00 |
        SpanCopy | Mono |    Mono | 347.87 ns | 2.0589 ns | 1.9259 ns |  2.32 |    0.02 |
 BufferBlockCopy | Mono |    Mono |  61.52 ns | 1.1691 ns | 1.0364 ns |  0.41 |    0.01 |

【讨论】:

每列在哪里操作?【参考方案4】:

对于原始类型数组(如double),您可以快速复制,即使对于带有指针的多维数组也是如此。

在下面的代码中,我用值 1 到 100 初始化了一个二维数组 A[10,10]。然后我将这些值复制到一个一维数组 B[100]

unsafe class Program
 
    static void Main(string[] args)
    
        double[,] A = new double[10, 10];

        for(int i = 0; i < 10; i++)
        
            for(int j = 0; j < 10; j++)
            
                A[i, j] = 10 * i + j + 1;
            
        
        // A has   1 ,2 .. 10,  11, 12 .. 20, ..  .. 99, 100 
        double[] B = new double[10 * 10];

        if (A.Length == B.Length)
        
            fixed (double* pA = A, pB = B)
            
                for(int i = 0; i < B.Length; i++)
                
                    pB[i] = pA[i];
                
            
            // B has 1, 2, 3, 4 .. 100
        
    

它有多快。我的测试表明它比原生 C# 副本和Buffer.BlockCopy() 快很多倍。您可以根据自己的情况试一试,然后告诉我们。

编辑 1 我将复制与四种方法进行了比较。 1) 两个嵌套循环,2) 一个串行循环,3) 指针,4) BlockCopy。我测量了各种大小数组的每个刻度的副本数。

N =   10x  10 (cpy/tck) Nested = 50,  Serial = 33, Pointer =    100, Buffer =    16
N =   20x  20 (cpy/tck) Nested = 133, Serial = 40, Pointer =    400, Buffer =   400
N =   50x  50 (cpy/tck) Nested = 104, Serial = 40, Pointer =   2500, Buffer =  2500
N =  100x 100 (cpy/tck) Nested = 61,  Serial = 41, Pointer =  10000, Buffer =  3333
N =  200x 200 (cpy/tck) Nested = 84,  Serial = 41, Pointer =  40000, Buffer =  2666
N =  500x 500 (cpy/tck) Nested = 69,  Serial = 41, Pointer = 125000, Buffer =  2840
N = 1000x1000 (cpy/tck) Nested = 33,  Serial = 45, Pointer = 142857, Buffer =  1890
N = 2000x2000 (cpy/tck) Nested = 30,  Serial = 43, Pointer = 266666, Buffer =  1826
N = 5000x5000 (cpy/tck) Nested = 21,  Serial = 42, Pointer = 735294, Buffer =  1712

这里很清楚谁是赢家。指针复制比任何其他方法都要好几个数量级。

编辑 2 显然,我不公平地利用了编译器/JIT 优化,因为当我将循环移到代表后面以平衡竞争环境时,数字发生了巨大变化。

N =   10x  10 (cpy/tck) Nested =  0, Serial =  0, Pointer =      0, Buffer =     0
N =   20x  20 (cpy/tck) Nested = 80, Serial = 14, Pointer =    100, Buffer =   133
N =   50x  50 (cpy/tck) Nested =147, Serial = 15, Pointer =    277, Buffer =  2500
N =  100x 100 (cpy/tck) Nested = 98, Serial = 15, Pointer =    285, Buffer =  3333
N =  200x 200 (cpy/tck) Nested =106, Serial = 15, Pointer =    272, Buffer =  3076
N =  500x 500 (cpy/tck) Nested =106, Serial = 15, Pointer =    276, Buffer =  3125
N = 1000x1000 (cpy/tck) Nested =101, Serial = 11, Pointer =    199, Buffer =  1396
N = 2000x2000 (cpy/tck) Nested =105, Serial =  9, Pointer =    186, Buffer =  1804
N = 5000x5000 (cpy/tck) Nested =102, Serial =  8, Pointer =    170, Buffer =  1673

缓冲的副本在此处位于顶部(感谢@Mehrdad),指针副本位于第二位。现在的问题是为什么指针复制不如缓冲区方法快?

【讨论】:

Buffer.BlockCopy 在内部使用 memmove,我猜它在一个汇编命令中复制任意数量的字节。这应该比使用指针快得多,指针在一条指令中只复制 1 个 double 并且需要循环多次。 是的,这是有道理的。缓冲区小于 CPU 缓存以保持快速是有意义的。不知是否根据CPU架构调整。 缓冲区复制很可能使用数据并行 SIMD/SSE CPU 指令一次最多移动 512 位,每条指令移动 8 个双精度数【参考方案5】:

如果以下形式的锯齿状数组可以工作,则可以避免复制:

double[][] leftNode = new double[3][];
leftNode[0] = sortedIndex;
leftNode[1] = sortedInstances;
leftNode[2] = sortedLabels;

【讨论】:

以上是关于在 C# 中复制数组的任何更快的方法?的主要内容,如果未能解决你的问题,请参考以下文章

通过计算复制 C 数组的更快方法

在 C# 中用较小的数组复制/填充大数组的最佳方法是啥?

c#将多个数组复制/合并到一个数组中的更好方法

如何更快地在 Java 中创建/复制对象? [复制]

有没有更快的方法来达到相同的结果? [复制]

如何在不复制 C# 的情况下将结构数组元素提取到变量中?