int와 같은 기본 유형을 클래스로 구현할 때의주의 사항은 무엇입니까?


27

객체 지향 프로그래밍 언어를 디자인하고 함축 할 때, 어떤 시점에서 기본 유형 (예 int: float, double또는 등가물)을 클래스 또는 다른 것으로 구현하는 방법을 선택해야합니다 . 분명히 C 패밀리의 언어는 클래스로 정의 하지 않는 경향이 있습니다 (자바에는 특수한 기본 유형이 있고 C #은이를 불변의 구조체로 구현합니다).

기본 유형이 클래스로 구현 될 때 (통합 계층 구조가있는 유형 시스템에서) 매우 중요한 이점을 생각할 수 있습니다. 이러한 유형은 루트 유형의 적절한 Liskov 하위 유형이 될 수 있습니다. 따라서 우리는 언어를 boxing / unboxing (명시 적 또는 암시 적), 랩퍼 유형, 특수 분산 규칙, 특수 동작 등과 복잡하게 만드는 것을 피합니다.

물론 언어 디자이너가 자신의 방식을 결정하는 이유를 부분적으로 이해할 수 있습니다. 클래스 인스턴스는 메모리 레이아웃에 vtable 또는 기타 메타 데이터가 포함될 수 있기 때문에 약간의 공간 오버 헤드가있는 경향이 있습니다. (언어가 상속을 허용하지 않는 경우).

기본 유형이 종종 클래스 가 아닌 유일한 이유는 공간 효율성 (및 특히 큰 배열에서 향상된 공간 위치) 입니까?

필자는 일반적으로 대답이 '예'라고 가정했지만 컴파일러에는 이스케이프 분석 알고리즘이 있으므로 인스턴스 (기본 유형이 아닌 모든 인스턴스)가 엄격하게 입증되면 공간 오버 헤드를 생략 할 수 있는지 여부를 추론 할 수 있습니다 노동 조합 지부.

위의 내용이 잘못 되었습니까? 아니면 누락 된 것이 있습니까?


답변:


19

그렇습니다, 그것은 효율성에 달려 있습니다. 그러나 영향을 과소 평가하거나 다양한 최적화가 얼마나 잘 작동하는지 과대 평가하는 것 같습니다.

첫째, 그것은 단지 "공간적 오버 헤드"가 아닙니다. 박스형 / 힙 할당 된 프리미티브를 만들면 성능 비용도 있습니다. GC에는 이러한 객체를 할당하고 수집해야하는 추가적인 압력이 있습니다. "원시 객체"가 불변 인 경우에는 두 배가됩니다. 그런 다음 간접적 인 캐시와 주어진 캐시 양에 적은 데이터가 들어가기 때문에 캐시 누락이 더 많습니다. "객체의 주소를로드 한 다음 해당 주소에서 실제 값을로드"한다는 사실은 "값을 직접로드"하는 것보다 더 많은 지시를받습니다.

둘째, 탈출 분석은 더 빠른 요정 먼지가 아닙니다. 이스케이프하지 않는 값에만 적용됩니다. 루프 계산 및 중간 계산 결과와 같은 로컬 계산을 최적화하는 것이 좋으며 측정 가능한 이점이 있습니다. 그러나 훨씬 더 많은 값이 객체와 배열의 필드에 존재합니다. 물론, 그것들은 이스케이프 분석 자체가 될 수 있지만, 일반적으로 변경 가능한 참조 유형이기 때문에, 그것들의 앨리어싱은 이스케이프 분석에 중대한 도전을 제시합니다. (2) 할당을 제거 할 목적으로 차이를 만들지 않습니다.

(게터 포함) 메소드를 호출하거나 인자로 객체를 전달 감안할 때 모든 객체의 탈출을 도울 수있는 다른 방법을하면, 당신은 모두 간 분석하지만, 대부분의 사소한 경우 필요합니다. 이것은 훨씬 비싸고 복잡합니다.

그리고 상황이 실제로 탈출하여 합리적으로 최적화 할 수없는 경우가 있습니다. 실제로 C 프로그래머가 힙 할당 문제를 얼마나 자주 겪고 있는지 고려한다면 상당히 많은 것들입니다. int가 포함 된 객체가 이스케이프하면 이스케이프 분석이 int에 적용되지 않습니다. 효율적인 프리미티브 필드에 작별 인사를하십시오 .

이는 또 다른 요점과 관련이 있습니다. 필요한 분석 및 최적화는 매우 복잡하고 활발한 연구 분야입니다. 어떤 언어 구현이 제안한 최적화 수준을 달성했는지 여부는 논쟁의 여지가 있지만, 그렇게해도 드물고 힘든 노력이었습니다. 이 거인들의 어깨에 서있는 것이 거인이되는 것보다 쉽지만 여전히 사소한 것은 아닙니다. 처음 몇 년 동안 언제라도 경쟁력있는 성능을 기대하지 마십시오.

그것은 그런 언어가 실용적이지 않다는 것은 아닙니다. 분명히 그들은 있습니다. 전용 프리미티브가있는 언어만큼 빠른 라인-라인이라고 가정하지 마십시오. 다시 말해, 충분히 똑똑한 컴파일러의 비전으로 자신을 회피하지 마십시오 .


탈출 분석에 대해 이야기 할 때, 나는 또한 자동 스토리지에 할당하는 것을 의미했습니다 (모든 것을 해결하지는 않지만 말한 것처럼 일부 것을 해결합니다). 또한 필드와 앨리어싱이 이스케이프 분석에 더 자주 실패 할 수있는 정도를 과소 평가했음을 인정합니다. 캐시 미스는 공간 효율성에 대해 이야기 할 때 내가 가장 염려 한 부분이므로이를 해결해 주셔서 감사합니다.
Theodoros Chatzigiannakis

@TheodorosChatzigiannakis 탈출 분석에 할당 전략을 변경하는 것이 포함되어 있습니다 (정직하게 사용 된 유일한 것으로 보이므로).

두 번째 단락 : 개체가 항상 힙 할당되거나 참조 유형일 필요는 없습니다. 실제로 그렇지 않은 경우 필요한 최적화를 비교적 쉽게 수행 할 수 있습니다. 초기 예제는 C ++의 스택 할당 객체와 이스케이프 분석을 언어로 직접 굽는 방법에 대한 Rust의 소유권 시스템을 참조하십시오.
amon

@amon 알고 있고 아마도 더 명확하게 만들어야했지만 OP는 하위 의미 사이의 참조 의미와 무손실 캐스트로 인해 힙 할당이 거의 필수적이고 암시적인 Java 및 C # 언어에만 관심이있는 것 같습니다. 그럼에도 불구하고 분석에서 벗어날 양을 사용하는 녹에 대한 좋은 점!

@delnan 스토리지 세부 사항을 추상화하는 언어에 주로 관심이 있지만, 해당 언어에 해당되지 않더라도 관련성이 있다고 생각되는 것을 자유롭게 포함하십시오.
Theodoros Chatzigiannakis

27

기본 유형이 종종 클래스가 아닌 유일한 이유는 공간 효율성 (및 특히 큰 배열에서 향상된 공간 위치)입니까?

아니.

다른 문제는 기본 유형이 기본 작업에 사용되는 경향이 있다는 것입니다. 컴파일러 int + int는 함수 호출로 컴파일되지 않고 일부 기본 CPU 명령어 (또는 동등한 바이트 코드)로 컴파일 될 것임을 알아야합니다 . 이 시점에서 int일반 객체로 사용하는 경우 어쨌든 효과적으로 물건을 개봉해야합니다.

이러한 종류의 작업은 실제로 서브 타이핑을 잘하지 않습니다. CPU 명령으로 디스패치 할 수 없습니다. CPU 명령어 에서 디스패치 할 수 없습니다 . 하위 유형의 전체 요점은 당신이 할 수있는 D곳을 사용할 수 있다는 것을 의미합니다 B. CPU 명령어는 다형성이 아닙니다. 프리미티브가이를 수행하려면 간단한 추가 (또는 기타)로 여러 배의 작업 비용이 소요되는 디스패치 로직으로 작업을 래핑해야합니다. int유형 계층 구조의 일부가 된다는 이점은 봉인 / 최종시 약간의 문제가됩니다. 그리고 그것은 바이너리 연산자에 대한 디스패치 로직으로 모든 두통을 무시하고 있습니다 ...

기본적으로, 원시적 형은 주위에 특별한 규칙을 많이 가질 필요가 얼마나 컴파일러 핸들 그들, 사용자가 자신의 유형으로 무엇을 할 수 있는지 어쨌든 , 그냥 완전히 별개로 취급하는 것이 시간이 간단하므로.


4
정수 등의 객체를 처리하는 동적 형식 언어의 구현을 확인하십시오. 최종 원시 CPU 명령어는 런타임 라이브러리에서 유일하게 권한이 부여 된 클래스 구현의 메소드 (오퍼레이터 과부하)에 숨겨 질 수 있습니다. 정적 유형 시스템과 컴파일러에서는 세부 사항이 다르게 보일 수 있지만 근본적인 문제는 아닙니다. 최악의 경우 상황이 더 느려집니다.

3
int + int기본 CPU 정수 덧셈 op로 컴파일되거나 동작하는 내장 명령어를 호출하는 정규 언어 수준 연산자 일 수 있습니다. 에서 int상속 의 이점은 에서 object다른 유형을 상속 할 수있을 int뿐만 아니라 권투없이 int행동 할 수도 있다는 것입니다 object. C # 제네릭을 고려하십시오. 공분산 및 반공 산성을 가질 수 있지만 클래스 유형에만 적용 가능합니다. 구조체 유형은 object(암시 적, 컴파일러 생성) 복싱을 통해서만 가능하기 때문에 자동으로 제외됩니다 .
Theodoros Chatzigiannakis

3
@delnan-정적 형식 구현에 대한 경험에서 볼 때 모든 비 시스템 호출은 기본 작업으로 귀결되기 때문에 오버 헤드가 있으면 성능에 큰 영향을 미치므로 채택에 훨씬 더 큰 영향을 미칩니다.
Telastyn

@TheodorosChatzigiannakis-훌륭합니다. 따라서 유용한 하위 / 수퍼 유형이없는 유형에 대한 분산과 모순을 얻을 수 있습니다 ... 그리고 CPU 연산자를 호출하기 위해 특수 연산자를 구현하면 여전히 특별합니다. 나는 그 생각에 동의하지 않습니다-나는 장난감 언어로 매우 비슷한 일을했지만, 구현하는 동안 당신이 기대하는 것만 큼 깨끗하지 않은 실질적인 문제가 있음을 발견했습니다.
Telastyn

1
@TheodorosChatzigiannakis 라이브러리 경계를 가로 질러 인라인하는 것은 확실히 가능하지만 "내가 갖고 싶은 고급 최적화"쇼핑 목록의 또 다른 항목입니다. 나는 쓸모 없을 정도로 보수적이지 않고 완전히 바르게되는 것이 악명 높지만 지적해야 할 의무가 있다고 생각합니다.

4

"기본 유형"이 전체 객체가되는 경우는 거의 없습니다 (여기서 객체는 디스패치 메커니즘에 대한 포인터를 포함하거나 디스패치 메커니즘에서 사용할 수있는 유형으로 태그가 지정된 데이터입니다).

  • 사용자 정의 유형이 기본 유형에서 상속 할 수 있기를 원합니다. 일반적으로 성능 및 보안 관련 두통을 유발하므로 바람직하지 않습니다. 컴파일은 int특정 고정 크기를 가지거나 메소드가 재정의 되지 않았다고 가정 할 수 없기 때문에 성능 문제 이며, ints의 의미 가 파괴 될 수 있기 때문에 보안 문제입니다 (임의의 정수 또는 정수를 고려하십시오). 불변이 아닌 값을 변경합니다).

  • 기본 유형에는 수퍼 유형이 있으며 기본 유형의 수퍼 유형 유형의 변수를 가지려고합니다. 예를 들어, 가정 int의가있다 Hashable, 당신은받는 함수 선언 할 Hashable일반 객체를받을뿐만 아니라 수있는 매개 변수 int들.

    이러한 유형은 불법으로 만들어 "해결"할 수 있습니다. 하위 유형을 제거하고 인터페이스가 유형이 아니라 유형 제약 조건이라고 결정하십시오. 분명히 이것은 타입 시스템의 표현력을 감소 시키며, 그러한 타입 시스템은 더 이상 객체 지향이라고 할 수 없습니다. 이 전략을 사용하는 언어에 대해서는 Haskell을 참조하십시오. 원시 형에는 수퍼 타입이 없기 때문에 C ++는 반쯤 있습니다.

    대안은 기본 유형의 전체 또는 부분 권투입니다. 권투 유형은 사용자가 볼 필요가 없습니다. 기본적으로 각 기본 유형에 대한 내부 박스 유형과 박스 유형과 기본 유형 사이의 암시 적 변환을 정의합니다. 박스형에 의미가 다른 경우에는 어색 할 수 있습니다. Java는 두 가지 문제점을 나타냅니다. 박스형 유형에는 동일성 개념이 있지만 기본 요소는 가치 동등성 개념 만 있으며 박스형은 널 입력 가능하지만 기본 요소는 항상 유효합니다. 이러한 문제는 값 유형에 대한 ID 개념을 제공하지 않고 연산자 오버로드를 제공하며 기본적으로 모든 객체를 nullable로 설정하지 않음으로써 완전히 피할 수 있습니다.

  • 정적 입력 기능이 없습니다. 변수는 기본 유형 또는 객체를 포함한 모든 값을 보유 할 수 있습니다. 따라서 강력한 타이핑을 보장하려면 모든 기본 유형을 항상 상자에 넣어야합니다.

