返回文章列表文章索引阅读全文
STARBLOG / 阅读手记
C#

C# High-Performance Array Copy Experiment

Preface Yesterday wc(Wyu_Cnk) asked a question Is there a more elegant way to copy multidimensional arrays in C#? Didn't they ask the right person? I've been working on image processing recently and dealing with memory, so I immediately thought of doing memory copies in C# just like in C/C++. Elegant? no, what we want is to show off, and the performance has to be strong too 🕶 Concepts First, let's clarify A Multi-dimensional Array in C# looks like this byte[,] arr = new by

曦远 (DealiAxy)约 21 分钟
目录10 个章节

Preface

Yesterday wc(Wyu_Cnk) asked a question

Is there a more elegant way to copy multidimensional arrays in C#?

Didn't they ask the right person? I've been working on image processing recently and dealing with memory, so I immediately thought of doing memory copies in C# just like in C/C++.

Elegant? no, what we want is to show off, and the performance has to be strong too 🕶

Concepts

First, let's clarify

A Multi-dimensional Array in C# looks like this

byte[,] arr = new byte[10, 10];

The following syntax is a Jagged Array, which is an array of arrays

byte[][] arr = new byte[10][];

For the specific differences, please see the references at the end of this article~

Getting Started

Next I'll introduce several methods for copying arrays, then compare the performance of the different implementations

Let's define some constants: SIZE is the array size, and COUNT is the number of loop iterations we'll use for the copy tests

const int COUNT = 32, SIZE = 32 << 20;

A bit-shift is used here: left-shifting 32 by 20 bits appends 20 zeros to the binary representation of 32, which is equivalent to 32*2^20. It's just a way to define a fairly large number. Computers nowadays are so powerful that copying a smaller array would be too fast to see any difference.

Next, define several arrays. Here I've written five pairs of one-dimensional arrays. Each different array-copy method is tested with a different array, so as to avoid CPU caching.

private 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],
    dSource = new byte[SIZE],
    dTarget = new byte[SIZE],
    eSource = new byte[SIZE],
    eTarget = new byte[SIZE];

Then we'll test all of these array-copy methods

  • Clone approach: array.Clone()
  • Linq: array.Select(x=>x).ToArray()
  • Array.Copy()
  • Buffer.BlockCopy()
  • Buffer.MemoryCopy()

Clone Approach

In C#, any object that implements the ICloneable interface has a Clone method

So arrays can also be copied this way

It's simple, just var newArray = (byte[])array.Clone()

The code is as follows

static void TestArrayClone() {
    var sw = Stopwatch.StartNew();
    sw.Start();
    for (var i = 0; i < COUNT; i++) {
        dTarget = (byte[])dSource.Clone();
    }

    sw.Stop();
    Console.WriteLine("Array.Clone: {0:N0} ticks, {1} ms", sw.ElapsedTicks, sw.ElapsedMilliseconds);
}

Stopwatch is used here to record the execution time. The other copy methods below also have this. We'll use the ticks and milliseconds computed this way to compare the performance differences among the implementations.

Linq Approach

You don't even need to test it to know this approach is the slowest

It just iterates through the elements one by one, then reconstructs a new array

The code is as follows

eTarget = eSource.Select(x => x).ToArray();

Array.Copy()

Use the static method Array.Copy() to copy arrays

Tip: Performance is pretty good, and it's convenient to use

The code is as follows; you only need to specify the length

Array.Copy(cSource, cTarget, SIZE);

Or use another overload that lets you specify the offset of each of the two arrays separately

Array.Copy(cSource, 0, cTarget, 0, SIZE);

Buffer.BlockCopy()

The Buffer class is used to manipulate arrays of primitive types

Manipulates arrays of primitive types.

The code is as follows

Buffer.BlockCopy(bSource, 0, bTarget, 0, SIZE);

Same as the second overload of Array.Copy above, you need to specify the offset of each of the two arrays separately

Buffer.MemoryCopy()

This is an unsafe method that requires pointers 😀 In theory it has the best performance

This is my favorite method~~(high-class)~~

To use unsafe code, first enable the allow unsafe code option in the compiler settings.

The function signature of this MemoryCopy method is as follows

static unsafe void MemoryCopy(void* source, void* destination, long destinationSizeInBytes, long sourceBytesToCopy)

The first two parameters are pointer types, and the latter two are lengths. Note that these are byte counts, not the number of array elements

In C#, a byte occupies 8 bits, which is exactly one byte, so you can just use the element count. If it's an array of another type, you have to calculate the byte count based on the type's size, then pass that in.

The code is as follows. Add the unsafe keyword to the function definition in order to use fixed blocks and pointers

static unsafe void TestBufferMemoryCopy() {
    var sw = Stopwatch.StartNew();
    fixed (byte* pSrc = fSource, pDest = fTarget) {
        for (int i = 0; i < COUNT; i++) {
            Buffer.MemoryCopy(pSrc, pDest, SIZE, SIZE);
        }
    }

    Console.WriteLine("Buffer.MemoryCopy (2d): {0:N0} ticks, {1} ms", sw.ElapsedTicks, sw.ElapsedMilliseconds);
}

