Array.Copy 대 Buffer.BlockCopy


124

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


3
잊지 마세요 Marshal.Copy:-). 음, Array.Copy참조 유형, 복합 값 유형 및 유형이 변경되지 않는 경우 Buffer.BlockCopy값 유형, 바이트 배열 및 바이트 매직 간의 "변환"에 사용하십시오. F.ex. StructLayout당신이 무엇을하는지 안다면 와의 조합 은 매우 강력합니다. 성능에 관해서는 memcpy/에 대한 관리되지 않는 호출 cpblk이 가장 빠른 것 같습니다 . code4k.blogspot.nl/2010/10/… 참조하십시오 .
atlaste

1
나는 byte[]. 릴리스 버전에는 차이가 없습니다. 때때로 Array.Copy, 때로는 Buffer.BlockCopy(약간) 더 빠릅니다.
Bitterblue

아래에 게시 된 새로운 포괄적 인 답변입니다. 버퍼 크기가 작은 경우에는 일반적으로 명시 적 루프 복사가 가장 좋습니다.
Special Sauce

나는 그들이 항상 똑같은 일을한다고 생각하지 않는다 – 당신은 Array.Copy를 사용하여 Int 배열을 바이트 배열로 복사 할 수 없다
mcmillab

Array.Copy예를 들어 동일한 순위 배열 만 복사 할 수 있습니다.
astrowalker

답변:


59

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


9
완전히 동의합니다. Buffer.BlockCopy에는 오류가 발생할 여지가 너무 많습니다. 간단하게 유지하고 주스가 어디에 있는지 (프로파일 링) 알 때까지 프로그램에서 주스를 짜내려고하지 마십시오.
Stephen

5
byte []를 다루고 있다면 어떨까요? BlockCopy와 관련된 다른 문제가 있습니까?
thecoop

4
@thecoop : byte []를 다루는 경우 BlockCopy를 사용하는 것이 좋습니다. "byte"의 정의가 나중에 바이트가 아닌 다른 것으로 변경되지 않는 한, 아마도 다른 부분에 상당히 부정적인 영향을 미칠 것입니다. 어쨌든 귀하의 코드. :) 유일한 다른 잠재적 인 문제는 BlockCopy가 곧바로 바이트를 수행하기 때문에 엔디안을 고려하지 않는다는 것입니다. 그러나 이것은 Windows가 아닌 컴퓨터에서만 작동하며 코드를 망친 경우에만 작동합니다. 첫 번째 장소. 또한 모노를 사용하는 경우 약간의 이상한 차이가있을 수 있습니다.
MusiGenesis

6
내 테스트에서 Array.Copy ()는 Buffer.BlockCopy ()와 성능면에서 매우 유사합니다. Buffer.BlockCopy는 640 요소 바이트 배열을 다룰 때 일관되게 10 % 미만 더 빠릅니다 (가장 관심있는 정렬입니다). 그러나 데이터, 데이터 유형, 배열 크기 등에 따라 달라질 수 있으므로 자체 데이터로 자체 테스트를 수행해야합니다. 두 방법 모두 Array.Clone ()을 사용하는 것보다 약 3 배 빠르며 for 루프에서 복사하는 것보다 20 배 빠릅니다.
Ken Smith

3
@KevinMiller : 어, UInt16요소 당 2 바이트입니다. 이 배열을 배열의 요소 수와 함께 BlockCopy에 전달하면 물론 배열의 절반 만 복사됩니다. 이것이 제대로 작동하려면 각 요소의 크기 (2) 에 요소 수를 곱한 값을 길이 매개 변수 로 전달해야합니다 . msdn.microsoft.com/en-us/library/…INT_SIZE예제에서 검색 하십시오 .
MusiGenesis

129

전주곡

나는 늦게 파티에 참여하고 있지만, 조회수가 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() 에서 찾을 수 있습니다 .

여담으로, 그것은 또한 .NET이 때 (100)의 배열 길이 주위 것으로 나타났다 Array.Clear()(로 설정을 배열의 명시 적 루프 할당 청소를 이길 시작 첫째 false, 0또는 null). 이것은 위의 유사한 결과와 일치합니다. 이러한 개별 벤치 마크는 온라인에서 발견되었습니다. manski.net/2012/12/net-array-clear-vs-arrayx-0-performance
Special Sauce

버퍼 크기를 말할 때; 바이트 또는 요소 수를 의미합니까?
dmarra

위의 답변에서 "버퍼 길이"와 "버퍼 크기"는 일반적으로 요소 수를 나타냅니다.
Special Sauce

소스 오프셋에서 5 바이트 씩 읽는 버퍼에 약 8 바이트의 데이터를 자주 복사해야하는 예가 있습니다. 명시 적 루프 복사본이 훨씬 더 빠르다면 Buffer.BlockCopy 또는 Array.Copy를 사용했습니다. Loop Results for 1000000 iterations 17.9515ms. Buffer.BlockCopy Results for 1000000 iterations 39.8937ms. Array.Copy Results for 1000000 iterations 45.9059ms 그러나 복사 크기가 20 바이트를 초과하면 명시 적 루프가 상당히 느립니다.
Tod Cunningham

