성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] Detecting blocking calls using asyn...
[정성태] 아쉽게도, 커뮤니티는 아니고 개인 블로그입니다. ^^
[정성태] 질문이 잘 이해가 안 됩니다. 우선, 해당 소스코드에서 ILis...
[양승조
] var대신 dinamic으로 선언해서 해결은 했습니다. 맞는 해...
[양승조
] 또 막혔습니다. ㅠㅠ var list = props[i].Ge...
[양승조
] 아. 감사합니다. 어제는 안됐던것 같은데....정신을 차려야겠네...
[정성태] "props[i].GetValue(props[i])" 코드에서 ...
[정성태] 저렇게 조각 코드 말고, 실제로 재현이 되는 예제 프로젝트를 압...
[정성태] Modules 창(Ctrl+Shift+U)을 띄워서, 해당 Op...
[정성태] 만드실 수 있습니다. 단지, Unity 엔진 내의 스크립트와 W...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>C# - .NET5부터 도입된 CollectionsMarshal</h1> <p> 좋은 글이 있어서 소개합니다. ^^<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [C#]CollectionsMarshal の解説 ; <a target='tab' href='https://zenn.dev/naminodarie/articles/a950920fe7d1a5'>https://zenn.dev/naminodarie/articles/a950920fe7d1a5</a> [번역글] CollectionsMarshal 해설 ; <a target='tab' href='https://docs.google.com/document/u/1/d/e/2PACX-1vSFmjP4qYtzJi01fpXjuaQpKTo8MWx6_ghCDrLH8TLIibqJ8Xf3p73MgB92GJmWlbVIMHnfwtquGI3_/pub'>https://docs.google.com/document/u/1/d/e/2PACX-1vSFmjP4qYtzJi01fpXjuaQpKTo8MWx6_ghCDrLH8TLIibqJ8Xf3p73MgB92GJmWlbVIMHnfwtquGI3_/pub</a> </pre> <br /> .NET 5부터 제공하는 <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.collectionsmarshal'>CollectionsMarshal 클래스</a>의 사용법과 주의 사항을 잘 설명하고 있습니다. 해당 클래스에는 AsSapn(.NET 5+ 지원)과 GetValueRefOrAddDefault(.NET 6+ 지원) 딱 2개의 메서드가 있는데요, 사실상 <a target='tab' href='https://www.c-sharpcorner.com/article/understanding-ref-and-out-with-c-sharp-7/'>C# 7.0부터 추가한 ref 구문</a>을 활용해 성능을 높이려는 마이크로소프트의 노력이 기존 컬렉션 구현에도 적용되기 시작한 경우라고 볼 수 있겠습니다. ^^<br /> <br /> <hr style='width: 50%' /><br /> <br /> 굳이 첨언하자면, 해당 예제들이 잘 제어된 코드 내에서 재현이 가능하다는 것을 알고 넘어가는 것도 좋겠습니다. <br /> <br /> 우선, List의 경우 다음과 같은 예제를 들고 있는데요,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System.Runtime.InteropServices; var list = Enumerable.Range(0, 10).ToList(); var span = CollectionsMarshal.AsSpan(list); span[^1] = -1; Console.WriteLine(string.Join(", ", list)); // 0, 1, 2, 3, 4, 5, 6, 7, 8, -1 list.Add(100); // 내부 배열이 재확보된다 span[0] = -2; // 이 span은 list의 내부 배열 참조는 아니다 Console.WriteLine(string.Join(", ", list)); // 0, 1, 2, 3, 4, 5, 6, 7, 8, -1, 100 </pre> <br /> 사실, 위에서 첫 번째 라인을 다음과 같이만 고쳐도,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // var list = Enumerable.Range(0, 10).ToList(); List<int> list = new List<int>() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; /* 또는 이렇게, List<int> list = new List<int>(); for (int i = 0; i < 10; i++) { list.Add(i); } */ </pre> <br /> 출력 결과는 본문과는 다르게 나옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0, 1, 2, 3, 4, 5, 6, 7, 8, -1 <span style='color: blue; font-weight: bold'>-2</span>, 1, 2, 3, 4, 5, 6, 7, 8, -1, 100 </pre> <br /> 왜냐하면, List<T> 타입의 버퍼 운영이 2배수로 늘어나는 방식이기 때문입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // List<T>.EnsureCapacity 메서드 코드 (.NET 버전 따라 바뀔 수 있음) private void EnsureCapacity(int min) { if (_items.Length < min) { <span style='color: blue; font-weight: bold'>int newCapacity = _items.Length == 0 ? DefaultCapacity : _items.Length * 2;</span> if ((uint)newCapacity > Array.MaxArrayLength) newCapacity = Array.MaxArrayLength; if (newCapacity < min) newCapacity = min; Capacity = newCapacity; } } </pre> <br /> 위의 코드에서 재미있는 것은 DefaultCapacity == 4라는 점입니다. 그래서 요소가 하나라도 있다면 List의 경우 기본으로 4개의 요소를 담을 수 있는 공간을 확보합니다. 이후로는 2배수로 진행하기 때문에 Add에 의한 용량 증가가 4, 8, 16, 32...로 늘어나 갈수록 확률이 낮아집니다.<br /> <br /> 또한, 원본 글의 예제 코드가 정확하게 문제를 재현할 수 있었던 것은, Enumerable.Range에 이은 ToList의 구현이 정확하게 Capacity를 지정해서 생성하기 때문입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // System.Linq.Enumerable.RangeIterator 소스 코드 public List<int> ToList() { List<int> list = new List<int>(<span style='color: blue; font-weight: bold'>_end - _start</span>); for (int cur = _start; cur != _end; cur++) { list.Add(cur); } return list; } </pre> <br /> 그래서 "list.Add(100)" 단 하나의 추가 코드로 인해 내부 배열을 (기존 10개에서) 20개로 새로 할당하는 과정을 거치게 된 것입니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 위에서, 제가 List<T>의 경우 기본 크기가 4, 이후 2배수로 늘어난다고 했는데요, Dictionary<TKey, TValue>의 경우에는 또 다릅니다.<br /> <br /> Dictionary는 내부 배열의 증가를 소수(prime)에 해당하는 크기만큼 바꾸도록 정하고 있는데요,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // System.Collections.Generic.Dictionary 소스 코드 private int Initialize(int capacity) { <span style='color: blue; font-weight: bold'>int size = HashHelpers.GetPrime(capacity);</span> int[] buckets = new int[size]; Entry[] entries = new Entry[size]; // Assign member variables after both arrays allocated to guard against corruption from OOM if second fails _freeList = -1; #if TARGET_64BIT _fastModMultiplier = HashHelpers.GetFastModMultiplier((uint)size); #endif _buckets = buckets; _entries = entries; return size; } </pre> <br /> 그래서 요소가 한 개 추가된 경우 기본 크기가 (소수인) 3개가 되었고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // System.Collections.HashHelpers에 미리 정의된 소수 public static readonly int[] primes = new int[72] { 3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919, 1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591, 17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437, 187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263, 1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369 }; </pre> <br /> 해당 예제에서는 2번의 추가 Add를 했을 때 그 수에 이르렀기 때문에,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System.Runtime.InteropServices; <span style='color: blue; font-weight: bold'>var dic = new Dictionary<string, int> { { "foo", 1 }, };</span> bool exists; ref var foo = ref CollectionsMarshal.GetValueRefOrAddDefault(dic, "foo", out exists); System.Diagnostics.Debug.Assert(exists); ref var bar = ref CollectionsMarshal.GetValueRefOrAddDefault(dic, "bar", out exists); System.Diagnostics.Debug.Assert(!exists); foo++; bar++; Console.WriteLine(string.Join(", ", dic)); // [foo, 2], [bar, 1] <span style='color: blue; font-weight: bold'>dic.Add("baz", -1);</span> foo++; Console.WriteLine(dic["foo"]); // 3 <span style='color: blue; font-weight: bold'>dic.Add("foobar", -1); // 여기에서 재확보 된다</span> foo++; // foo는 dic의 내부 참조가 아니다 Console.WriteLine(dic["foo"]); // 3 </pre> <br /> 내부에서 Resize 과정을 거쳐,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // System.Collections.Generic.Dictionary 소스 코드 private bool TryInsert(TKey key, TValue value, InsertionBehavior behavior) { // ...[생략]... int count = _count; <span style='color: blue; font-weight: bold'>if (count == entries.Length)</span> { <span style='color: blue; font-weight: bold'>Resize();</span> bucket = ref GetBucket(hashCode); } // ...[생략]... } private void Resize() => Resize(<span style='color: blue; font-weight: bold'>HashHelpers.ExpandPrime(_count)</span>, false); </pre> <br /> 재할당이 발생한 것입니다. 즉, 이것 역시 내부 요소 수가 커지면서 재할당이 발생할 확률이 낮아지므로 Span을 통한 예제 출력 결과가 원문에서 쓴 것처럼 나오지 않을 수도 있습니다.<br /> <br /> 물론, 어쨌든 원문 저자가 말한 것처럼, "값을 추가하기 전후로 사용하거나 하면 위험"하다는 것에는 변함이 없습니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> Enumeration 작업을 할 때 컬렉션이 변경되면 "Collection was modified; enumeration operation may not execute." 이런 예외가 발생하는데요, 혹시 위에서도 가능할까요?<br /> <br /> 이를 위해 확장 메서드가 좋을 듯한데, 아쉽게도 확장 메서드는 제네릭 지원을 하지 못해 다음과 같이 타입을 지정해야만 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > static class ListHelper { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void AddEx(this List<int> list, int item) { #if DEBUG int old = list.Capacity; #endif list.Add(item); #if DEBUG if (list.Capacity != old) { throw new InvalidOperationException("Collection was modified; Add operation may not execute."); } #endif } } </pre> <br /> 위의 코드로 다시 실행하면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > List<int> list = Enumerable.Range(0, 10).ToList(); list.AddEx(500); // 예외 발생 InvalidOperationException </pre> <br /> 목적은 달성했지만, 음... 여러모로 깔끔하지 않군요. ^^<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
9011
(왼쪽의 숫자를 입력해야 합니다.)