Then

While searching for materials, I also found that some people used the Buffer.Memcpy method, but this is an internal method that isn't publicly exposed, so you have to use some black magic to call it

I tinkered with it for a long time and finally came up with the code to call this non-public method

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

internal class Program {
    private static Memcpy memcpy;
    static Program() {
        var methodInfo = typeof(Buffer).GetMethod(
            "Memcpy",
            BindingFlags.Static | BindingFlags.NonPublic,
            null,
            new Type[] { typeof(byte*), typeof(byte*), typeof(int) },
            null
        );
        if (methodInfo == null) {
            Console.WriteLine("init failed! method is not found.");
            return;
        }

        memcpy = (Memcpy)Delegate.CreateDelegate(typeof(Memcpy), methodInfo);
    }
}

In actual tests, the performance of this Memcpy and MemoryCopy is about the same

I took a look at the .NetCore source code

Sure enough, the two implementations are basically the same

// Used by ilmarshalers.cpp
internal static unsafe void Memcpy(byte* dest, byte* src, int len)
{
    Debug.Assert(len >= 0, "Negative length in memcpy!");
    Memmove(ref *dest, ref *src, (nuint)(uint)len /* force zero-extension */);
}

The other one

public static unsafe void MemoryCopy(void* source, void* destination, long destinationSizeInBytes, long sourceBytesToCopy)
{
    if (sourceBytesToCopy > destinationSizeInBytes) { ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.sourceBytesToCopy);
    }
    Memmove(ref *(byte*)destination, ref *(byte*)source, checked((nuint)sourceBytesToCopy));
}

Both of these ultimately call the Memmove method

The difference is just that the parameters of these two methods are different.

benchmark

Performance test results

Array.Copy: 49,923,612 ticks, 49 ms
Buffer.BlockCopy: 52,497,377 ticks, 52 ms
Buffer.Memcpy: 49,067,555 ticks, 49 ms
Buffer.MemoryCopy (2d): 48,982,014 ticks, 48 ms
Array.Clone: 360,640,218 ticks, 360 ms
Linq: 1,988,890,052 ticks, 1988 ms

Array.Copy: 48,653,699 ticks, 48 ms
Buffer.BlockCopy: 48,040,093 ticks, 48 ms
Buffer.Memcpy: 47,818,057 ticks, 47 ms
Buffer.MemoryCopy (2d): 49,084,413 ticks, 49 ms
Array.Clone: 406,848,666 ticks, 406 ms
Linq: 1,943,498,307 ticks, 1943 ms

Array.Copy: 48,943,429 ticks, 48 ms
Buffer.BlockCopy: 47,989,824 ticks, 47 ms
Buffer.Memcpy: 48,053,817 ticks, 48 ms
Buffer.MemoryCopy (2d): 49,065,368 ticks, 49 ms
Array.Clone: 364,339,126 ticks, 364 ms
Linq: 1,999,189,800 ticks, 1999 ms

Array.Copy: 49,679,913 ticks, 49 ms
Buffer.BlockCopy: 48,651,877 ticks, 48 ms
Buffer.Memcpy: 48,262,443 ticks, 48 ms
Buffer.MemoryCopy (2d): 49,683,361 ticks, 49 ms
Array.Clone: 429,384,291 ticks, 429 ms
Linq: 1,932,109,712 ticks, 1932 ms

Which method you should use to copy arrays is obvious at a glance now, right~ 😃

References

_EOF_

C# High-Performance Array Copy Experiment

本文作者:
程序设计实验室
版权声明:
本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

相关文章

C#学习(一)委托的概念和使用

前言 C#学习系列是根据我以前的笔记整理出来复习,顺便发一下文章做个记录。 先引用一个介绍: C# 中的委托(Delegate)类似于 C 或 C++ 中函数的指针。委托(Delegate) 是存有对某个方法的引用的一种引用类型变量。引用可在运行时被改变。 委托(Delegate)特别用于实现事件和回调方法。所有的委托(Delegate)都派生自 System.Delegate 类。 根据使用其他

2021-12-18 · C# · 约 1 分钟

C#学习(三)深入理解委托、匿名方法和-Lambda-表达式

前言 lambda的概念建立在委托的基础上,委托,实现了类型安全的回调方法。在.NET 中回调无处不在,所以委托也无处不在,事件模型建立在委托机制上,Lambda 表达式本质上就是一种匿名委托。本文中将完成一次关于委托的旅行,全面阐述委托及其核心话题,逐一梳理委托、委托链、事件、匿名方法和 Lambda 表达式。 委托的定义 了解委托,从其定义开始,通常一个委托被声明为: public deleg

2021-12-18 · C# · 约 1 分钟

讨论

共 0 条

良言一句三冬暖,恶语伤人六月寒。

加载评论中…