정적 타이핑이있는 언어는 가능한 경우 기본 유형을 사용하는 것이 좋으며 최후의 수단으로 박스 유형으로 만 대체됩니다. 많은 프로그램이 성능에 크게 영향을 미치지는 않지만 기본 유형의 크기와 구성이 매우 관련이있는 경우가 있습니다. 수십억 개의 데이터 포인트를 메모리에 맞출 필요가있는 대규모 크 런칭을 생각해보십시오. 에서 전환doublefloatC에서 실행 가능한 공간 최적화 전략 일 수 있지만 모든 숫자 유형이 항상 상자에 들어 있으면 영향을 미치지 않습니다 (따라서 디스패치 메커니즘 포인터를 위해 메모리의 절반 이상을 낭비하십시오). 박스형 프리미티브 유형을 로컬로 사용하는 경우 컴파일러 내장 함수를 사용하여 복싱을 제거하는 것이 매우 간단하지만, 언어의 전체 성능을 "충분한 고급 컴파일러"에 베팅하는 것은 근시안적입니다.


int모든 언어에서 거의 불변이다.
Scott Whitlock

6
@ ScottWhitlock 왜 그렇게 생각할 수 있는지 알지만 일반적으로 기본 유형은 변경할 수없는 값 유형입니다. 제정신의 언어로는 숫자 7의 값을 변경할 수 없습니다. 그러나 많은 언어에서는 기본 유형의 값을 보유한 변수를 다른 값으로 재 지정할 수 있습니다. C와 같은 언어에서 변수는 명명 된 메모리 위치이며 포인터처럼 작동합니다. 변수가 가리키는 값과 다릅니다. int값은 불변하지만, int변수는 아니다.
amon

