[.net] Array.Copy 대 Buffer.BlockCopy

Array.CopyBuffer.BlockCopy는 모두 동일한 작업을 수행하지만 BlockCopy빠른 바이트 수준의 기본 배열 복사를 목표로하는 반면 Copy범용 구현입니다. 내 질문은-어떤 상황에서 사용해야 BlockCopy합니까? 기본 유형 배열을 복사 할 때 언제라도 사용해야합니까, 아니면 성능을 위해 코딩하는 경우에만 사용해야합니까? Buffer.BlockCopyover 사용 에 대해 본질적으로 위험한 것이 Array.Copy있습니까?



답변

의 매개 변수 Buffer.BlockCopy는 인덱스 기반이 아니라 바이트 기반이므로을 사용하는 것보다 코드를 망칠 가능성이 더 높으므로 코드 의 성능이 중요한 섹션 Array.Copy에서만 사용 Buffer.BlockCopy합니다.


답변

전주곡

나는 늦게 파티에 참여하고 있지만, 조회수가 3 만 2 천명으로이 문제를 바로 잡을 가치가 있습니다. 지금까지 게시 된 답변의 대부분의 마이크로 벤치마킹 코드는 테스트 루프에서 메모리 할당을 이동하지 않고 (심각한 GC 아티팩트를 유발 함), 변수 대 결정적 실행 흐름, JIT 워밍업을 테스트하지 않는 등 하나 이상의 심각한 기술적 결함이 있습니다. 테스트 내 변동성을 추적하지 않습니다. 또한 대부분의 답변은 다양한 버퍼 크기와 다양한 기본 유형의 영향을 테스트하지 않았습니다 (32 비트 또는 64 비트 시스템과 관련하여). 이 질문을 좀 더 포괄적으로 다루기 위해, 저는 가능한 한 대부분의 일반적인 “고장난”을 줄이는 사용자 지정 마이크로 벤치마킹 프레임 워크에 연결했습니다. 테스트는 32 비트 컴퓨터와 64 비트 컴퓨터 모두에서 .NET 4.0 릴리스 모드로 실행되었습니다. 결과는 평균 20 회 테스트 실행으로, 각 실행에는 방법 당 1 백만 번의 시행이있었습니다. 테스트 된 기본 유형은byte(1 바이트), int(4 바이트) 및 double(8 바이트). 세 가지 방법은 테스트되었습니다 : Array.Copy(), Buffer.BlockCopy()루프, 그리고 간단한 당 인덱스 할당. 여기에 게시하기에는 데이터가 너무 방대하므로 중요한 사항을 요약하겠습니다.

테이크 아웃

  • 당신의 버퍼 길이가 75 ~ 100 이하에 대한 경우, 명시 적 루프 복사 루틴은 더 빨리 (약 5 % 정도) 중 하나 이상의 보통 Array.Copy()또는 Buffer.BlockCopy()32 비트 및 64 비트 시스템에서 테스트 3 개 원시 유형. 또한 명시 적 루프 복사 루틴은 두 가지 대안에 비해 성능의 변동성이 눈에 띄게 낮습니다. 좋은 성능은 거의 확실하게 메서드 호출 오버 헤드가없는 CPU L1 / L2 / L3 메모리 캐싱에 의해 악용되는 참조 지역성 때문 입니다.
    • 들어 double버퍼 32 비트 시스템에서만 : 명시 적 루프 복사 루틴은 모든 버퍼 모두 대안이 100,000까지 시험 지정 크기보다 낫다. 개선은 다른 방법보다 3-5 % 우수합니다. 의 성능 때문입니다 Array.Copy()과는 Buffer.BlockCopy()완전히 네이티브 32 비트 폭을 통과에 따라 저하된다. 따라서 동일한 효과가 long버퍼 에도 적용될 것이라고 가정합니다 .
  • 버퍼 크기가 ~ 100을 초과하는 경우 명시 적 루프 복사는 다른 두 가지 방법보다 훨씬 느려집니다 (방금 언급 한 특정 예외가 있음). 차이점은 byte[]큰 버퍼 크기에서 명시 적 루프 복사가 7 배 이상 느려질 수있는에서 가장 두드러 집니다.
  • 일반적으로, 3 종류의 프리미티브에 대해 시험 및 모든 버퍼 크기에 걸쳐, Array.Copy()Buffer.BlockCopy()거의 동일하게 수행 하였다. 평균적 Array.Copy()으로 약 2 % 이하의 시간이 소요되는 매우 경미한 가장자리가있는 것 같습니다 (그러나 0.2 %-0.5 % 더 나은 것이 일반적입니다) Buffer.BlockCopy(). 알 수없는 이유로 Buffer.BlockCopy()Array.Copy(). 이 효과는 여러 완화를 시도하고 그 이유에 대한 작동 가능한 이론이 없음에도 불구하고 제거 할 수 없습니다.
  • Array.Copy()“스마트”하고 더 일반적이며 훨씬 안전한 방법 이기 때문에 매우 약간 더 빠르고 평균적으로 변동성이 적기 때문에 Buffer.BlockCopy()거의 모든 일반적인 경우에 선호되어야합니다 . Buffer.BlockCopy()훨씬 더 나은 유일한 사용 사례 는 소스 및 대상 배열 값 유형이 다른 경우입니다 (Ken Smith의 답변에서 지적했듯이). 이 시나리오는 흔하지 않지만 Array.Copy().NET의 직접 캐스팅에 비해 지속적인 “안전한”값 유형 캐스팅으로 인해 여기에서 성능이 매우 떨어질 수 있습니다 Buffer.BlockCopy().
  • 동일한 유형의 배열 복사 Array.Copy()보다 빠른 StackOverflow 외부의 추가 증거는 여기Buffer.BlockCopy() 에서 찾을 수 있습니다 .

