Development Tip

C #에서 얕은 복사를 수행하는 가장 빠른 방법

yourdevel 2021. 1. 9. 11:04
반응형

C #에서 얕은 복사를 수행하는 가장 빠른 방법


C #에서 얕은 복사를 수행하는 가장 빠른 방법은 무엇일까요? 얕은 복사를 수행하는 방법은 두 가지뿐입니다.

  1. MemberwiseClone
  2. 각 필드를 하나씩 복사 (수동)

(2)가 (1)보다 빠르다는 것을 알았습니다. 얕은 복사를 수행하는 다른 방법이 있는지 궁금합니다.


이것은 많은 가능한 솔루션과 각각에 대한 많은 장단점이있는 복잡한 주제입니다. 여기 에 C #으로 복사본을 만드는 여러 가지 방법을 설명 하는 멋진 기사가 있습니다 . 요약:

  1. 수동 복제
    지루하지만 높은 수준의 제어.

  2. MemberwiseClone
    만 사용하여 복제는 단순 복사본을 만듭니다. 즉, 참조 유형 필드의 경우 원본 개체와 해당 복제본이 동일한 개체를 참조합니다.


  3. 기본적 으로 리플렉션 얕은 복사본으로 복제 , 전체 복사를 수행하기 위해 다시 작성할 수 있습니다. 장점 : 자동화. 단점 : 반사가 느립니다.

  4. 직렬화로 복제
    쉽고 자동화됩니다. 일부 제어를 포기하고 직렬화가 가장 느립니다.

  5. IL로 복제, 확장 방법으로 복제
    일반적이지 않은 고급 솔루션.


혼란 스럽습니다. 얕은 카피에 대한 다른 성능을 제거MemberwiseClone() 해야합니다 . CLI에서 RCW 이외의 모든 유형은 다음 순서로 얕은 복사가 가능해야합니다.

  • 유형에 대한 nursery에 메모리를 할당하십시오.
  • memcpy원본에서 새로운 데이터로. 대상이 nursery에 있으므로 쓰기 장벽이 필요하지 않습니다.
  • 객체에 사용자 정의 종료자가있는 경우 종료 대기중인 항목의 GC 목록에 추가합니다.
    • 소스 개체가 SuppressFinalize이를 호출하고 이러한 플래그가 개체 헤더에 저장되어 있으면 복제본에서 설정을 해제합니다.

CLR 내부 팀의 누군가가 이것이 사실이 아닌 이유를 설명 할 수 있습니까?


몇 가지 따옴표로 시작하고 싶습니다.

실제로 MemberwiseClone은 일반적으로 특히 복잡한 유형의 경우 다른 것보다 훨씬 낫습니다.

혼란 스럽습니다. MemberwiseClone ()은 얕은 복사본에 대한 다른 성능을 없애야합니다. [...]

이론적으로 얕은 복사를 구현하는 가장 좋은 방법은 C ++ 복사 생성자입니다. 컴파일 시간 크기를 알고 모든 필드의 멤버 별 복제를 수행합니다. 차선책 memcpy은 기본적으로 MemberwiseClone작동 하는 방법 입니다. 이것은 이론적으로 성능 측면에서 다른 모든 가능성을 제거해야 함을 의미합니다. 권리?

...하지만 분명히 빠른 속도는 아니며 다른 모든 솔루션을 제거하지도 않습니다. 하단에는 실제로 2 배 이상 빠른 솔루션을 게시했습니다. 그래서 : 틀 렸습니다.

MemberwiseClone의 내부 테스트

성능에 대한 기본 가정을 확인하기 위해 간단한 blittable 유형을 사용하는 간단한 테스트부터 시작하겠습니다.

[StructLayout(LayoutKind.Sequential)]
public class ShallowCloneTest
{
    public int Foo;
    public long Bar;

    public ShallowCloneTest Clone()
    {
        return (ShallowCloneTest)base.MemberwiseClone();
    }
}

