Microsoft MVP성태의 닷넷 이야기
.NET Framework: 1113. C# 10 - (12) 문자열 보간 성능 개선 [링크 복사], [링크+제목 복사],
조회: 19731
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 2개 있습니다.)
(시리즈 글이 15개 있습니다.)
.NET Framework: 1094. C# 10 - (1) 구조체를 생성하는 record struct
; https://www.sysnet.pe.kr/2/0/12790

.NET Framework: 1096. C# 10 - (2) 전역 네임스페이스 선언
; https://www.sysnet.pe.kr/2/0/12792

.NET Framework: 1097. C# 10 - (3) 개선된 변수 초기화 판정
; https://www.sysnet.pe.kr/2/0/12793

.NET Framework: 1099. C# 10 - (4) 상수 문자열에 포맷 식 사용 가능
; https://www.sysnet.pe.kr/2/0/12796

.NET Framework: 1100. C# 10 - (5) 속성 패턴의 개선
; https://www.sysnet.pe.kr/2/0/12799

.NET Framework: 1101. C# 10 - (6) record class 타입의 ToString 메서드를 sealed 처리 허용
; https://www.sysnet.pe.kr/2/0/12801

.NET Framework: 1103. C# 10 - (7) Source Generator V2 APIs
; https://www.sysnet.pe.kr/2/0/12804

.NET Framework: 1104. C# 10 - (8) 분해 구문에서 기존 변수의 재사용 가능
; https://www.sysnet.pe.kr/2/0/12805

.NET Framework: 1105. C# 10 - (9) 비동기 메서드가 사용할 AsyncMethodBuilder 선택 가능
; https://www.sysnet.pe.kr/2/0/12807

.NET Framework: 1108. C# 10 - (10) 개선된 #line 지시자
; https://www.sysnet.pe.kr/2/0/12812

.NET Framework: 1109. C# 10 - (11) Lambda 개선
; https://www.sysnet.pe.kr/2/0/12813

.NET Framework: 1113. C# 10 - (12) 문자열 보간 성능 개선
; https://www.sysnet.pe.kr/2/0/12826

.NET Framework: 1114. C# 10 - (13) 단일 파일 내에 적용되는 namespace 선언
; https://www.sysnet.pe.kr/2/0/12828

.NET Framework: 1115. C# 10 - (14) 구조체 타입에 기본 생성자 정의 가능
; https://www.sysnet.pe.kr/2/0/12829

.NET Framework: 1116. C# 10 - (15) CallerArgumentExpression 특성 추가
; https://www.sysnet.pe.kr/2/0/12835




C# 10 - (12) 문자열 보간 성능 개선

휴~~~ 이번에도 역시, 이를 설명하기 위해 지난 2개의 글을 먼저 설명할 필요가 있었습니다. ^^;

C# - FormattableString 타입
; https://www.sysnet.pe.kr/2/0/12819

C# - .NET 6부터 공개된 ISpanFormattable 사용법
; https://www.sysnet.pe.kr/2/0/12821

그리고, 이번 문법에 대해서는 spec 문서보다는 다음의 블로그 글을 읽는 것이 더 이해가 잘 될 것입니다.

String Interpolation in C# 10 and .NET 6
; https://devblogs.microsoft.com/dotnet/string-interpolation-in-c-10-and-net-6/

사실 이번 글은 위의 내용을 정리한 것에 불과합니다.




자, 그럼 왜 문자열 보간에 대한 개선을 해야만 했는지, 기존 문자열 보간이 가진 문제점 - 즉, string.Format에 대한 문제점들을 다음과 같이 나열할 수 있습니다.

1) string.Format은 꽤나 무거운 작업을 동반합니다. 즉, 서식이나 정렬에 관계되고 심지어 자체적인 문자열 파싱도 해야 합니다. 그런데, 여기서 재미있는 것은 C# 컴파일러의 경우 string interpolation 표현이 있을 때 이미 한번 파싱을 해 string.Format으로 변경하는데요, 이것을 다시 런타임에 string.Format 메서드 내에서 다시 파싱을 하고 있었다는 점입니다.

2) string.Format은 object 인자를 받는데, 이로 인해 값 타입의 인스턴스인 경우 반드시 박싱 연산을 필요로 하게 됩니다. 이와 함께, 그동안 값 타입의 성능을 높이기 위한 모든 다양한 방법들, 가령 Span<char> 등의 인스턴스는 string.Format의 Object 타입 제한으로 인해 사용할 수 없는 문제점이 있습니다.