1
@amon : 제정신의 언어가 없습니다. Java : thedailywtf.com/articles/Disgruntled-Bomb-Java-Edition
Mason Wheeler

get rid of subtyping and decide that interfaces aren't types but type constraints.... such a type system wouldn't be called object-oriented any longer 그러나 이것은 프로토 타입 기반 프로그래밍처럼 들립니다. 이는 분명히 OOP입니다.
Michael

1
@ ScottWhitlock 문제는 int b = a가 있다면 a의 값을 변경하는 b에 무언가를 할 수 있는지 여부입니다. 이것이 가능한 일부 언어 구현이 있었지만 배열에 대해 동일한 작업을 수행하는 것과 달리 병리학 적이며 바람직하지 않은 것으로 간주됩니다.
Random832

2

내가 알고있는 대부분의 구현은 컴파일러가 대부분의 시간에 기본 표현으로 기본 유형을 효율적으로 사용할 수 있도록 그러한 클래스에 세 가지 제한을 부과합니다. 이러한 제한 사항은 다음과 같습니다.

  • 불변성
  • 최종성 (파생 불가능)
  • 정적 타이핑

컴파일러 기본 표현에서 객체에 프리미티브를 상자에 넣어야 하는 상황 은 Object참조가이를 가리키는 경우와 같이 비교적 드 rare니다 .