답변

사용하는 것이 합리적 일 때의 또 다른 예는 Buffer.BlockCopy()원시 배열 (예 : shorts)이 제공되고이를 바이트 배열로 변환해야하는 경우 (예 : 네트워크를 통한 전송)입니다. Silverlight AudioSink의 오디오를 처리 할 때이 방법을 자주 사용합니다. 샘플을 short[]배열 로 제공 하지만에 byte[]제출하는 패킷을 빌드 할 때 배열 로 변환 해야합니다 Socket.SendAsync(). 를 사용 BitConverter하고 배열을 하나씩 반복 할 수 있지만 이렇게하는 것이 훨씬 빠릅니다 (내 테스트에서는 약 20 배).

Buffer.BlockCopy(shortSamples, 0, packetBytes, 0, shortSamples.Length * sizeof(short)).  

그리고 같은 트릭이 반대로 작동합니다.

Buffer.BlockCopy(packetBytes, readPosition, shortSamples, 0, payloadLength);

이것은 안전한 C # (void *)에서 C 및 C ++에서 흔히 볼 수있는 일종의 메모리 관리에 가깝습니다 .


답변

내 테스트에 따르면 성능은 Array.Copy보다 Buffer.BlockCopy를 선호하는 이유 가 아닙니다 . 내 테스트에서 Array.Copy는 실제로 Buffer.BlockCopy보다 빠릅니다 .

var buffer = File.ReadAllBytes(...);

var length = buffer.Length;
var copy = new byte[length];

var stopwatch = new Stopwatch();

TimeSpan blockCopyTotal = TimeSpan.Zero, arrayCopyTotal = TimeSpan.Zero;

const int times = 20;

for (int i = 0; i < times; ++i)
{
    stopwatch.Start();
    Buffer.BlockCopy(buffer, 0, copy, 0, length);
    stopwatch.Stop();

    blockCopyTotal += stopwatch.Elapsed;

    stopwatch.Reset();

    stopwatch.Start();
    Array.Copy(buffer, 0, copy, 0, length);
    stopwatch.Stop();

    arrayCopyTotal += stopwatch.Elapsed;

    stopwatch.Reset();
}

Console.WriteLine("bufferLength: {0}", length);
Console.WriteLine("BlockCopy: {0}", blockCopyTotal);
Console.WriteLine("ArrayCopy: {0}", arrayCopyTotal);
Console.WriteLine("BlockCopy (average): {0}", TimeSpan.FromMilliseconds(blockCopyTotal.TotalMilliseconds / times));
Console.WriteLine("ArrayCopy (average): {0}", TimeSpan.FromMilliseconds(arrayCopyTotal.TotalMilliseconds / times));

출력 예 :

bufferLength: 396011520
BlockCopy: 00:00:02.0441855
ArrayCopy: 00:00:01.8876299
BlockCopy (average): 00:00:00.1020000
ArrayCopy (average): 00:00:00.0940000


답변

ArrayCopy는 BlockCopy보다 똑똑합니다. 소스와 대상이 동일한 배열 인 경우 요소를 복사하는 방법을 알아냅니다.

int 배열을 0,1,2,3,4로 채우고 적용하면 :

Array.Copy (배열, 0, 배열, 1, array.Length-1);

예상대로 0,0,1,2,3으로 끝납니다.

BlockCopy로 이것을 시도하면 0,0,2,3,4를 얻습니다. array[0]=-1그 후에 할당하면 예상대로 -1,0,2,3,4가되지만 배열 길이가 6과 같이 짝수이면 -1,256,2,3,4,5가됩니다. 위험한 물건. 한 바이트 배열을 다른 배열로 복사하는 것 외에는 BlockCopy를 사용하지 마십시오.

Array.Copy 만 사용할 수있는 또 다른 경우가 있습니다. 배열 크기가 2 ^ 31보다 긴 경우입니다. Array.Copy에는 long크기 매개 변수 가있는 오버로드가 있습니다. BlockCopy에는이 기능이 없습니다.


답변