3) string.Format은 3개까지 인자를 받는 오버로드를 제공하지만, 그 이후부터는 params Object[]로 처리하기 때문에 인자 수가 4개 이상인 경우부터 무조건 배열을 위한 힙 메모리 할당이 발생합니다.

4) 인자로 넘어온 값은 문자열 변환을 위해 반드시 ToString 호출을 필요로 하며, 당연히 이때 임시 문자열이 생성됩니다.

이런 유의 문제점들을 C# 10에서 해결했다는데, 사실 어떻게 저런 것들을 개선할 수 있었을지 잘 상상이 안 됩니다. 한번 볼까요? ^^




C# 컴파일러의 경우, foreach에 배열을 전달해 열거를 하게 되면,

int[] array = ...;
foreach (int i in array)
{
    Use(i);
}

원래의 IEnumerator/IEnumerable을 활용한 코드로 번역하지 않고,

int[] array = ...;
using (IEnumerator<int> e = array.GetEnumerator())
{
    while (e.MoveNext())
    {
        Use(e.Current);
    }
}

좀 더 빠른 성능을 내도록 일부러 indexer를 활용하는 식으로 바꿔서 번역한다고 합니다.

int[] array = ...;
for (int i = 0; i < array.Length; i++)
{
    Use(array[i]);
}

그리고, 이런 식의 전처리를 컴파일러 쪽의 용어로 "Lowering"이라고 일컫는다는데,,, ^^ Lowering을 한글로 뭐라고 표현하면 좋을까요? ^^ 의미상으로 보면 "구문 변환"이면서 다소 최적화를 하므로 "최적 구문 변환" 정도로 하면 될까요? ^^; (혹시 이에 해당하는 번역을 아시는 분은 덧글 부탁드립니다.)

그러니까, C# 10 컴파일러는 문자열 보간에 대해서도 단순히 이전처럼 string.Concat나 string.Format으로 번역하지 않고, 좀 더 성능이 좋은 코드로 바꾸게 되었는데요, 바로 그 역할을 위해 .NET 6에 추가된 타입이 System.Runtime.CompilerServices.DefaultInterpolatedStringHandler ref struct입니다.

생각했던 것보다 의외로 해결책은 간단합니다. DefaultInterpolatedStringHandler는 문자열 처리를 StringBuilder처럼 하는데, Append 시킬 문자열에 대해 제네릭 인자로 처리하기 때문에 박싱 문제에서 자유롭게 된 것입니다.

namespace System.Runtime.CompilerServices
{
    [InterpolatedStringHandler]
    public ref struct DefaultInterpolatedStringHandler
    {
        public DefaultInterpolatedStringHandler(int literalLength, int formattedCount);
        public DefaultInterpolatedStringHandler(int literalLength, int formattedCount, System.IFormatProvider? provider);
        public DefaultInterpolatedStringHandler(int literalLength, int formattedCount, System.IFormatProvider? provider, System.Span<char> initialBuffer);

        public void AppendLiteral(string value);

        public void AppendFormatted<T>(T value);
        public void AppendFormatted<T>(T value, string? format);
        public void AppendFormatted<T>(T value, int alignment);
        public void AppendFormatted<T>(T value, int alignment, string? format);

        public void AppendFormatted(ReadOnlySpan<char> value);
        public void AppendFormatted(ReadOnlySpan<char> value, int alignment = 0, string? format = null);

        public void AppendFormatted(string? value);
        public void AppendFormatted(string? value, int alignment = 0, string? format = null);
        public void AppendFormatted(object? value, int alignment = 0, string? format = null);

        public string ToStringAndClear();
    }
}

따라서, C# 9 이전에는 다음과 같은 식의 문자열 보간을 하면,

public static string FormatVersion(int major, int minor, int build, int revision) =>
    $"{major}.{minor}.{build}.{revision}";

이렇게 번역을 했지만,

public static string FormatVersion(int major, int minor, int build, int revision)
{
    var array = new object[4];
    array[0] = major; // 박싱 발생
    array[1] = minor; // 박싱 발생
    array[2] = build; // 박싱 발생
    array[3] = revision; // 박싱 발생
    return string.Format("{0}.{1}.{2}.{3}", array); // 내부에서 각각 ToString을 호출해 문자열 힙 할당 발생
}