테스트는 MemberwiseCloneagaist raw 의 성능을 확인할 수 있도록 고안되었습니다 memcpy. 이것은 blittable 유형이기 때문에 가능합니다.

직접 테스트하려면 안전하지 않은 코드로 컴파일하고 JIT 억제를 비활성화하고 릴리스 모드를 컴파일 한 다음 테스트하십시오. 또한 관련된 모든 줄 뒤에 타이밍을 넣었습니다.

구현 1 :

ShallowCloneTest t1 = new ShallowCloneTest() { Bar = 1, Foo = 2 };
Stopwatch sw = Stopwatch.StartNew();
int total = 0;
for (int i = 0; i < 10000000; ++i)
{
    var cloned = t1.Clone();                                    // 0.40s
    total += cloned.Foo;
}

Console.WriteLine("Took {0:0.00}s", sw.Elapsed.TotalSeconds);

기본적으로이 테스트를 여러 번 실행하고 어셈블리 출력을 확인하여 최적화되지 않았는지 등을 확인했습니다. 최종 결과는이 코드 한 줄에 소요되는 시간 (0.40 초)을 대략적으로 알 수 있다는 것입니다. 내 PC. 이것은 MemberwiseClone.

구현 2 :

sw = Stopwatch.StartNew();

total = 0;
uint bytes = (uint)Marshal.SizeOf(t1.GetType());
GCHandle handle1 = GCHandle.Alloc(t1, GCHandleType.Pinned);
IntPtr ptr1 = handle1.AddrOfPinnedObject();

for (int i = 0; i < 10000000; ++i)
{
    ShallowCloneTest t2 = new ShallowCloneTest();               // 0.03s
    GCHandle handle2 = GCHandle.Alloc(t2, GCHandleType.Pinned); // 0.75s (+ 'Free' call)
    IntPtr ptr2 = handle2.AddrOfPinnedObject();                 // 0.06s
    memcpy(ptr2, ptr1, new UIntPtr(bytes));                     // 0.17s
    handle2.Free();

    total += t2.Foo;
}

handle1.Free();
Console.WriteLine("Took {0:0.00}s", sw.Elapsed.TotalSeconds);

이 수치를 자세히 살펴보면 몇 가지 사항을 알 수 있습니다.

  • 개체를 만들고 복사하는 데 약 0.20 초가 걸립니다. 정상적인 상황에서는 이것이 가능한 가장 빠른 코드입니다.
  • 그러나 이렇게하려면 개체를 고정 및 고정 해제해야합니다. 0.81 초가 걸립니다.

그렇다면이 모든 것이 왜 그렇게 느린가요?

내 설명은 GC와 관련이 있다는 것입니다. 기본적으로 구현은 전체 GC 전후에 메모리가 동일하게 유지된다는 사실에 의존 할 수 없습니다 (메모리 주소는 GC 중에 변경 될 수 있으며, 이는 얕은 복사 중을 포함하여 언제든지 발생할 수 있음). 즉, 가능한 옵션은 두 가지뿐입니다.

  1. 데이터를 고정하고 복사합니다. 참고 GCHandle.Alloc만이 할 수있는 방법 중 하나입니다, 물론 C ++ / CLI 같은 것들을 당신에게 더 나은 성능을 제공 할 것으로 알려져있다.
  2. 필드 열거. 이렇게하면 GC 수집간에 멋진 작업을 수행 할 필요가 없으며 GC 수집 중에 GC 기능을 사용하여 이동 된 개체 스택의 주소를 수정할 수 있습니다.

MemberwiseClone 방법 1을 사용합니다. 즉, 고정 절차로 인해 성능 저하가 발생합니다.

(훨씬) 더 빠른 구현

