[C#] “as”및 nullable 유형의 성능 놀라움

난 그냥 nullable 형식을 다루는 심도 C #의 4 장을 수정하고 “as”연산자를 사용하여 쓸 수있는 섹션을 추가하고 있습니다 :

object o = ...;
int? x = o as int?;
if (x.HasValue)
{
    ... // Use x.Value in here
}

나는 이것이 정말로 깔끔하다고 생각했으며 “is”와 캐스트를 사용하여 C # 1에 비해 성능을 향상시킬 수 있다고 생각했다. .

그러나 이것은 사실이 아닙니다. 아래에 샘플 테스트 응용 프로그램이 포함되어 있습니다. 기본적으로 객체 배열 내의 모든 정수를 합산합니다.하지만 배열에는 많은 null 참조와 문자열 참조 및 박스형 정수가 포함됩니다. 벤치 마크는 C # 1에서 사용해야하는 코드, “as”연산자를 사용하는 코드 및 LINQ 솔루션을 시작하는 데 사용됩니다. 놀랍게도 C # 1 코드는이 경우 20 배 더 빠릅니다. 심지어 LINQ 코드 (반복자를 고려할 때 느려질 것으로 예상 됨)도 “as”코드를 능가합니다.

isinstnullable 형식 에 대한 .NET 구현이 정말 느립니까? unbox.any문제를 일으키는 추가 항목 입니까? 이것에 대한 또 다른 설명이 있습니까? 현재 성능에 민감한 상황에서 이것을 사용하지 않는 것에 대한 경고를 포함해야한다고 생각합니다 …

결과 :

시전 : 10000000 : 121
As : 10000000 : 2211
LINQ : 10000000 : 2143

암호:

using System;
using System.Diagnostics;
using System.Linq;

class Test
{
    const int Size = 30000000;

    static void Main()
    {
        object[] values = new object[Size];
        for (int i = 0; i < Size - 2; i += 3)
        {
            values[i] = null;
            values[i+1] = "";
            values[i+2] = 1;
        }

        FindSumWithCast(values);
        FindSumWithAs(values);
        FindSumWithLinq(values);
    }

    static void FindSumWithCast(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            if (o is int)
            {
                int x = (int) o;
                sum += x;
            }
        }
        sw.Stop();
        Console.WriteLine("Cast: {0} : {1}", sum,
                          (long) sw.ElapsedMilliseconds);
    }

    static void FindSumWithAs(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;
            if (x.HasValue)
            {
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("As: {0} : {1}", sum,
                          (long) sw.ElapsedMilliseconds);
    }

    static void FindSumWithLinq(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = values.OfType<int>().Sum();
        sw.Stop();
        Console.WriteLine("LINQ: {0} : {1}", sum,
                          (long) sw.ElapsedMilliseconds);
    }
}



답변

분명히 JIT 컴파일러가 첫 번째 경우에 생성 할 수있는 머신 코드가 훨씬 더 효율적입니다. 실제로 도움이되는 한 가지 규칙은 객체를 상자 값과 동일한 유형의 변수로만 상자를 풀 수 있다는 것입니다. 따라서 JIT 컴파일러는 매우 효율적인 코드를 생성 할 수 있으므로 값 변환을 고려할 필요가 없습니다.

것이다 객체가 null는 아니고, 예상되는 형태이며, 소요하지만 몇 기계어 명령 경우 운영자 테스트가 쉽고, 단지 확인된다. 캐스트도 쉽습니다. JIT 컴파일러는 객체에서 값 비트의 위치를 ​​알고이를 직접 사용합니다. 복사 또는 변환이 발생하지 않으며 모든 기계 코드는 인라인이며 약 12 ​​가지 지침이 필요합니다. 이것은 권투가 일반적 일 때 .NET 1.0에서 실제로 효율적이어야했습니다.

int로 캐스팅? 더 많은 작업이 필요합니다. 박스형 정수의 값 표현은의 메모리 레이아웃과 호환되지 않습니다 Nullable<int>. 박스형 열거 형 타입으로 인해 변환이 필요하고 코드가 까다로워집니다. JIT 컴파일러는 JIT_Unbox_Nullable이라는 CLR 헬퍼 함수에 대한 호출을 생성하여 작업을 완료합니다. 이것은 모든 값 유형에 대한 범용 함수이며 유형을 검사하는 많은 코드가 있습니다. 그리고 값이 복사됩니다. 이 코드가 mscorwks.dll에 잠겨 있기 때문에 비용을 추정하기 어렵지만 수백 개의 기계 코드 명령어가있을 수 있습니다.

Linq OfType () 확장 메소드는 is 연산자와 캐스트 도 사용합니다 . 그러나 이것은 일반 유형으로 캐스트됩니다. JIT 컴파일러는 임의의 값 유형으로 캐스트를 수행 할 수있는 JIT_Unbox () 도우미 함수에 대한 호출을 생성합니다. Nullable<int>적은 작업이 필요하다는 점을 감안할 때 캐스트 속도가 느린 이유를 잘 설명하지 못했습니다 . ngen.exe가 여기에 문제를 일으킬 수 있다고 생각합니다.


답변

나에게 isinstnullable 유형에서는 실제로 느린 것 같습니다 . 방법에서 FindSumWithCast나는 바꿨다

if (o is int)

if (o is int?)

또한 실행 속도가 크게 느려집니다. 내가 볼 수있는 IL의 유일한 차이점은

isinst     [mscorlib]System.Int32

로 바뀐다

isinst     valuetype [mscorlib]System.Nullable`1<int32>


답변

이것은 원래 Hans Passant의 훌륭한 답변에 대한 주석으로 시작되었지만 너무 길어서 여기에 약간의 비트를 추가하고 싶습니다.

먼저 C # as연산자는 isinstIL 명령어를 내 보냅니다 ( is오퍼레이터도 마찬가지입니다 ). ( castclass직접 캐스트를 수행하면 컴파일러가 런타임 검사를 생략 할 수 없다는 것을 알고 또 다른 흥미로운 명령이 발생합니다.)

여기에 무엇을 isinst(수행 ECMA 335 파티션 III, 4.6 )

형식 : isinst typeTok

typeTok는 메타 데이터 토큰 (a 인 typeref, typedef또는 typespec원하는 클래스를 나타내는).

경우 typeTok이 nullable이 아닌 값 형식하거나로 해석됩니다 일반적인 매개 변수 유형은 “박스” typeTok을 .

경우 typeTok이 null 허용 유형, Nullable<T>그것은 “박스”로 해석됩니다T

가장 중요한 것은:

실제 유형 (검증되지 추적 방식) 경우 obj가 있다 검증 양도 간 유형 typeTok 다음 isinst성공하고 OBJ (같은 결과를 검증로서의 형 트랙 상태) 그대로 리턴 typeTok를 . 강제 (§1.6) 및 변환 (§3.27)과 달리 isinst개체의 실제 유형을 변경하지 않고 개체 ID를 유지합니다 (파티션 I 참조).

따라서 성능 킬러는 isinst이 경우가 아니라 추가 unbox.any입니다. JIT 코드 만 보았을 때 Hans의 답변에서 명확하지 않았습니다. 일반적으로 C # 컴파일러는 unbox.any이후 를 방출합니다 isinst T?(하지만 참조 유형 isinst T인 경우 수행하는 경우 생략 T).

왜 그렇게합니까? isinst T?명백한 효과가 전혀 없습니다 T?. 대신,이 모든 지시 사항은에 "boxed T"개봉 할 수있는 것이 있다는 것 T?입니다. 실제를 얻으려면 T?, 우리는 여전히 언 박싱해야하는 우리 "boxed T"에게 T?, 컴파일러가 방출 이유입니다 unbox.any후를 isinst. 당신이 그것에 대해 생각하는 경우에 대한 “상자 형식”때문에,이 의미가 T?있습니다 단지 "boxed T"와 제조 castclassisinst언 박스가 일치하지 않아 수행합니다.

Hans의 발견 내용을 표준의 일부 정보로 백업 하면 다음과 같습니다.

(ECMA 335 파티션 III, 4.33) : unbox.any

상자 형식의 값 형식에 적용하면 unbox.any명령은 obj에 포함 된 값을 추출합니다 (type O). (로는 동등 unbox하였다 ldobj.) 기준 입력에인가되면, unbox.any명령과 동일한 효과를 갖는다 castclasstypeTok한다.

(ECMA 335 파티션 III, 4.32) : unbox

일반적으로 unbox상자 개체 내부에 이미 존재하는 값 형식의 주소를 간단히 계산합니다. nullable 값 형식을 개봉 할 때는이 방법을 사용할 수 없습니다. 상자 작업 중에 Nullable<T>값이 boxed로 변환 되기 때문에 Ts구현시 종종 Nullable<T>힙 에서 새로운 것을 제조 하고 새로 할당 된 객체에 대한 주소를 계산해야합니다.


답변

흥미롭게도, 나는 ( 이 초기 테스트 와 비슷한) dynamic속도가 느려서 운영자 지원에 대한 피드백을 전달했습니다 . 비슷한 이유가 있습니다.Nullable<T>

사랑 해요 Nullable<T>. 또 다른 재미있는 점은 JIT null가 Null을 허용하지 않는 구조체를 발견 하고 제거하더라도 다음과 같이 실패한다는 것입니다 Nullable<T>.

using System;
using System.Diagnostics;
static class Program {
    static void Main() {
        // JIT
        TestUnrestricted<int>(1,5);
        TestUnrestricted<string>("abc",5);
        TestUnrestricted<int?>(1,5);
        TestNullable<int>(1, 5);

        const int LOOP = 100000000;
        Console.WriteLine(TestUnrestricted<int>(1, LOOP));
        Console.WriteLine(TestUnrestricted<string>("abc", LOOP));
        Console.WriteLine(TestUnrestricted<int?>(1, LOOP));
        Console.WriteLine(TestNullable<int>(1, LOOP));

    }
    static long TestUnrestricted<T>(T x, int loop) {
        Stopwatch watch = Stopwatch.StartNew();
        int count = 0;
        for (int i = 0; i < loop; i++) {
            if (x != null) count++;
        }
        watch.Stop();
        return watch.ElapsedMilliseconds;
    }
    static long TestNullable<T>(T? x, int loop) where T : struct {
        Stopwatch watch = Stopwatch.StartNew();
        int count = 0;
        for (int i = 0; i < loop; i++) {
            if (x != null) count++;
        }
        watch.Stop();
        return watch.ElapsedMilliseconds;
    }
}


답변

위의 FindSumWithAsAndHas의 결과입니다. 대체 텍스트

이것은 FindSumWithCast의 결과입니다. 대체 텍스트

결과:

  • 를 사용 as하여 객체가 Int32의 인스턴스인지 먼저 테스트합니다. 후드 아래에서 사용하고 있습니다 isinst Int32(손으로 쓴 코드와 비슷합니다 : if (o is int)). 그리고를 사용 as하면 무조건 객체의 압축을 풉니 다. 그리고 속성을 호출하는 것은 실제 성능을 저하시키는 요소입니다 (여전히 함수입니다), IL_0027

  • 캐스트를 사용하여 object가 int if (o is int); 후드 아래에서 이것을 사용하고 isinst Int32있습니다. int의 인스턴스 인 경우 값 IL_002D를 안전하게 개봉 할 수 있습니다

간단히 말해, 이것은 as접근법 을 사용하는 의사 코드입니다 .

int? x;

(x.HasValue, x.Value) = (o isinst Int32, o unbox Int32)

if (x.HasValue)
    sum += x.Value;    

그리고 이것은 캐스트 접근법을 사용하는 의사 코드입니다.

if (o isinst Int32)
    sum += (o unbox Int32)

따라서 캐스트 ( (int)a[i], 구문은 캐스트처럼 보이지만 실제로는 unboxing, cast 및 unboxing이 동일한 구문을 공유합니다. 다음에는 올바른 용어로 pedantic 할 것입니다) 객체가 결정적으로 int. as접근 방식 을 사용한다고 말할 수는 없습니다 .


답변

이 답변을 최신 상태로 유지하려면이 페이지에 대한 대부분의 토론이 이제 C # 7.1.NET 4.7 과 관련 이 있다고 언급 할 가치가 있습니다. 최고의 IL 코드를 생성하는 슬림 구문을 지원하는 을 사용하는 것이 좋습니다.

OP의 원래 예는 …

object o = ...;
int? x = o as int?;
if (x.HasValue)
{
    // ...use x.Value in here
}

간단하게 …

if (o is int x)
{
    // ...use x in here
}

나는 당신이 .NET을 작성할 때 새로운 구문에 대한 하나 개의 공통 사용은 것으로 나타났습니다 값 유형을 (예 struct에서 C # )이 구현 IEquatable<MyStruct>(대부분의 예상대로). 강력한 형식의 Equals(MyStruct other)메서드를 구현 한 후에 는 다음과 같이 형식화되지 않은 Equals(Object obj)재정의 (에서 상 속됨 Object)를 정상적으로 리디렉션 할 수 있습니다 .

public override bool Equals(Object obj) => obj is MyStruct o && Equals(o);

 


부록은 :Release 빌드 IL의 (각각)이 응답하여 위에서 도시 된 두 예 제 기능을위한 코드는 다음과 같다. 새로운 구문에 대한 IL 코드는 실제로 1 바이트 작지만, 거의 호출하지 않고 (vs. 2) unbox가능한 경우 연산을 완전히 피함으로써 크게 이깁니다 .

// static void test1(Object o, ref int y)
// {
//     int? x = o as int?;
//     if (x.HasValue)
//         y = x.Value;
// }

[0] valuetype [mscorlib]Nullable`1<int32> x
        ldarg.0
        isinst [mscorlib]Nullable`1<int32>
        unbox.any [mscorlib]Nullable`1<int32>
        stloc.0
        ldloca.s x
        call instance bool [mscorlib]Nullable`1<int32>::get_HasValue()
        brfalse.s L_001e
        ldarg.1
        ldloca.s x
        call instance !0 [mscorlib]Nullable`1<int32>::get_Value()
        stind.i4
L_001e: ret

// static void test2(Object o, ref int y)
// {
//     if (o is int x)
//         y = x;
// }

[0] int32 x,
[1] object obj2
        ldarg.0
        stloc.1
        ldloc.1
        isinst int32
        ldnull
        cgt.un
        dup
        brtrue.s L_0011
        ldc.i4.0
        br.s L_0017
L_0011: ldloc.1
        unbox.any int32
L_0017: stloc.0
        brfalse.s L_001d
        ldarg.1
        ldloc.0
        stind.i4
L_001d: ret

이전에 사용 가능한 옵션을 능가하는 새로운 C # 7 구문 의 성능에 대한 내 의견을 입증하는 추가 테스트 는 여기 (특히 ‘D’)를 참조 하십시오 .


답변

추가 프로파일 링 :

using System;
using System.Diagnostics;

class Program
{
    const int Size = 30000000;

    static void Main(string[] args)
    {
        object[] values = new object[Size];
        for (int i = 0; i < Size - 2; i += 3)
        {
            values[i] = null;
            values[i + 1] = "";
            values[i + 2] = 1;
        }

        FindSumWithIsThenCast(values);

        FindSumWithAsThenHasThenValue(values);
        FindSumWithAsThenHasThenCast(values);

        FindSumWithManualAs(values);
        FindSumWithAsThenManualHasThenValue(values);



        Console.ReadLine();
    }

    static void FindSumWithIsThenCast(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            if (o is int)
            {
                int x = (int)o;
                sum += x;
            }
        }
        sw.Stop();
        Console.WriteLine("Is then Cast: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsThenHasThenValue(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;

            if (x.HasValue)
            {
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("As then Has then Value: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsThenHasThenCast(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;

            if (x.HasValue)
            {
                sum += (int)o;
            }
        }
        sw.Stop();
        Console.WriteLine("As then Has then Cast: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithManualAs(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            bool hasValue = o is int;
            int x = hasValue ? (int)o : 0;

            if (hasValue)
            {
                sum += x;
            }
        }
        sw.Stop();
        Console.WriteLine("Manual As: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsThenManualHasThenValue(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;

            if (o is int)
            {
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("As then Manual Has then Value: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

}

산출:

Is then Cast: 10000000 : 303
As then Has then Value: 10000000 : 3524
As then Has then Cast: 10000000 : 3272
Manual As: 10000000 : 395
As then Manual Has then Value: 10000000 : 3282

이 수치에서 무엇을 추론 할 수 있습니까?

  • 첫째, IS-다음 캐스트 접근 속도가 매우 빠르고보다 같은 접근 방식. 303 대 3524
  • 둘째, .Value는 캐스팅보다 약간 느립니다. 3524 vs 3272
  • 셋째, .HasValue (즉 사용하여 수동을 사용하는 것보다 느린 변두리입니다 입니다 ). 3524 vs 3282
  • 넷째, 사과 – 투 – 사과 비교 (즉, 두 시뮬레이션 HasValue의 지정 및 시뮬레이션 값이 함께 발생 변환) 사이에 일을 같이 시뮬레이션같은 실제 접근 방식을 우리는 볼 수 있습니다 로 시뮬레이션 속도가 매우 빠르고보다 여전히 같은 실제 . 395 대 3524
  • 마지막으로, 첫 번째와 네 번째 결론에 따르면
    구현에 문제가 있습니다 ^ _ ^