C# 10부터는 이렇게 바뀌게 됩니다.

public static string FormatVersion(int major, int minor, int build, int revision)
{
    var handler = new DefaultInterpolatedStringHandler(literalLength: 3, formattedCount: 4);
    handler.AppendFormatted(major); // 박싱 및 문자열 힙 할당 없음
    handler.AppendLiteral(".");
    handler.AppendFormatted(minor); // 박싱 및 문자열 힙 할당 없음
    handler.AppendLiteral(".");
    handler.AppendFormatted(build); // 박싱 및 문자열 힙 할당 없음
    handler.AppendLiteral(".");
    handler.AppendFormatted(revision); // 박싱 및 문자열 힙 할당 없음
    return handler.ToStringAndClear();
}

박싱은 물론이고 개별 힙 할당도 없어지고 마지막의 ToStringAndClear에서 최종 문자열을 만들어 단 한 번의 힙 할당만 하고 있습니다.

또한, StringBuilder와는 달리 DefaultInterpolatedStringHandler는 AppendFormatted를 public으로 공개했기 때문에, 사용자 정의 구조체 타입에도 ISpanFormattable만 구현되어 있다면 그대로 사용할 수 있습니다. 일례로 아래의 예제 코드는 실행해 보면 Console.WriteLine에서 Person 타입의 object.ToString 메서드가 실행되지 않고 TryFormat 메서드가 실행되는 것을 확인할 수 있습니다.

using System.Diagnostics;
using System.Runtime.InteropServices;

Console.WriteLine(Format.FormatVersion(1, 5, 1, 2<span style='color: blue; font-weight: bold'>,
                new Person { Age = 25 }</span>));

public class Format
{
    public static string FormatVersion(int major, int minor, int build, int revision, Person person) =>
        $"{major}.{minor}.{build}.{revision}.{person}";
}

// "C# - .NET 6부터 공개된 ISpanFormattable 사용법" 글의 예제 코드
public struct Person : ISpanFormattable
{
    public int Age;

    public override string ToString()
    {
        return ToString(null, null);
    }

    // ...[생략]...

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

    // ...[생략]...
}




자, 그런데 과거 FormattableString이 나온 이유를 상기시켜 보면, 이제 저렇게 바뀐 문자열 보간 처리 방식도 여전히 Localization에 대한 문제가 남아 있다는 것을 알 수 있습니다. 역시나 마이크로소프트는 이 문제도 함께 해결해야만 했을 것이고, 이를 위해 문자열 보간 처리를 위임하는 DefaultInterpolatedStringHandler에게 사용자가 넘겨주는 Localization 정보를 활용할 수 있는 메서드를 제공하는 방식으로 우회하고 있습니다.

즉, 위의 예제 코드에서 만약 사용자가 Localization을 제공해야 한다면 우선 다음과 같은 식의 메서드를 하나 정의합니다.

public static string Create(IFormatProvider provider, 
    [InterpolatedStringHandlerArgument("provider")] ref DefaultInterpolatedStringHandler handler) =>
        handler.ToStringAndClear();

해당 메서드에는 InterpolatedStringHandlerArgument 특성이 DefaultInterpolatedStringHandler 매개 변수에 적용되었고, 그 인자로 "provider"라는 문자열로 IFormatProvider를 전달할 매개 변수 이름을 지정했습니다. 그리고 이렇게 만든 메서드를 이전의 FormatVersion 메서드에 다음과 같이 적용시켜 주면,

public class Format
{
    public static string FormatVersion(int major, int minor, int build, int revision) =>
        Create(CultureInfo.InvariantCulture, $"{major}.{minor}.{build}.{revision}");

    public static string Create(IFormatProvider provider, 
        [InterpolatedStringHandlerArgument("provider")] ref DefaultInterpolatedStringHandler handler) =>
            handler.ToStringAndClear();
}

C# 10 컴파일러는 이를 인지하고 FormatVersion 메서드의 내부 코드를 다음과 같이 생성해 줍니다.