모든 경우에 관리되지 않는 코드는 유형의 크기에 대한 가정을 할 수 없으며 데이터를 고정해야합니다. 크기에 대한 가정을하면 컴파일러가 루프 언 롤링, 레지스터 할당 등과 같은 더 나은 최적화를 수행 할 수 있습니다 (C ++ 복사 ctor가보다 빠릅니다 memcpy). 데이터를 고정 할 필요가 없다는 것은 추가적인 성능 저하가 없음을 의미합니다. .NET JIT가 어셈블러이기 때문에 이론적으로 이것은 간단한 IL 방출을 사용하여 더 빠른 구현을 수행하고 컴파일러가이를 최적화 할 수 있도록 허용해야 함을 의미합니다.

그렇다면 이것이 기본 구현보다 빠를 수있는 이유를 요약하려면?

  1. 개체를 고정 할 필요가 없습니다. 움직이는 물체는 GC에 의해 처리되며 실제로 이것은 끊임없이 최적화됩니다.
  2. 복사 할 구조의 크기에 대한 가정을 할 수 있으므로 더 나은 레지스터 할당, 루프 풀기 등을 허용합니다.

우리가 목표로하는 것은 memcpy0.17 초 이상의 원시 성능입니다 .

이를 위해 우리는 기본적으로 하나 이상의를 사용하고 call, 객체를 생성하고, 여러 copy명령을 수행 할 수 없습니다 . Cloner구현 과 비슷해 보이지만 몇 가지 중요한 차이점이 있습니다 (가장 중요 : Dictionary중복 CreateDelegate호출 없음 및 없음 ). 여기에 간다 :

public static class Cloner<T>
{
    private static Func<T, T> cloner = CreateCloner();

    private static Func<T, T> CreateCloner()
    {
        var cloneMethod = new DynamicMethod("CloneImplementation", typeof(T), new Type[] { typeof(T) }, true);
        var defaultCtor = typeof(T).GetConstructor(new Type[] { });

        var generator = cloneMethod .GetILGenerator();

        var loc1 = generator.DeclareLocal(typeof(T));

        generator.Emit(OpCodes.Newobj, defaultCtor);
        generator.Emit(OpCodes.Stloc, loc1);

        foreach (var field in typeof(T).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
        {
            generator.Emit(OpCodes.Ldloc, loc1);
            generator.Emit(OpCodes.Ldarg_0);
            generator.Emit(OpCodes.Ldfld, field);
            generator.Emit(OpCodes.Stfld, field);
        }

        generator.Emit(OpCodes.Ldloc, loc1);
        generator.Emit(OpCodes.Ret);

        return ((Func<T, T>)cloneMethod.CreateDelegate(typeof(Func<T, T>)));
    }

    public static T Clone(T myObject)
    {
        return cloner(myObject);
    }
}

이 코드는 0.16 초라는 결과로 테스트했습니다. 즉, MemberwiseClone.

더 중요한 것은이 속도가 memcpy'정상적인 상황에서 최적의 솔루션' 인와 동등 하다는 것입니다.

개인적으로 이것이 가장 빠른 솔루션이라고 생각합니다. 가장 좋은 부분은 .NET 런타임이 더 빨라지면 (SSE 명령에 대한 적절한 지원 등)이 솔루션도 마찬가지입니다.

편집 참고 : 위의 샘플 코드는 기본 생성자가 공용이라고 가정합니다. 그렇지 않은 경우에 대한 호출 GetConstructor은 null 반환합니다. 이 경우 다른 GetConstructor서명 중 하나를 사용 하여 보호 또는 개인 생성자를 가져 옵니다 . https://docs.microsoft.com/en-us/dotnet/api/system.type.getconstructor?view=netframework-4.8을 참조 하십시오.


복잡한 이유는 무엇입니까? MemberwiseClone이면 충분합니다.

public class ClassA : ICloneable
{
   public object Clone()
   {
      return this.MemberwiseClone();
   }
}

// let's say you want to copy the value (not reference) of the member of that class.
public class Main()
{
    ClassA myClassB = new ClassA();
    ClassA myClassC = new ClassA();
    myClassB = (ClassA) myClassC.Clone();
}

이것은 동적 IL 생성을 사용하는 방법입니다. 온라인 어딘가에서 찾았습니다.

public static class Cloner
{
    static Dictionary<Type, Delegate> _cachedIL = new Dictionary<Type, Delegate>();

