C #에서 얕은 복사를 수행하는 가장 빠른 방법
C #에서 얕은 복사를 수행하는 가장 빠른 방법은 무엇일까요? 얕은 복사를 수행하는 방법은 두 가지뿐입니다.
- MemberwiseClone
- 각 필드를 하나씩 복사 (수동)
(2)가 (1)보다 빠르다는 것을 알았습니다. 얕은 복사를 수행하는 다른 방법이 있는지 궁금합니다.
이것은 많은 가능한 솔루션과 각각에 대한 많은 장단점이있는 복잡한 주제입니다. 여기 에 C #으로 복사본을 만드는 여러 가지 방법을 설명 하는 멋진 기사가 있습니다 . 요약:
수동 복제
지루하지만 높은 수준의 제어.MemberwiseClone
만 사용하여 복제는 단순 복사본을 만듭니다. 즉, 참조 유형 필드의 경우 원본 개체와 해당 복제본이 동일한 개체를 참조합니다.
기본적 으로 리플렉션 얕은 복사본으로 복제 , 전체 복사를 수행하기 위해 다시 작성할 수 있습니다. 장점 : 자동화. 단점 : 반사가 느립니다.직렬화로 복제
쉽고 자동화됩니다. 일부 제어를 포기하고 직렬화가 가장 느립니다.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();
}
}
테스트는 MemberwiseClone
agaist 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 중에 변경 될 수 있으며, 이는 얕은 복사 중을 포함하여 언제든지 발생할 수 있음). 즉, 가능한 옵션은 두 가지뿐입니다.
- 데이터를 고정하고 복사합니다. 참고
GCHandle.Alloc
만이 할 수있는 방법 중 하나입니다, 물론 C ++ / CLI 같은 것들을 당신에게 더 나은 성능을 제공 할 것으로 알려져있다. - 필드 열거. 이렇게하면 GC 수집간에 멋진 작업을 수행 할 필요가 없으며 GC 수집 중에 GC 기능을 사용하여 이동 된 개체 스택의 주소를 수정할 수 있습니다.
MemberwiseClone
방법 1을 사용합니다. 즉, 고정 절차로 인해 성능 저하가 발생합니다.
(훨씬) 더 빠른 구현
모든 경우에 관리되지 않는 코드는 유형의 크기에 대한 가정을 할 수 없으며 데이터를 고정해야합니다. 크기에 대한 가정을하면 컴파일러가 루프 언 롤링, 레지스터 할당 등과 같은 더 나은 최적화를 수행 할 수 있습니다 (C ++ 복사 ctor가보다 빠릅니다 memcpy
). 데이터를 고정 할 필요가 없다는 것은 추가적인 성능 저하가 없음을 의미합니다. .NET JIT가 어셈블러이기 때문에 이론적으로 이것은 간단한 IL 방출을 사용하여 더 빠른 구현을 수행하고 컴파일러가이를 최적화 할 수 있도록 허용해야 함을 의미합니다.
그렇다면 이것이 기본 구현보다 빠를 수있는 이유를 요약하려면?
- 개체를 고정 할 필요가 없습니다. 움직이는 물체는 GC에 의해 처리되며 실제로 이것은 끊임없이 최적화됩니다.
- 복사 할 구조의 크기에 대한 가정을 할 수 있으므로 더 나은 레지스터 할당, 루프 풀기 등을 허용합니다.
우리가 목표로하는 것은 memcpy
0.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
'Development Tip' 카테고리의 다른 글
React-디버깅을 위해 DOM 요소에서 컴포넌트 가져 오기 (0) | 2021.01.09 |
---|---|
Android Studio-레이아웃의 텍스트 및 디자인 미리보기보기 (0) | 2021.01.09 |
PHP CodeSniffer는 얼마나 유용합니까? (0) | 2021.01.09 |
Google Geocoding API-REQUEST_DENIED (0) | 2021.01.09 |
MySQL : 10 분 이상 지난 모든 행 삭제 (0) | 2021.01.09 |