@TodCunningham, 8 바이트 데이터? 긴 등가물을 의미합니까? 단일 요소를 캐스팅하고 복사하거나 (빠르게) 해당 루프를 수동으로 펼칩니다.
astrowalker

67

사용하는 것이 합리적 일 때의 또 다른 예는 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 ++에서 흔히 볼 수있는 일종의 메모리 관리에 가깝습니다 .


6
멋진 아이디어입니다. 엔디안 문제에 부딪힌 적이 있습니까?
Phillip

예, 시나리오에 따라 그 문제가 발생할 수 있다고 생각합니다. 내 자신의 시나리오는 일반적으로 (a) 동일한 컴퓨터에서 바이트 배열과 짧은 배열 사이를 앞뒤로 전환해야하거나 (b) 동일한 컴퓨터에 데이터를 보내고 있다는 것을 알고 있습니다. endianness, 내가 원격 측을 제어합니다. 그러나 원격 시스템이 호스트 순서가 아닌 네트워크 순서로 데이터를 보낼 것으로 예상하는 프로토콜을 사용하고 있다면이 접근 방식은 문제를 일으킬 것입니다.
Ken Smith

Ken은 또한 자신의 블로그에 BlockCopy에 대한 기사가 있습니다. blog.wouldbetheologian.com/2011/11/…
Drew Noakes

4
.Net Core 2.1 이후로 복사하지 않고도이 작업을 수행 할 수 있습니다. MemoryMarshal.AsBytes<T>또는 MemoryMarshal.Cast<TFrom, TTo>한 프리미티브의 시퀀스를 다른 프리미티브의 후속으로 해석 할 수 있습니다.
Timo

16

내 테스트에 따르면 성능은 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

1
이 답변이 댓글에 가깝다는 점에 대해 죄송하지만 댓글이 너무 깁니다. 합의가 Buffer.BlockCopy가 성능 측면에서 더 나은 것 같았 기 때문에 테스트로 합의를 확인할 수 없다는 것을 모두가 알고 있어야한다고 생각했습니다.
Kevin

10
테스트 방법에 문제가 있다고 생각합니다. 당신이 주목하고있는 대부분의 시간차는 애플리케이션 회전, 자체 캐싱, JIT 실행 등의 결과입니다. 더 작은 버퍼로 시도하되 수천 번 시도하십시오. 그런 다음 루프 내에서 전체 테스트를 6 회 반복하고 마지막 실행에만주의를 기울이십시오. 내 테스트에는 640 바이트 배열에 대해 Array.Copy ()보다 5 % 더 빠르게 실행되는 Buffer.BlockCopy ()가 있습니다. 그다지 빠르지는 않지만 조금.
Ken Smith

2
특정 문제에 대해 동일하게 측정 했지만 Array.Copy ()와 Buffer.BlockCopy () 사이에 성능 차이가 없음을 알 수있었습니다 . 어떤 경우 BlockCopy 실제로 내 응용 프로그램을 살해하는 unsafey을 도입 한 인스턴스에.
gatopeich

1
Array.Copy를 추가하는 것과 마찬가지로 소스 위치에 대해 long을 지원하므로 바이트 배열 로 분할하면 범위를 벗어난 예외가 발생하지 않습니다.
Alxwest

2
내가 방금 만든 테스트를 기반으로 ( bitbucket.org/breki74/tutis/commits/… ) 바이트 배열을 다룰 때 두 가지 방법간에 실질적인 성능 차이가 없다고 말할 수 있습니다.
Igor Brejc 2014 년

4

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에는이 기능이 없습니다.


2
BlockCopy를 사용한 테스트 결과는 예상치 못한 것이 아닙니다. 블록 복사는 한 번에 한 바이트가 아닌 한 번에 데이터 청크를 복사하려고하기 때문입니다. 32 비트 시스템에서는 한 번에 4 바이트를 복사하고 64 비트 시스템에서는 한 번에 8 바이트를 복사합니다.
Pharap

따라서 정의되지 않은 동작이 예상됩니다.
binki 2015

2

이 주장을 고려하기 위해이 벤치 마크를 작성하는 방법을주의하지 않으면 쉽게 오도 될 수 있습니다. 나는 이것을 설명하기 위해 매우 간단한 테스트를 작성했습니다. 아래의 테스트에서 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


5
답변에서 타이밍 결과가 보이지 않습니다. 콘솔 출력을 포함하십시오.
ToolmakerSteve

0

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);
    }

3
불쾌하지는 않지만 테스트 결과는 실제로 도움이되지 않습니다.) 우선 "20ms 더 빠름"은 전체 시간을 알지 못하면 아무 것도 말해주지 않습니다. 또한이 두 테스트를 매우 다른 방식으로 수행했습니다. BlockCopy 케이스에는 추가 메서드 호출과 Array.Copy 케이스에없는 대상 배열 할당이 있습니다. 멀티 스레딩 변동 (가능한 작업 전환, 코어 전환)으로 인해 테스트를 실행할 때마다 다른 결과를 쉽게 얻을 수 있습니다.
Bunny83 2014

@ Bunny83 댓글 주셔서 감사합니다. 이제 더 공정한 비교를 제공해야하는 타이머 위치를 약간 수정했습니다. 그리고 blockcopy가 array.copy보다 빠르지 않다는 사실에 조금 놀랐습니다.
stt106 2014
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.