    public static T Clone<T>(T myObject)
    {
        Delegate myExec = null;

        if (!_cachedIL.TryGetValue(typeof(T), out myExec))
        {
            var dymMethod = new DynamicMethod("DoClone", typeof(T), new Type[] { typeof(T) }, true);
            var cInfo = myObject.GetType().GetConstructor(new Type[] { });

            var generator = dymMethod.GetILGenerator();

            var lbf = generator.DeclareLocal(typeof(T));

            generator.Emit(OpCodes.Newobj, cInfo);
            generator.Emit(OpCodes.Stloc_0);

            foreach (var field in myObject.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
            {
                // Load the new object on the eval stack... (currently 1 item on eval stack)
                generator.Emit(OpCodes.Ldloc_0);
                // Load initial object (parameter)          (currently 2 items on eval stack)
                generator.Emit(OpCodes.Ldarg_0);
                // Replace value by field value             (still currently 2 items on eval stack)
                generator.Emit(OpCodes.Ldfld, field);
                // Store the value of the top on the eval stack into the object underneath that value on the value stack.
                //  (0 items on eval stack)
                generator.Emit(OpCodes.Stfld, field);
            }

            // Load new constructed obj on eval stack -> 1 item on stack
            generator.Emit(OpCodes.Ldloc_0);
            // Return constructed object.   --> 0 items on stack
            generator.Emit(OpCodes.Ret);

            myExec = dymMethod.CreateDelegate(typeof(Func<T, T>));

            _cachedIL.Add(typeof(T), myExec);
        }

        return ((Func<T, T>)myExec)(myObject);
    }
}

실제로 MemberwiseClone은 일반적으로 특히 복잡한 유형의 경우 다른 것보다 훨씬 낫습니다.

그 이유는 수동으로 복사본을 생성하는 경우 유형의 생성자 중 하나를 호출해야하지만 멤버 별 복제를 사용하면 메모리 블록을 복사하는 것 같습니다. 이러한 유형에는 매우 비용이 많이 드는 구성 작업이 있으므로 구성원 별 복제가 절대적으로 가장 좋은 방법입니다.

Onece는 {string A = Guid.NewGuid (). ToString ()}과 같은 유형을 썼습니다. 멤버 별 클론이 새 인스턴스를 만들고 멤버를 수동으로 할당하는 것보다 훨씬 빠릅니다.

아래 코드의 결과 :

수동 복사 : 00 : 00 : 00.0017099

MemberwiseClone : 00 : 00 : 00.0009911

namespace MoeCard.TestConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            Program p = new Program() { AAA = Guid.NewGuid().ToString(), BBB = 123 };
            Stopwatch sw = Stopwatch.StartNew();
            for (int i = 0; i < 10000; i++)
            {
                p.Copy1();
            }
            sw.Stop();
            Console.WriteLine("Manual Copy:" + sw.Elapsed);

            sw.Restart();
            for (int i = 0; i < 10000; i++)
            {
                p.Copy2();
            }
            sw.Stop();
            Console.WriteLine("MemberwiseClone:" + sw.Elapsed);
            Console.ReadLine();
        }

        public string AAA;

        public int BBB;

        public Class1 CCC = new Class1();

        public Program Copy1()
        {
            return new Program() { AAA = AAA, BBB = BBB, CCC = CCC };
        }
        public Program Copy2()
        {
            return this.MemberwiseClone() as Program;
        }

        public class Class1
        {
            public DateTime Date = DateTime.Now;
        }
    }

}

