난 그냥 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”코드를 능가합니다.
isinst
nullable 형식 에 대한 .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가 여기에 문제를 일으킬 수 있다고 생각합니다.
답변
나에게 isinst
nullable 유형에서는 실제로 느린 것 같습니다 . 방법에서 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
연산자는 isinst
IL 명령어를 내 보냅니다 ( 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"
와 제조 castclass
및 isinst
언 박스가 일치하지 않아 수행합니다.
Hans의 발견 내용을 표준의 일부 정보로 백업 하면 다음과 같습니다.
(ECMA 335 파티션 III, 4.33) : unbox.any
상자 형식의 값 형식에 적용하면
unbox.any
명령은 obj에 포함 된 값을 추출합니다 (typeO
). (로는 동등unbox
하였다ldobj
.) 기준 입력에인가되면,unbox.any
명령과 동일한 효과를 갖는다castclass
typeTok한다.
(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의 결과입니다.
결과:
-
를 사용
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
- 마지막으로, 첫 번째와 네 번째 결론에 따르면
구현에 문제가 있습니다 ^ _ ^
