.Net 4.0의 새로운 튜플 유형이 값 유형 (구조체)이 아닌 참조 유형 (클래스) 인 이유


89

누구든지 대답을 알고 있거나 이에 대한 의견이 있습니까?

튜플은 일반적으로 그다지 크지 않기 때문에 클래스보다 구조체를 사용하는 것이 더 합리적이라고 가정합니다. 뭐라고?


1
2016 년 이후에 여기에 걸림돌이되는 사람들을 위해. C # 7 이상에서 Tuple 리터럴은 family 유형입니다 ValueTuple<...>. 에서 참조 참조 C # 튜플 유형
타 미르 다니엘 리

답변:


94

Microsoft는 단순성을 위해 모든 튜플 형식 참조 형식을 만들었습니다.

개인적으로 이것이 실수라고 생각합니다. 필드가 4 개 이상인 튜플은 매우 드문 경우이며 어쨌든 더 형식적인 대안 (예 : F #의 레코드 유형)으로 대체해야하므로 작은 튜플 만 실제적으로 유용합니다. 내 벤치 마크에 따르면 최대 512 바이트의 unboxed 튜플이 boxed tuple보다 더 빠를 수 있습니다.

메모리 효율성이 한 가지 관심사이지만 가장 큰 문제는 .NET 가비지 수집기의 오버 헤드라고 생각합니다. .NET에서 할당 및 수집은 가비지 수집기가 매우 최적화되지 않았기 때문에 (예 : JVM에 비해) 매우 비쌉니다 . 또한 기본 .NET GC (워크 스테이션)는 아직 병렬화되지 않았습니다. 결과적으로 튜플을 사용하는 병렬 프로그램은 모든 코어가 공유 가비지 수집기를 위해 경쟁하면서 중단되어 확장 성을 파괴합니다. 이것은 주된 관심사 일뿐만 아니라 AFAIK가이 문제를 조사 할 때 Microsoft에 의해 완전히 무시되었습니다.

또 다른 문제는 가상 디스패치입니다. 참조 유형은 하위 유형을 지원하므로 해당 멤버는 일반적으로 가상 디스패치를 ​​통해 호출됩니다. 반대로 값 유형은 하위 유형을 지원할 수 없으므로 멤버 호출이 완전히 모호하지 않고 항상 직접 함수 호출로 수행 될 수 있습니다. CPU가 프로그램 카운터가 끝나는 위치를 예측할 수 없기 때문에 가상 디스패치는 최신 하드웨어에서 엄청나게 비쌉니다. JVM은 가상 디스패치를 ​​최적화하기 위해 많은 노력을 기울이지 만 .NET은 그렇지 않습니다. 그러나 .NET은 값 형식의 형태로 가상 디스패치로부터의 이스케이프를 제공합니다. 따라서 튜플을 값 유형으로 표현하면 여기서도 성능이 크게 향상 될 수 있습니다. 예를 들어,GetHashCode 2- 튜플에서 백만 번은 0.17 초가 걸리지 만 동등한 구조체에서 호출하는 데는 0.008 초 밖에 걸리지 않습니다. 즉, 값 유형이 참조 유형보다 20 배 빠릅니다.

튜플에서 이러한 성능 문제가 일반적으로 발생하는 실제 상황은 튜플을 사전의 키로 사용하는 것입니다. 실제로 Stack Overflow 질문 F # 의 링크를 따라이 스레드를 우연히 발견했습니다. 내 알고리즘은 Python보다 느리게 실행됩니다! 저자의 F # 프로그램은 박스형 튜플을 사용했기 때문에 Python보다 느리다는 것이 밝혀졌습니다. 손으로 쓴 struct형식을 사용하여 수동으로 unboxing 하면 F # 프로그램이 Python보다 몇 배 더 빠르고 빠릅니다. 튜플이 시작될 참조 유형이 아니라 값 유형으로 표현된다면 이러한 문제는 발생하지 않았을 것입니다.


2
@Bent : 예, F #의 핫 경로에서 튜플을 발견했을 때 정확히 제가하는 일입니다. .NET Framework에서 boxed 및 unboxed 튜플을 모두 제공했다면 좋을 것입니다 ...
JD

18
가상 디스패치와 관련하여 귀하의 책임이 잘못되었다고 생각합니다. Tuple<_,...,_>유형이 봉인되었을 수 있으므로 참조 유형 임에도 불구하고 가상 디스패치가 필요하지 않습니다. 왜 그들이 참조 유형인지보다 봉인되지 않은 이유에 대해 더 궁금합니다.
kvb

2
튜플은 하나의 함수에서 생성하고 다른 함수에 반환 한 후 다시 사용하지 않을 것이다하는 시나리오에 대한 내 테스트, 노출 필드에서 구조는 우수한 성능을 제공하는 것 어떤 타격 할 수 있도록 큰 아니라 크기 데이터 항목을 스택. 변경 불가능한 클래스는 참조가 구성 비용을 정당화 할만큼 충분히 전달되는 경우에만 더 좋습니다 (데이터 항목이 클수록 선호하는 트레이드 오프를 위해 전달해야하는 횟수가 줄어 듭니다). 튜플은 단순히 함께 붙어있는 여러 변수를 나타내야하기 때문에 구조체는 이상적 일 것입니다.
supercat 2013

2
"최대 512 바이트의 unboxed 튜플은 여전히 ​​boxed보다 빠를 수 있습니다." -어떤 시나리오입니까? 당신은 수있는 빠른 데이터의 512B를 들고 클래스 인스턴스보다 512B 구조체를 할당 할 수 있지만, 그것을 주변에 통과하는 100 배 이상 느린 (86을 추정)이 될 것입니다. 내가 간과하고있는 것이 있습니까?
Groo


45

그 이유는 작은 튜플 만이 작은 메모리 풋 프린트를 가지기 때문에 값 유형으로 의미가 있기 때문입니다. 더 큰 튜플 (즉, 속성이 더 많은 튜플)은 16 바이트보다 크므로 실제로 성능이 저하됩니다.

일부 튜플은 값 유형이고 다른 튜플은 참조 유형이되어 개발자가 Microsoft 직원이 모든 참조 유형을 만드는 것이 더 간단하다고 생각하는 것을 알도록 강요하는 대신.

아, 의심이 확인되었습니다! 튜플 작성을 참조하십시오 .

첫 번째 주요 결정은 튜플을 참조 또는 값 유형으로 처리할지 여부였습니다. 튜플의 값을 변경하고 싶을 때마다 변경이 불가능하므로 새 값을 만들어야합니다. 참조 유형 인 경우 타이트 루프에서 튜플의 요소를 변경하면 많은 가비지가 생성 될 수 있습니다. F # 튜플은 참조 형식 이었지만 2 개 또는 3 개 요소 튜플이 대신 값 형식이면 성능 향상을 실현할 수 있다는 느낌이 들었습니다. 내부 튜플을 만든 일부 팀은 시나리오가 관리되는 개체를 많이 만드는 데 매우 민감했기 때문에 참조 형식 대신 값을 사용했습니다. 그들은 값 유형을 사용하면 더 나은 성능을 제공한다는 것을 발견했습니다. 튜플 사양의 첫 번째 초안에서 우리는 2, 3, 4 요소 튜플을 값 유형으로 유지하고 나머지는 참조 유형입니다. 그러나 다른 언어의 대표자를 포함하는 디자인 회의에서 두 유형 간의 의미가 약간 다르기 때문에이 "분할"디자인이 혼란 스러울 것이라고 결정했습니다. 행동과 디자인의 일관성이 잠재적 인 성능 향상보다 우선 순위가 높은 것으로 결정되었습니다. 이 입력을 기반으로 모든 튜플이 참조 유형이되도록 설계를 변경했지만 일부 크기의 튜플에 대해 값 유형을 사용할 때 속도가 향상되었는지 확인하기 위해 F # 팀에 성능 조사를 요청했습니다. 컴파일러가 F #으로 작성 되었기 때문에이를 테스트하는 좋은 방법이있었습니다. 다양한 시나리오에서 튜플을 사용한 대규모 프로그램의 좋은 예입니다. 결국 F # 팀은 일부 튜플이 참조 형식이 아닌 값 형식 인 경우 성능이 향상되지 않는다는 것을 발견했습니다. 이로 인해 튜플에 참조 유형을 사용하기로 한 결정에 대해 기분이 좋아졌습니다.



아, 알 겠어요. 나는 여전히 값 유형이 실제로 여기에서 아무 의미가 없다는 것에 약간 혼란스러워합니다. P
Bent Rasmussen

나는 일반적인 인터페이스가 없다는 주석을 읽었으며 코드를 일찍 보았을 때 정확히 또 다른 인상을 받았습니다. 튜플 유형이 얼마나 비 제네릭인지는 정말 감동적이지 않습니다. 하지만 항상 직접 만들 수 있다고 생각합니다 ... 어쨌든 C #에서는 구문 지원이 없습니다. 그러나 적어도 ... 그래도 제네릭의 사용과 제약은 .Net에서 여전히 제한적이라고 느낍니다. 매우 포괄적 인 매우 추상적 인 라이브러리에 대한 상당한 잠재력이 있지만 제네릭에는 공변 반환 유형과 같은 추가 항목이 필요할 수 있습니다.
Bent Rasmussen

7
"16 바이트"제한은 가짜입니다. .NET 4에서 이것을 테스트했을 때 GC가 너무 느려 최대 512 바이트의 unboxed 튜플이 더 빠를 수 있음을 발견했습니다. 또한 Microsoft의 벤치 마크 결과에 의문을 제기합니다. 나는 그들이 병렬 처리를 무시했다고 확신합니다 (F # 컴파일러는 병렬이 아닙니다). NET의 워크 스테이션 GC도 병렬이 아니기 때문에 GC를 피하는 것이 실제로 효과가 있습니다.
JD

호기심 때문에 컴파일러 팀이 튜플을 EXPOSED-FIELD 구조체 로 만드는 아이디어를 테스트했는지 궁금합니다 . 다양한 특성을 가진 유형의 인스턴스가 있고 다른 특성 하나를 제외하고 동일한 인스턴스가 필요한 경우 노출 필드 구조체는 다른 유형보다 훨씬 빠르게 수행 할 수 있으며 구조체가 얻을 때만 이점이 커집니다. 더 큽니다.
supercat

7

.NET System.Tuple <...> 유형이 구조체로 정의 된 경우 확장 가능하지 않습니다. 예를 들어, long 정수의 삼항 튜플은 현재 다음과 같이 확장됩니다.

type Tuple3 = System.Tuple<int64, int64, int64>
type Tuple33 = System.Tuple<Tuple3, Tuple3, Tuple3>
sizeof<Tuple3> // Gets 4
sizeof<Tuple33> // Gets 4

삼항 튜플이 구조체로 정의 된 경우 결과는 다음과 같습니다 (내가 구현 한 테스트 예제를 기반으로 함).

sizeof<Tuple3> // Would get 32
sizeof<Tuple33> // Would get 104

튜플은 F #에서 기본 제공 구문을 지원하고이 언어에서 매우 자주 사용되기 때문에 "struct"튜플은 F # 프로그래머가 인식하지 못한 채 비효율적 인 프로그램을 작성할 위험이 있습니다. 아주 쉽게 일어날 것입니다.

let t3 = 1L, 2L, 3L
let t33 = t3, t3, t3

제 생각에 "struct"튜플은 일상적인 프로그래밍에서 상당한 비 효율성을 만들 가능성이 높습니다. 반면, 현재 존재하는 "클래스"튜플은 @Jon이 언급 한 것처럼 특정 비 효율성을 유발합니다. 그러나 나는 "발생 확률"과 "잠재적 손상"의 곱이 현재 클래스보다 구조체에서 훨씬 더 높을 것이라고 생각합니다. 따라서 현재 구현은 덜 악합니다.

이상적으로는 "class"튜플과 "struct"튜플이 모두 존재하며 둘 다 F #에서 구문을 지원합니다!

편집 (2017-10-07)

이제 구조체 튜플은 다음과 같이 완전히 지원됩니다.


2
불필요한 복사를 피한다면 , 각 인스턴스가 복사 비용이 힙 객체 생성 비용을 초과 할만큼 충분히 복사되지 않는 한 모든 크기 의 노출 된 필드 구조체가 동일한 크기의 변경 불가능한 클래스보다 더 효율적일 것입니다. 손익분기 사본 수는 개체 크기에 따라 다릅니다). 하나는 불변 척 구조체를 원하지만, (구조체가 무엇 인 변수의 컬렉션으로 표시하도록 설계 구조체 경우 이러한 복사는 피할 수 있다는 그들이 큰 경우에도)을 효율적으로 사용할 수 있습니다.
supercat

2
F #은으로 구조체를 전달한다는 생각에 잘 ref맞지 않거나, 특히 박스형 일 때 소위 "불변 구조체"가 아니라는 사실을 좋아하지 않을 수 있습니다. 너무 나쁜 .net은 강제로 매개 변수를 전달하는 개념을 구현하지 않았습니다 const ref. 많은 경우 이러한 의미가 실제로 필요한 것이기 때문입니다.
supercat

1
덧붙여서, 저는 GC의 상각 된 비용이 객체 할당 비용의 일부라고 생각합니다. 매 메가 바이트 할당 이후에 L0 GC가 필요한 경우 64 바이트 할당 비용은 L0 GC 비용의 약 1 / 16,000에 필요한 L1 또는 L2 GC 비용의 일부입니다. 그것의 결과.
supercat

4
"발생 확률 곱하기 잠재적 손상의 곱은 현재 클래스보다 구조체에서 훨씬 더 높을 것이라고 생각합니다." FWIW, 나는 야생에서 튜플의 튜플을 거의 본 적이 없으며 디자인 결함으로 간주하지만 사람들이 튜플을 키로 사용할 때 끔찍한 성능으로 어려움을 겪는 경우가 많습니다 Dictionary. 예 : stackoverflow.com/questions/5850243 /…
JD

3
@Jon이 답변을 작성한 지 2 년이 지났으며 이제 최소 2 및 3 튜플이 구조체이면 더 좋을 것이라는 데 동의합니다. 이와 관련하여 F # 언어 사용자 음성 제안 이 작성되었습니다. 최근 몇 년 동안 빅 데이터, 양적 금융 및 게임 분야의 애플리케이션이 엄청나게 증가했기 때문에이 문제는 시급합니다.
Marc Sigrist 2014 년

4

2- 튜플의 경우 이전 버전의 공통 유형 시스템에서 항상 KeyValuePair <TKey, TValue>를 사용할 수 있습니다. 가치 유형입니다.

Matt Ellis 기사에 대한 사소한 설명은 참조 유형과 값 유형 간의 사용 의미의 차이가 불변성이 유효 할 때 "미미"하다는 것입니다 (물론 여기에 해당됨). 그럼에도 불구하고 튜플이 일부 임계 값에서 참조 유형으로 교차하는 혼란을 야기하지 않는 것이 BCL 디자인에서 가장 좋았을 것이라고 생각합니다.


값이 반환 된 후 한 번 사용되는 경우 모든 크기의 노출 된 필드 구조체는 스택을 날려 버릴만큼 엄청나게 크지 않은 경우에만 다른 유형을 능가합니다. 클래스 객체를 만드는 비용은 참조가 여러 번 공유되는 경우에만 회수됩니다. 범용 고정 크기 이기종 유형이 클래스가되는 것이 유용 할 때가 있지만 구조체가 "큰"것에도 더 나은 경우가 있습니다.
supercat 2013

이 유용한 경험 규칙을 추가해 주셔서 감사합니다. 하지만 당신이 내 입장을 오해하지 않았 으면 좋겠다. 나는 가치 형 중독자 다. ( stackoverflow.com/a/14277068 은 의심의 여지가 없습니다).
Glenn Slayden 2013

값 유형은 .net의 훌륭한 기능 중 하나이지만, 불행히도 msdn dox를 작성한 사람은 서로 다른 사용 사례가 여러 개 있으며 사용 사례마다 지침이 다르다는 것을 인식하지 못했습니다. msdn이 권장하는 struct 스타일은 균질 한 값을 나타내는 struct에만 사용해야 하지만 덕트 테이프와 함께 고정 된 몇 가지 독립적 인 값을 나타내야하는 경우에는 해당 스타일의 struct를 사용해서는 안됩니다. 노출 된 공개 필드.
supercat 2013

0

모르겠지만 F # 튜플을 사용한 적이 있다면 언어의 일부입니다. .dll을 만들고 튜플 유형을 반환 한 경우이를 넣을 유형을 갖는 것이 좋습니다. 이제 F #이 언어 (.Net 4)의 일부인 것으로 의심됩니다. CLR에 일부 공통 구조를 수용하기 위해 일부 수정이 이루어졌습니다. F #에서

에서 http://en.wikibooks.org/wiki/F_Sharp_Programming/Tuples_and_Records

let scalarMultiply (s : float) (a, b, c) = (a * s, b * s, c * s);;

val scalarMultiply : float -> float * float * float -> float * float * float

scalarMultiply 5.0 (6.0, 10.0, 20.0);;
val it : float * float * float = (30.0, 50.0, 100.0)
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.