public static string FormatVersion(int major, int minor, int build, int revision)
{
    IFormatProvider invariantCulture = CultureInfo.InvariantCulture;
    IFormatProvider provider = invariantCulture;
    DefaultInterpolatedStringHandler defaultInterpolatedStringHandler;
    defaultInterpolatedStringHandler..ctor(3, 4, invariantCulture);
    defaultInterpolatedStringHandler.AppendFormatted<int>(major);
    defaultInterpolatedStringHandler.AppendLiteral(".");
    defaultInterpolatedStringHandler.AppendFormatted<int>(minor);
    defaultInterpolatedStringHandler.AppendLiteral(".");
    defaultInterpolatedStringHandler.AppendFormatted<int>(build);
    defaultInterpolatedStringHandler.AppendLiteral(".");
    defaultInterpolatedStringHandler.AppendFormatted<int>(revision);
    return Format.Create(provider, ref defaultInterpolatedStringHandler);
}

이뿐만이 아닙니다. 사실 위의 코드를 자세히 들여다보면 결국 AppendFormatted로 들어갈 문자열들이 어딘가에는 버퍼로 있어야 한다는 것을 알 수 있으며 결국 그 버퍼들이 생성되는 문제가 있다는 것도 짐작할 수 있습니다. 마이크로소프트는 일단 이 문제를 ArrayPool을,

C# - ArrayPool<T> 소개
; https://www.sysnet.pe.kr/2/0/12478

C# - ArrayPool<T>와 MemoryPool<T> 소개
; https://www.sysnet.pe.kr/2/0/12480

이용하는 방식으로 기본 구현을 제공합니다. 하지만, 이것조차도 사용하고 싶지 않을 때, 즉 사용자 측에서 TryFormat으로 인해 필요한 버퍼의 크기를 미리 알 수 있다면 직접 stackalloc 등의 버퍼로 대체할 수 있도록 또 다른 인자를 제공합니다. 아래는 그렇게 해서 바뀐 Create 메서드와 그것을 사용해 FormatVersion2 메서드에 반영한 예제를 보여줍니다.

public static string Create(IFormatProvider provider, Span<char> myBuffer,
    [InterpolatedStringHandlerArgument("provider", "myBuffer")] ref DefaultInterpolatedStringHandler handler) =>
        handler.ToStringAndClear();

public static string FormatVersion2(int major, int minor, int build, int revision) =>
    Create(CultureInfo.InvariantCulture, stackalloc char[64], $"{major}.{minor}.{build}.{revision}");

그럼, C# 10 컴파일러는 ArrayPool로부터 버퍼를 받아오지 않고 사용자가 넘겨준 버퍼를 활용하도록 다음과 같은 코드를 생성합니다.

public unsafe static string FormatVersion2(int major, int minor, int build, int revision)
{
    IFormatProvider invariantCulture = CultureInfo.InvariantCulture;
    IFormatProvider formatProvider = invariantCulture;
    Span<char> span = new Span<char>(stackalloc byte[(UIntPtr)128], 64);
    Span<char> span2 = span;
    IFormatProvider provider = formatProvider;
    Span<char> myBuffer = span2;
    DefaultInterpolatedStringHandler defaultInterpolatedStringHandler;
    defaultInterpolatedStringHandler..ctor(3, 4, invariantCulture, span2);
    defaultInterpolatedStringHandler.AppendFormatted<int>(major);
    defaultInterpolatedStringHandler.AppendLiteral(".");
    defaultInterpolatedStringHandler.AppendFormatted<int>(minor);
    defaultInterpolatedStringHandler.AppendLiteral(".");
    defaultInterpolatedStringHandler.AppendFormatted<int>(build);
    defaultInterpolatedStringHandler.AppendLiteral(".");
    defaultInterpolatedStringHandler.AppendFormatted<int>(revision);
    return Format.Create(provider, myBuffer, ref defaultInterpolatedStringHandler);
}

그리고, 사용자의 편의를 위해 Create 메서드는 고정적으로 사용할 수 있는 유형인 까닭에 미리 string 타입에 포함시켰으므로 최종적으로는 위의 코드를 다음과 같이 간단하게 변경할 수 있습니다.

public class Format
{
    public static string FormatVersion(int major, int minor, int build, int revision) =>
        string.Create(CultureInfo.InvariantCulture, $"{major}.{minor}.{build}.{revision}");

    public static string FormatVersion2(int major, int minor, int build, int revision) =>
        string.Create(CultureInfo.InvariantCulture, stackalloc char[64], $"{major}.{minor}.{build}.{revision}");
}

복잡하게 설명했지만, Localization이나 사용자 버퍼를 전달하고 싶다면 string.Create를 사용하면 되고, 그 외의 경우라면 그냥 일반적인 보간 문자열을 예전처럼 사용하면 됩니다. (어쩌면, 원래부터 DefaultInterpolatedStringHandler가 관여됐다면 애당초 FormattableString은 나오지 않았을 것입니다.)