이것은 컴파일러에서 약간의 특수한 경우 처리를 추가하지만 신화적인 고급 고급 컴파일러에만 국한되지는 않습니다. 이 최적화는 주요 언어의 실제 프로덕션 컴파일러에 있습니다. 스칼라는 자신 만의 가치 클래스를 정의 할 수도 있습니다.


1

스몰 토크에서 그것들 (int, float 등)은 모두 일류 객체입니다. 특별한 경우 SmallIntegers이 성문화과 효율성을 위해 가상 머신에 의해 다르게 취급, 따라서 SmallInteger 클래스는 서브 클래스를 인정하지된다는 것입니다 (실제 제한하지 않은.)이 어떤 특별한 배려를 필요로하지 않습니다 코드 생성이나 가비지 수집과 같은 자동 루틴으로 구별되기 때문에 프로그래머의 입장에서.

스몰 토크 컴파일러 (소스 코드-> VM 바이트 코드)와 VM nativizer (바이트 코드-> 기계 코드)는 생성 된 코드 (JIT)를 최적화하여 이러한 기본 객체에 대한 기본 조작의 패널티를 줄입니다.


1

나는 OO 언어와 런타임을 설계하고 있었다 (이것은 완전히 다른 이유로 실패했다).

int true 클래스와 같은 것을 만드는 데 본질적으로 잘못된 것은 없습니다. 사실 이제는 3 개 (클래스, 배열 및 프리미티브)가 아닌 2 가지 종류의 힙 헤더 (클래스 및 배열) 만 있으므로 GC를보다 쉽게 ​​설계 할 수 있습니다. ].

프리미티브 유형에는 대부분 최종 / 밀봉 된 메소드가 있어야합니다 (+ 정말 중요합니다. ToString은 그다지 중요하지 않습니다). 이를 통해 컴파일러는 거의 모든 함수 호출을 정적으로 해결하고 인라인 할 수 있습니다. 대부분의 경우 이것은 복사 동작으로 중요하지 않습니다. (언어 수준에서 포함을 사용하도록 선택했습니다. [.NET도 마찬가지였습니다.]) 경우에 따라 메서드가 봉인되지 않으면 컴파일러에서 다음을 호출해야합니다. int + int를 구현하는 데 사용되는 함수

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.