마지막으로 여기에 내 코드를 제공합니다.

    #region 数据克隆
    /// <summary>
    /// 依据不同类型所存储的克隆句柄集合
    /// </summary>
    private static readonly Dictionary<Type, Func<object, object>> CloneHandlers = new Dictionary<Type, Func<object, object>>();

    /// <summary>
    /// 根据指定的实例,克隆一份新的实例
    /// </summary>
    /// <param name="source">待克隆的实例</param>
    /// <returns>被克隆的新的实例</returns>
    public static object CloneInstance(object source)
    {
        if (source == null)
        {
            return null;
        }
        Func<object, object> handler = TryGetOrAdd(CloneHandlers, source.GetType(), CreateCloneHandler);
        return handler(source);
    }

    /// <summary>
    /// 根据指定的类型,创建对应的克隆句柄
    /// </summary>
    /// <param name="type">数据类型</param>
    /// <returns>数据克隆句柄</returns>
    private static Func<object, object> CreateCloneHandler(Type type)
    {
        return Delegate.CreateDelegate(typeof(Func<object, object>), new Func<object, object>(CloneAs<object>).Method.GetGenericMethodDefinition().MakeGenericMethod(type)) as Func<object, object>;
    }

    /// <summary>
    /// 克隆一个类
    /// </summary>
    /// <typeparam name="TValue"></typeparam>
    /// <param name="value"></param>
    /// <returns></returns>
    private static object CloneAs<TValue>(object value)
    {
        return Copier<TValue>.Clone((TValue)value);
    }
    /// <summary>
    /// 生成一份指定数据的克隆体
    /// </summary>
    /// <typeparam name="TValue">数据的类型</typeparam>
    /// <param name="value">需要克隆的值</param>
    /// <returns>克隆后的数据</returns>
    public static TValue Clone<TValue>(TValue value)
    {
        if (value == null)
        {
            return value;
        }
        return Copier<TValue>.Clone(value);
    }

    /// <summary>
    /// 辅助类,完成数据克隆
    /// </summary>
    /// <typeparam name="TValue">数据类型</typeparam>
    private static class Copier<TValue>
    {
        /// <summary>
        /// 用于克隆的句柄
        /// </summary>
        internal static readonly Func<TValue, TValue> Clone;

        /// <summary>
        /// 初始化
        /// </summary>
        static Copier()
        {
            MethodFactory<Func<TValue, TValue>> method = MethodFactory.Create<Func<TValue, TValue>>();
            Type type = typeof(TValue);
            if (type == typeof(object))
            {
                method.LoadArg(0).Return();
                return;
            }
            switch (Type.GetTypeCode(type))
            {
                case TypeCode.Object:
                    if (type.IsClass)
                    {
                        method.LoadArg(0).Call(Reflector.GetMethod(typeof(object), "MemberwiseClone")).Cast(typeof(object), typeof(TValue)).Return();
                    }
                    else
                    {
                        method.LoadArg(0).Return();
                    }
                    break;
                default:
                    method.LoadArg(0).Return();
                    break;
            }
            Clone = method.Delegation;
        }

    }
    #endregion

MemberwiseClone은 유지 관리가 덜 필요합니다. 기본 속성 값이 도움이되는지, 기본값이있는 항목을 무시할 수 있는지 모르겠습니다.


다음은 리플렉션을 사용하여 액세스 MemberwiseClone한 다음 필요 이상으로 리플렉션을 사용하지 않도록 대리자를 캐시 하는 작은 도우미 클래스입니다 .

public static class CloneUtil<T>
{
    private static readonly Func<T, object> clone;

    static CloneUtil()
    {
        var cloneMethod = typeof(T).GetMethod("MemberwiseClone", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
        clone = (Func<T, object>)cloneMethod.CreateDelegate(typeof(Func<T, object>));
    }

    public static T ShallowClone(T obj) => (T)clone(obj);
}

public static class CloneUtil
{
    public static T ShallowClone<T>(this T obj) => CloneUtil<T>.ShallowClone(obj);
}

You can call it like this:

Person b = a.ShallowClone();

ReferenceURL : https://stackoverflow.com/questions/966451/fastest-way-to-do-shallow-copy-in-c-sharp

반응형