재미있는 것은 C# 10 컴파일러에게 DefaultInterpolatedStringHandler와 같은 타입을 사용자 정의해 인식시킬 수 있다는 점입니다. 이를 위해 필요한 것은 일정한 포맷을 가진 AppendLiteral, AppendFormatted와 같은 메서드를 담고 있을 것과 InterpolatedStringHandlerAttribute 특성만 부여돼 있으면 됩니다.

일례로 다음과 같은 타입을 하나 임의로 만들 수 있고,

[InterpolatedStringHandler]
public ref struct MyInterpolatedStringHandler
{
    public MyInterpolatedStringHandler(int literalLength, int formattedCount)
    {
    }

    public MyInterpolatedStringHandler(int literalLength, int formattedCount, IFormatProvider provider)
    {
    }

    public MyInterpolatedStringHandler(int literalLength, int formattedCount, IFormatProvider provider, Span<char> initialBuffer)
    {
    }

    public string MakeText()
    {
        return null;
    }

    public void AppendLiteral(string value)
    {
    }

    public void AppendFormatted<T>(T value)
    {
    }
}

이것을 사용하는 FormatVersion 메서드를 자유롭게 정의할 수 있습니다.

public class Format
{
    public static string FormatVersion(int major, int minor, int build, int revision) =>
        string.Create(CultureInfo.InvariantCulture, $"{major}.{minor}.{build}.{revision}");

    public static string FormatVersion2(int major, int minor, int build, int revision) =>
        string.Create(CultureInfo.InvariantCulture, stackalloc char[64], $"{major}.{minor}.{build}.{revision}");

    public static string FormatVersion3(int major, int minor, int build, int revision) =>
        Format.UseMyHandler($"{major}.{minor}.{build}.{revision}");

    public static string UseMyHandler([InterpolatedStringHandlerArgument()] ref MyInterpolatedStringHandler handler) =>
        handler.MakeText();
}

그럼 C# 10 컴파일러는 여러분들이 정의한 타입을 이용해 문자열 보간 구문을 처리하는 코드를 생성합니다.

// Format
public static string FormatVersion3(int major, int minor, int build, int revision)
{
    MyInterpolatedStringHandler myInterpolatedStringHandler = new MyInterpolatedStringHandler(3, 4);
    myInterpolatedStringHandler.AppendFormatted<int>(major);
    myInterpolatedStringHandler.AppendLiteral(".");
    myInterpolatedStringHandler.AppendFormatted<int>(minor);
    myInterpolatedStringHandler.AppendLiteral(".");
    myInterpolatedStringHandler.AppendFormatted<int>(build);
    myInterpolatedStringHandler.AppendLiteral(".");
    myInterpolatedStringHandler.AppendFormatted<int>(revision);
    return Format.UseMyHandler(ref myInterpolatedStringHandler);
}




이와 관련한 변화들을 좀 볼까요? ^^

새로운 DefaultInterpolatedStringHandler도 사실 ref struct 타입은 처리할 수 없습니다. 왜냐하면, 위와 같은 처리 자체가 대상 값 타입이 ISpanFormattable을 구현하고 있는 경우에 한해 힙 할당을 없애는 방향으로 동작하기 때문에 인터페이스조차 상속할 수 없는 ref struct는 그에 대한 혜택을 받지 못합니다. 하지만, ref struct 유형 중 가장 많이 사용되는 ReadOnlySpan<char>에 대해서는 오버로드된 메서드를 가지고 있기 때문에,

public void AppendFormatted(ReadOnlySpan<char> value);
public void AppendFormatted(ReadOnlySpan<char> value, int alignment = 0, string? format = null);

문자열에 대한 처리에 한해서는 다음과 같이 직접 문자열 보간에 사용하는 것이 가능해졌습니다.

ReadOnlySpan<char> span = "Hello World!"[0..2].AsSpan();
string text = $"{span,4}"; // C# 9 이하에서는 컴파일 오류 - error CS0029: Cannot implicitly convert type 'System.ReadOnlySpan<char>' to 'object'

또한 StringBuilder의 경우 Append 메서드에 AppendInterpolatedStringHandler가 적용된 오버로드를 추가해,

; https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendinterpolatedstringhandler

문자열 보간으로 Append하는 경우,

StringBuilder sb = new StringBuilder();
int value = 5;
sb.Append($"{value}");

AppendInterpolatedStringHandler를 사용하는 코드로 C# 10 컴파일러는 번역을 합니다.

StringBuilder.AppendInterpolatedStringHandler appendInterpolatedStringHandler;
appendInterpolatedStringHandler..ctor(0, 1, stringBuilder);
appendInterpolatedStringHandler.AppendFormatted<int>(value);
stringBuilder2.Append(ref appendInterpolatedStringHandler);

위의 번역이 재미있는 것이, 만약 여러분들이 기존에 StrinBuilder와 함께 사용하던 문자열 보간 코드가 있다면 C# 10 컴파일러로 다시 빌드하는 것만으로도 성능 향상의 기회를 가질 수 있다는 점입니다. ^^

마지막으로 AssertInterpolatedStringHandler를 제공해,

Debug.AssertInterpolatedStringHandler Struct
; https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.debug.assertinterpolatedstringhandler

Debug.Assert에서의 문자열 보간 사용 시 힙 메모리 할당을 최적화할 수 있게 되었고, TryWriteInterpolatedStringHandler를 제공해,

MemoryExtensions.TryWriteInterpolatedStringHandler Struct
; https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.trywriteinterpolatedstringhandler

제공해야 할 문자열 크기에 비해 버퍼가 작다면 Append 관련 코드를 빠르게 벗어날 수 있는 InterpolatedStringHandler가 제공되고 있습니다. 아마도, 이런 식으로 사용자 정의된 handler는 점점 더 늘어날 것이고 이로 인해 BCL은 계속해서 힙 메모리 사용을 줄이는 쪽으로 가게 될 것입니다.

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




C# 10 - (1) 구조체를 생성하는 record struct (공식 문서, Static Abstract Members In Interfaces C# 10 Preview)
; https://www.sysnet.pe.kr/2/0/12790

C# 10 - (2) 전역 네임스페이스 선언 (공식 문서, Global Using Directive)
; https://www.sysnet.pe.kr/2/0/12792

C# 10 - (3) 개선된 변수 초기화 판정 (공식 문서, Improved Definite Assignment)
; https://www.sysnet.pe.kr/2/0/12793

C# 10 - (4) 상수 문자열에 포맷 식 사용 가능 (공식 문서, Constant Interpolated Strings)
; https://www.sysnet.pe.kr/2/0/12796

C# 10 - (5) 속성 패턴의 개선 (공식 문서, Extended property patterns)
; https://www.sysnet.pe.kr/2/0/12799

C# 10 - (6) record class 타입의 ToString 메서드를 sealed 처리 허용 (공식 문서, Sealed record ToString)
; https://www.sysnet.pe.kr/2/0/12801

C# 10 - (7) Source Generator V2 APIs (Source Generator V2 APIs)
; https://www.sysnet.pe.kr/2/0/12804

C# 10 - (8) 분해 구문에서 기존 변수의 재사용 가능 (공식 문서, Mix declarations and variables in deconstruction)
; https://www.sysnet.pe.kr/2/0/12805

C# 10 - (9) 비동기 메서드가 사용할 AsyncMethodBuilder 선택 가능 (공식 문서, Async method builder override); 
; https://www.sysnet.pe.kr/2/0/12807

C# 10 - (10) 개선된 #line 지시자 (공식 문서, Enhanced #line directive)
; https://www.sysnet.pe.kr/2/0/12812

C# 10 - (11) Lambda 개선 (공식 문서 1, 공식 문서 2, Lambda improvements) 
; https://www.sysnet.pe.kr/2/0/12813

C# 10 - (12) 문자열 보간 성능 개선 (공식 문서, Interpolated string improvements)
; https://www.sysnet.pe.kr/2/0/12826

C# 10 - (13) 단일 파일 내에 적용되는 namespace 선언 (공식 문서, File-scoped namespace)
; https://www.sysnet.pe.kr/2/0/12828

C# 10 - (14) 구조체 타입에 기본 생성자 정의 가능 (공식 문서, Parameterless struct constructors)
; https://www.sysnet.pe.kr/2/0/12829

C# 10 - (15) CallerArgumentExpression 특성 추가 (공식 문서, Caller expression attribute)
; https://www.sysnet.pe.kr/2/0/12835