이 주장을 고려하기 위해이 벤치 마크를 작성하는 방법을주의하지 않으면 쉽게 오도 될 수 있습니다. 나는 이것을 설명하기 위해 매우 간단한 테스트를 작성했습니다. 아래의 테스트에서 Buffer.BlockCopy를 먼저 시작하거나 Array.Copy를 시작하는 것 사이에서 테스트 순서를 바꾸면 가장 먼저 진행되는 것이 거의 항상 가장 느립니다 (가장 가까운 것임에도 불구하고). 이것은 내가 단순히 테스트를 여러 번 실행하지 않는 여러 가지 이유 때문에 다른 하나가 정확한 결과를 제공하지 못한다는 것을 의미합니다.

나는 1000000 순차 이중 배열에 대해 각각 1000000 시도로 테스트를 유지하는 데 의지했습니다. 그러나 나는 처음 900000 사이클을 무시하고 나머지를 평균화합니다. 이 경우 버퍼가 우수합니다.

private static void BenchmarkArrayCopies()
        {
            long[] bufferRes = new long[1000000];
            long[] arrayCopyRes = new long[1000000];
            long[] manualCopyRes = new long[1000000];

            double[] src = Enumerable.Range(0, 1000000).Select(x => (double)x).ToArray();

            for (int i = 0; i < 1000000; i++)
            {
                bufferRes[i] = ArrayCopyTests.ArrayBufferBlockCopy(src).Ticks;
            }

            for (int i = 0; i < 1000000; i++)
            {
                arrayCopyRes[i] = ArrayCopyTests.ArrayCopy(src).Ticks;
            }

            for (int i = 0; i < 1000000; i++)
            {
                manualCopyRes[i] = ArrayCopyTests.ArrayManualCopy(src).Ticks;
            }

            Console.WriteLine("Loop Copy: {0}", manualCopyRes.Average());
            Console.WriteLine("Array.Copy Copy: {0}", arrayCopyRes.Average());
            Console.WriteLine("Buffer.BlockCopy Copy: {0}", bufferRes.Average());

            //more accurate results - average last 1000

            Console.WriteLine();
            Console.WriteLine("----More accurate comparisons----");

            Console.WriteLine("Loop Copy: {0}", manualCopyRes.Where((l, i) => i > 900000).ToList().Average());
            Console.WriteLine("Array.Copy Copy: {0}", arrayCopyRes.Where((l, i) => i > 900000).ToList().Average());
            Console.WriteLine("Buffer.BlockCopy Copy: {0}", bufferRes.Where((l, i) => i > 900000).ToList().Average());
            Console.ReadLine();
        }

public class ArrayCopyTests
    {
        private const int byteSize = sizeof(double);

        public static TimeSpan ArrayBufferBlockCopy(double[] original)
        {
            Stopwatch watch = new Stopwatch();
            double[] copy = new double[original.Length];
            watch.Start();
            Buffer.BlockCopy(original, 0 * byteSize, copy, 0 * byteSize, original.Length * byteSize);
            watch.Stop();
            return watch.Elapsed;
        }

        public static TimeSpan ArrayCopy(double[] original)
        {
            Stopwatch watch = new Stopwatch();
            double[] copy = new double[original.Length];
            watch.Start();
            Array.Copy(original, 0, copy, 0, original.Length);
            watch.Stop();
            return watch.Elapsed;
        }

        public static TimeSpan ArrayManualCopy(double[] original)
        {
            Stopwatch watch = new Stopwatch();
            double[] copy = new double[original.Length];
            watch.Start();
            for (int i = 0; i < original.Length; i++)
            {
                copy[i] = original[i];
            }
            watch.Stop();
            return watch.Elapsed;
        }
    }

https://github.com/chivandikwa/Random-Benchmarks


답변

BlockCopy가 Array.Copy보다 ‘성능’이점이 없음을 다시 보여주는 테스트 사례를 추가하고 싶습니다. 내 컴퓨터의 릴리스 모드에서 동일한 성능을 보이는 것 같습니다 (둘 다 5 천만 개의 정수를 복사하는 데 약 66ms가 걸립니다). 디버그 모드에서 BlockCopy는 약간 더 빠릅니다.

    private static T[] CopyArray<T>(T[] a) where T:struct
    {
        T[] res = new T[a.Length];
        int size = Marshal.SizeOf(typeof(T));
        DateTime time1 = DateTime.Now;
        Buffer.BlockCopy(a,0,res,0, size*a.Length);
        Console.WriteLine("Using Buffer blockcopy: {0}", (DateTime.Now - time1).Milliseconds);
        return res;
    }

    static void Main(string[] args)
    {
        int simulation_number = 50000000;
        int[] testarray1 = new int[simulation_number];

        int begin = 0;
        Random r = new Random();
        while (begin != simulation_number)
        {
            testarray1[begin++] = r.Next(0, 10000);
        }

        var copiedarray = CopyArray(testarray1);

        var testarray2 = new int[testarray1.Length];
        DateTime time2 = DateTime.Now;
        Array.Copy(testarray1, testarray2, testarray1.Length);
        Console.WriteLine("Using Array.Copy(): {0}", (DateTime.Now - time2).Milliseconds);
    }