성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] Working with Rust Libraries from C#...
[정성태] Detecting blocking calls using asyn...
[정성태] 아쉽게도, 커뮤니티는 아니고 개인 블로그입니다. ^^
[정성태] 질문이 잘 이해가 안 됩니다. 우선, 해당 소스코드에서 ILis...
[양승조
] var대신 dinamic으로 선언해서 해결은 했습니다. 맞는 해...
[양승조
] 또 막혔습니다. ㅠㅠ var list = props[i].Ge...
[양승조
] 아. 감사합니다. 어제는 안됐던것 같은데....정신을 차려야겠네...
[정성태] "props[i].GetValue(props[i])" 코드에서 ...
[정성태] 저렇게 조각 코드 말고, 실제로 재현이 되는 예제 프로젝트를 압...
[정성태] Modules 창(Ctrl+Shift+U)을 띄워서, 해당 Op...
글쓰기
제목
이름
암호
전자우편
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# - .NET 6부터 공개된 ISpanFormattable 사용법</h1> <p> ISpanFormattable 인터페이스는,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > ISpanFormattable Interface ; <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/api/system.ispanformattable'>https://learn.microsoft.com/en-us/dotnet/api/system.ispanformattable</a> </pre> <br /> <a target='tab' href='https://github.com/dotnet/runtime/issues/26373'>.NET Repo의 commit 시기</a>로 봤을 때 .NET Core 2.1부터 포함된 것으로 보입니다. 하지만 그동안 internal 접근 상태였다가 .NET 6부터 public으로 풀렸습니다. 이 인터페이스를 보면, 마이크로소프트가 .NET Core의 기본적인 힙 메모리 사용을 얼마나 줄이고 싶어 하는지 느낄 수 있습니다. (어찌 보면, 광적이라는 표현이 적합할지도 모릅니다. ^^;)<br /> <br /> <a name='sample_code'></a> 이것저것 설명 말고 코드 먼저 보는 게 좋겠죠? ^^ 다음은 ISpanFormattable을 상속한 구조체의 코드로 내부 코드는 .NET Core 6의 구현을 가져와서 대충(가령 음수나 format/provider 처리 없이) 씌워본 것입니다.<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; Person p = new Person { Age = 25 }; int written = 0; <span style='color: blue; font-weight: bold'>Span<char> buffer = stackalloc char[100];</span> p.TryFormat(<span style='color: blue; font-weight: bold'>buffer</span>, out written, null, null); // buffer에 Person 인스턴스의 ToString에 준하는 문자열이 포함됨 public struct Person : <span style='color: blue; font-weight: bold'>ISpanFormattable</span> { public int Age; public override string ToString() { return ToString(null, null); } public string ToString(string? format, IFormatProvider? formatProvider) { if (format == null) { return Age.ToString(); } return string.Format(formatProvider, format, Age); } public unsafe bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider) { return TryFormatInt32(this.Age, -1, format, provider, destination, out charsWritten); } public static bool TryFormatInt32(int value, int hexMask, ReadOnlySpan<char> format, IFormatProvider? provider, Span<char> destination, out int charsWritten) { return TryUInt32ToDecStr((uint)value, -1, destination, out charsWritten); } private unsafe static bool TryUInt32ToDecStr(uint value, int digits, Span<char> destination, out int charsWritten) { int num = Math.Max(digits, CountDigits(value)); if (num > destination.Length) { charsWritten = 0; return false; } charsWritten = num; fixed (char* reference = &MemoryMarshal.GetReference<char>(destination)) { char* ptr = reference; char* ptr2 = ptr + num; if (digits <= 1) { do { ValueTuple<uint, uint> valueTuple = Math.DivRem(value, 10U); value = valueTuple.Item1; uint item = valueTuple.Item2; *(--ptr2) = (char)(item + 48U); } while (value != 0U); } else { ptr2 = UInt32ToDecChars(ptr2, value, digits); } } return true; } internal unsafe static char* UInt32ToDecChars(char* bufferEnd, uint value, int digits) { while (--digits >= 0 || value != 0U) { ValueTuple<uint, uint> valueTuple = Math.DivRem(value, 10U); value = valueTuple.Item1; uint item = valueTuple.Item2; *(--bufferEnd) = (char)(item + 48U); } return bufferEnd; } public static int CountDigits(uint value) { int num = 1; if (value >= 100000U) { value /= 100000U; num += 5; } if (value >= 10U) { if (value < 100U) { num++; } else if (value < 1000U) { num += 2; } else if (value < 10000U) { num += 3; } else { num += 4; } } return num; } } </pre> <br /> 코드에서 알 수 있듯이, ISpanFormattable은 주로 값 형식의 ToString에 대한 힙 할당 문제를 해결하는 목적으로 나온 것입니다. 사실 그동안 제네릭의 지원을 통해 대부분의 경우 박싱 문제를 없애 힙 할당을 많이 줄였지만, 의외로 많이 사용하면서 피해 갈 수 없는 것이 바로 ToString이었습니다. 즉, 다음과 같은 간단한 코드에서조차,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > int value = 5; Console.WriteLine(value); </pre> <br /> 내부적으로는 value.ToString을 호출해 System.String을 힙에 할당해 처리하게 됩니다. 이런 문제를 마이크로소프트는 위의 코드에서처럼 Span을 이용함으로써 해결하고 있는 것입니다.<br /> <br /> 실제로 이것은 StringBuilder에 적용돼 기본 타입들에 대해 힙 할당 없이 값을 출력할 수 있는 기능을 제공합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public StringBuilder Append(int value) => AppendSpanFormattable(value); public StringBuilder Append(long value) => AppendSpanFormattable(value); public StringBuilder Append(float value) => AppendSpanFormattable(value); public StringBuilder Append(double value) => AppendSpanFormattable(value); public StringBuilder Append(decimal value) => AppendSpanFormattable(value); public StringBuilder Append(ushort value) => AppendSpanFormattable(value); public StringBuilder Append(uint value) => AppendSpanFormattable(value); private StringBuilder AppendSpanFormattable<T>(T value) <span style='color: blue; font-weight: bold'>where T : ISpanFormattable</span> { if (<span style='color: blue; font-weight: bold'>value.TryFormat</span>(RemainingCurrentChunk, out int charsWritten, format: default, provider: null)) { m_ChunkLength += charsWritten; return this; } return Append(value.ToString()); } private Span<char> RemainingCurrentChunk { get => new Span<char>(m_ChunkChars, m_ChunkLength, m_ChunkChars.Length - m_ChunkLength); } </pre> <br /> 하지만 아쉽게도 AppendSpanFormattable이 private이기 때문에 사용자 정의 값 형식에 대해서는 박싱이 발생합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Person p = new Person { Age = 32 }; // 구조체 Person StringBuilder sb = new StringBuilder(); sb.Append(p); // 힙 할당 발생 </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;' > Person p = new Person { Age = 32 }; // 구조체 Person StringBuilder sb = new StringBuilder(); <span style='color: blue; font-weight: bold'>sb.Append(p.Age); // 힙 할당 없음!</span> </pre> <br /> <hr style='width: 50%' /><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;' > Andrew Lock | .NET Escapades ; <a target='tab' href='https://andrewlock.net/a-deep-dive-on-stringbuilder-part-2-appending-strings-built-in-types-and-lists/'>https://andrewlock.net/a-deep-dive-on-stringbuilder-part-2-appending-strings-built-in-types-and-lists/</a> </pre> <br /> 그나저나, 저렇게 처리함으로써 코드는 점점 더 (무거워지고) 복잡해지는데, 과연 힙의 할당량을 줄이는 것이 얼마나 의미가 있길래... 저렇게까지 하는 걸까요? ^^ 종종 있는 ASP.NET Core/5+의 성능 증가 발표를 보면 아마도 저런 과한 노력의 덕분이지 않을까... 하는 예상입니다.<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
6881
(왼쪽의 숫자를 입력해야 합니다.)