Language Feature Status
; https://github.com/dotnet/roslyn/blob/main/docs/Language%20Feature%20Status.md




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 7/23/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2023-07-23 12시19분
String.Create is 🔥 for string concatenation
; https://twitter.com/Dave_DotNet/status/1682699744747958274/photo/1

Optimizing memory usage with modern .NET features
; https://mijailovic.net/2025/04/10/memory-optimizations/
정성태

... 121  122  123  124  125  126  127  128  129  130  131  [132]  133  134  135  ...
NoWriterDateCnt.TitleFile(s)
1756정성태9/23/201427493기타: 48. NVidia 제품의 과다한 디스크 사용 [2]
1755정성태9/22/201434281오류 유형: 241. Unity Web Player를 설치해도 여전히 설치하라는 화면이 나오는 경우 [4]
1754정성태9/22/201424682VC++: 80. 내 컴퓨터에서 C++ AMP 코드가 실행이 될까요? [1]
1753정성태9/22/201420621오류 유형: 240. Lync로 세미나 참여 시 소리만 들리지 않는 경우 [1]
1752정성태9/21/201441071Windows: 100. 윈도우 8 - RDP 연결을 이용해 VNC처럼 사용자 로그온 화면을 공유하는 방법 [5]
1751정성태9/20/201438962.NET Framework: 464. 프로세스 간 통신 시 소켓 필요 없이 간단하게 Pipe를 열어 통신하는 방법 [1]파일 다운로드1
1750정성태9/20/201423836.NET Framework: 463. PInvoke 호출을 이용한 비동기 파일 작업파일 다운로드1
1749정성태9/20/201423736.NET Framework: 462. 커널 객체를 위한 null DACL 생성 방법파일 다운로드1
1748정성태9/19/201425388개발 환경 구성: 238. [Synergy] 여러 컴퓨터에서 키보드, 마우스 공유
1747정성태9/19/201428512오류 유형: 239. psexec 실행 오류 - The system cannot find the file specified.
1746정성태9/18/201426108.NET Framework: 461. .NET EXE 파일을 닷넷 프레임워크 버전에 상관없이 실행할 수 있을까요? - 두 번째 이야기 [6]파일 다운로드1
1745정성태9/17/201423044개발 환경 구성: 237. 리눅스 Integration Services 버전 업그레이드 하는 방법 [1]
1744정성태9/17/201431070.NET Framework: 460. GetTickCount / GetTickCount64와 0x7FFE0000 주솟값 [4]파일 다운로드1
1743정성태9/16/201420985오류 유형: 238. 설치 오류 - Failed to get size of pseudo bundle
1742정성태8/27/201426983개발 환경 구성: 236. Hyper-V에 설치한 리눅스 VM의 VHD 크기 늘리는 방법 [2]
1741정성태8/26/201421338.NET Framework: 459. GetModuleHandleEx로 알아보는 .NET 메서드의 DLL 모듈 관계파일 다운로드1
1740정성태8/25/201432526.NET Framework: 458. 닷넷 GC가 순환 참조를 해제할 수 있을까요? [2]파일 다운로드1
1739정성태8/24/201426573.NET Framework: 457. 교착상태(Dead-lock) 해결 방법 - Lock Leveling [2]파일 다운로드1
1738정성태8/23/201422066.NET Framework: 456. C# - CAS를 이용한 Lock 래퍼 클래스파일 다운로드1
1737정성태8/20/201419770VS.NET IDE: 93. Visual Studio 2013 동기화 문제
1736정성태8/19/201425588VC++: 79. [부연] CAS Lock 알고리즘은 과연 빠른가? [2]파일 다운로드1
1735정성태8/19/201418262.NET Framework: 455. 닷넷 사용자 정의 예외 클래스의 최소 구현 코드 - 두 번째 이야기
1734정성태8/13/201419927오류 유형: 237. Windows Media Player cannot access the file. The file might be in use, you might not have access to the computer where the file is stored, or your proxy settings might not be correct.
1733정성태8/13/201426367.NET Framework: 454. EmptyWorkingSet Win32 API를 사용하는 C# 예제파일 다운로드1
1732정성태8/13/201434486Windows: 99. INetCache 폴더가 다르게 보이는 이유
1731정성태8/11/201427088개발 환경 구성: 235. 점(.)으로 시작하는 파일명을 탐색기에서 만드는 방법
... 121  122  123  124  125  126  127  128  129  130  131  [132]  133  134  135  ...