Haskell (GHC)이 왜 그렇게 빠른가?


247

Haskell ( GHC컴파일러 포함)은 예상보다 훨씬 빠릅니다 . 올바르게 사용하면 저수준 언어에 가까워 질 수 있습니다. (하스 켈러가 가장 좋아하는 일은 C의 5 % 이내에서 시도하는 것입니다 (또는 이길 수도 있지만 GHC가 Haskell을 C로 컴파일하기 때문에 비효율적 인 C 프로그램을 사용하고 있음을 의미합니다). 내 질문은 왜?

Haskell은 선언적이며 람다 미적분학을 기반으로합니다. 튜링 머신을 기반으로하는 머신 아키텍처는 분명히 필수적입니다. 실제로 Haskell은 특정 평가 순서조차 없습니다. 또한 머신 데이터 유형을 처리하는 대신 항상 대수 데이터 유형을 만듭니다.

그러나 가장 좋은 것은 고차 함수입니다. 함수를 즉석에서 생성하고 던지면 프로그램 속도가 느려질 것이라고 생각할 것입니다. 그러나 고차 함수를 사용하면 실제로 Haskell이 더 빨라집니다. 실제로 Haskell 코드를 최적화하려면 더 기계적인 것이 아니라 더 우아하고 추상적으로 만들어야합니다. Haskell의 고급 기능 중 어느 것도 개선하지 않으면 성능에 영향을 미치지 않는 것으로 보입니다 .

이것이 소리가 들리는 것은 유감이지만 여기에 내 질문이 있습니다 : 왜 Haskell (GHC로 컴파일 됨)은 추상적 특성과 물리적 기계와의 차이점을 고려할 때 그렇게 빠릅니까?

참고 : 내가 C와 다른 명령 언어가 Turing Machines와 다소 비슷하다고 말한 이유는 Haskell이 Lambda Calculus와 유사하지는 않지만 명령 언어에서는 유한 한 수의 상태 (일명 줄 번호)가 있기 때문입니다. 상태와 현재 테이프가 테이프에 수행 할 작업을 결정하도록 테이프 (램)와 함께. Turing Machines에서 컴퓨터로의 전환에 대해서는 Wikipedia 항목 인 Turing machine equivalents를 참조하십시오 .


27
"GHC가 Haskell을 C로 컴파일 한 이후"-그렇지 않습니다. GHC에는 여러 개의 백엔드가 있습니다. 가장 오래된 것 (기본값은 아님)은 C 생성기입니다. IR에 대한 Cmm 코드를 생성하지만 일반적으로 기대하는 "C로 컴파일"하는 것은 아닙니다. ( downloads.haskell.org/~ghc/latest/docs/html/users_guide/… )
viraptor 2018 년

20
Simon Payton Jones (GHC의 주요 구현 자)의 Functional Programming Languages의 구현을 읽는 것이 좋습니다 . 많은 질문에 대답 할 것입니다.
Joe Hillenbrand

94
왜? 25 년의 노력.
8

31
"그에 대한 사실적인 답변이있을지라도 의견을 구하는 것 외에는 아무것도 할 수 없습니다." -이것이 질문을 끝내는 최악의 이유입니다. 이 때문에 있는 좋은 대답을하지만, 잠재적으로 낮은 품질의 사람들을 끌 것이다. 왝! 나는 학문적 연구와 특정 발전이 일어난시기에 대해 좋은 역사적 역사적 사실 답변을 얻었습니다. 그러나 사람들 이이 질문이 품질이 낮은 답변을 끌 수도 있다고 걱정하기 때문에 게시 할 수 없습니다. 다시 요
sclv

7
@cimmanon 나는의 기초를 걸어 한 달 또는 여러 블로그 게시물을 필요 자세한 방법 기능 컴파일러 작품. 주식 하드웨어에서 그래프 머신을 깨끗하게 구현하고 추가 정보를 얻기 위해 관련 소스를 가리키는 방법을 개략적으로 설명하기 위해서는 SO 답변 만 필요합니다.
sclv

답변:


264

Dietrich Epp에 동의합니다 . GHC를 빠르게 만드는 여러 가지 요소 의 조합입니다 .

무엇보다 Haskell은 매우 높은 수준입니다. 이를 통해 컴파일러는 코드를 손상시키지 않고 적극적인 최적화 를 수행 할 수 있습니다.

SQL에 대해 생각하십시오. 이제 SELECT명령문을 작성할 때 명령 루프 처럼 보일있지만 그렇지 않습니다 . 그것은 수있는 모습 이 지정된 조건과 일치하는 하나를 찾기 위해 노력하고 그 테이블의 모든 행을 통해 루프하지만, 실제로는 완전히 다른 성능 특성을 가지고있는 -은 "컴파일러"(DB를 엔진) 조회 대신 인덱스를 수행 할 수있다. 그러나 SQL은 매우 수준이 높기 때문에 "컴파일러"는 완전히 다른 알고리즘을 대체하고 여러 프로세서 나 I / O 채널 또는 전체 서버를 투명하게 적용 할 수 있습니다.

하스켈은 같은 것으로 생각합니다. 방금 Haskell에 입력 목록을 두 번째 목록에 매핑하고 두 번째 목록을 세 번째 목록으로 필터링 한 다음 결과 항목 수를 계산하도록 요청 했다고 생각할 수 있습니다 . 그러나 GHC가 장면 뒤에서 스트림 퓨전 재 작성 규칙을 적용하여 전체 항목을 하나의 엄격한 기계 코드 루프로 변환하여 할당없이 데이터를 한 번에 전달하는 전체 작업을 수행하는 것을 보지 못했습니다. 지루하고 오류가 발생하기 쉬우 며 손으로 직접 작성하는 것이 불가능합니다. 코드에 하위 수준의 세부 정보가 없기 때문에 실제로 가능합니다.

그것을 보는 또 다른 방법은… 왜 Haskell이 빠르지 않아야 하는가? 속도를 늦추려면 어떻게해야합니까?

Perl 또는 JavaScript와 같은 해석 된 언어가 아닙니다. Java 나 C #과 같은 가상 머신 시스템도 아닙니다. 네이티브 머신 코드까지 완전히 컴파일되므로 오버 헤드가 없습니다.

OO 언어 [Java, C #, JavaScript…]와 달리 Haskell은 [C, C ++, Pascal…]과 같은 전체 유형을 지 웁니다. 모든 유형 검사는 컴파일 타임에만 발생합니다. 따라서 속도를 늦추기 위해 런타임 유형 검사가 없습니다. (이 문제에 대해서는 null 포인터 검사가 없습니다. Java의 경우 JVM에서 null 포인터를 검사하고 예외를 지연하면 예외를 throw해야합니다. Haskell은 해당 검사를 신경 쓸 필요가 없습니다.)

"런타임에서 함수를 즉시 생성"하는 것은 느리게 들리지만 매우주의 깊게 살펴보면 실제로 그렇게하지는 않습니다. 그것은 할 수 처럼 당신이,하지만 당신은하지 않습니다. 당신이 말한다면 (+5), 글쎄, 그건 하드 코딩 된 소스 코드에있다. 런타임시 변경할 수 없습니다. 따라서 실제로 동적 기능은 아닙니다. 카레 기능조차도 실제로 데이터 블록에 매개 변수를 저장합니다. 모든 실행 코드는 실제로 컴파일 타임에 존재합니다. 런타임 해석이 없습니다. "평가 기능"이있는 다른 언어와는 다릅니다.

파스칼에 대해 생각하십시오. 오래되어 아무도 더 이상 사용하지 않지만 파스칼이 느리다고 불평하는 사람은 아무도 없습니다 . 싫어하는 것이 많지만 느림은 실제로 그중 하나가 아닙니다. Haskell은 수동 메모리 관리가 아닌 가비지 수집을 제외하고는 Pascal과 다른 점을 실제로 많이하지 않습니다. 그리고 불변의 데이터는 GC 엔진에 몇 가지 최적화를 허용한다 [지연 한 평가는 다소 복잡하다].

문제는 하스켈 고급 스럽고 정교하고 고급 스러워 보인다 는 것입니다. 모두들 "오 와우, 이거 정말 강력 합니다. 놀랍도록 느려 야합니다! " 또는 적어도, 그것은 당신이 기대하는 방식이 아닙니다. 그렇습니다. 놀라운 유형의 시스템이 있습니다. 그러나 당신은 무엇을 알고 있습니까? 모든 것은 컴파일 타임에 발생합니다. 런타임에 사라졌습니다. 예, 한 줄의 코드로 복잡한 ADT를 구성 할 수 있습니다. 그러나 당신은 무엇을 알고 있습니까? ADT는 단순한 평범한 C union입니다 struct. 더 이상 없습니다.

진짜 살인자는 게으른 평가입니다. 코드의 엄격함 / 게으름을 올바르게 얻으면 여전히 우아하고 아름다운 어리석은 빠른 코드를 작성할 수 있습니다. 그러나이 문제가 발생하면 프로그램이 수천 배 느려집니다 . 이러한 이유는 분명하지 않습니다.

예를 들어, 파일에 각 바이트가 몇 번 나타나는지 계산하는 간단한 작은 프로그램을 작성했습니다. 25킬로바이트 입력 파일의 경우, 프로그램은했다 20분 실행하려면를 삼킬 6기가바이트 의 RAM을! 터무니없는! 그러나 문제가 무엇인지 깨달았고 단일 뱅 패턴을 추가했으며 런타임은 0.02 초로 떨어졌습니다 .

이곳 에서 Haskell이 예기치 않게 느리게 진행됩니다. 그리고 그것에 익숙해지기까지 확실히 시간이 걸립니다. 그러나 시간이 지남에 따라 정말 빠른 코드를 작성하는 것이 더 쉬워집니다.

하스켈을 그렇게 빨리 만드는 이유는 무엇입니까? 청정. 정적 유형. 게으름. 그러나 무엇보다도 컴파일러가 코드의 기대치를 손상시키지 않고 구현을 근본적으로 변경할 수있을 정도로 충분히 높은 수준입니다.

하지만 그건 내 의견 일뿐입니다 ...


13
@cimmanon 나는 그것이 순수한 의견 기반 이라고 생각하지 않습니다 . 다른 사람들이 대답을 원했던 것은 흥미로운 질문입니다. 하지만 다른 유권자들의 생각을 보게 될 것 같습니다.
MathematicalOrchid

8
@cimmanon-이 검색은 1.5 스레드 만 제공하며 모두 검토 감사와 관련이 있습니다. 스레드에 대한 upvoted 응답은 "당신이 이해하지 못하는 것들의 검토를 중단하십시오." 누군가가 이것에 대한 대답이 반드시 너무 광범위하다고 생각하면 대답이 너무 광범위하지 않기 때문에 놀랍고 대답을 즐길 것이라고 제안합니다.
sclv

34
"예를 들어 Java에서 JVM은 널 포인터를 확인하고이를 지연시키는 경우 예외를 발생시켜야합니다." Java의 암시 적 널 검사는 (대부분) 비용이 들지 않습니다. Java 구현은 가상 메모리를 사용하여 널 주소를 누락 된 페이지에 맵핑 할 수 있으므로 널 포인터를 역 참조하면 CPU 레벨에서 페이지 결함이 트리거되어 Java가 상위 예외로 포착됩니다. 따라서 대부분의 null 검사는 CPU의 메모리 매핑 장치에서 무료로 수행됩니다.
Boann

4
@cimmanon : 아마도 그것은 Haskell 사용자들이 실제로 친절한 개방적인 사람들의 유일한 공동체 인 것처럼 보이기 때문일 것입니다. 기회가있을 때마다 새로운 것을 서로 찢어 버리십시오…“정상적인”것으로 간주되는 것 같습니다.
Evi1M4chine

14
@MathematicalOrchid : 실행하는데 20 분이 걸린 오리지널 프로그램의 사본이 있습니까? 왜 그렇게 느린 지 연구하는 것이 꽤 유익하다고 생각합니다.
George

79

오랫동안 기능적 언어, 특히 게으른 기능적 언어는 빠를 수 없다고 생각되었습니다. 그러나 이것은 초기 구현이 본질적으로 해석되고 진정으로 컴파일되지 않았기 때문입니다.

그래프 축소를 기반으로 디자인의 두 번째 물결이 나타 났으며 훨씬 더 효율적인 컴파일 가능성을 열었습니다. 사이먼 페이튼 존스는 자신의 두 권의 책에서이 연구에 대해 쓴 기능 프로그래밍 언어의 구현튜토리얼 : 구현 함수형 언어 (Wadler와 핸콕에 의해 섹션 전, 데이비드 레스터 작성 후자). (Lennart Augustsson은 또한 이전 책의 주요 동기 중 하나는 광범위하게 주석을 달지 않은 LML 컴파일러가 컴파일을 수행하는 방식을 설명하고 있다고 알려주었습니다.)

이러한 작업에서 설명한 것과 같은 그래프 축소 방법의 핵심 개념은 프로그램을 일련의 명령어로 생각하지 않고 일련의 로컬 축소를 통해 평가 되는 종속성 그래프라는 것입니다 . 두 번째 핵심 통찰력은 이러한 그래프의 평가를 해석 할 필요가 없지만 그래프 자체 를 코드로 작성할 수 있다는 것 입니다 . 특히, 그래프의 노드를 "값 또는 'opcode'및 작동 할 값"이 아니라 호출 할 때 원하는 값을 반환하는 함수로 나타낼 수 있습니다. 이 호출 처음 그들의 값에 대한 하위 노드를 요구 한 다음에 작동 하고 그냥 결과를 반환 "라는 새로운 명령어 자체를 덮어 씁니다.

이것은 GHC가 오늘날에도 어떻게 작동하는지에 대한 기본 사항을 제시하는 후반부에 설명되어 있습니다 . . GHC의 현재 실행 모델은 GHC Wiki 에 자세히 설명되어 있습니다.

통찰은 기계가 작동하는 방식이 기계가 작동하는 방식이 아니라 작동 방식에 대한 "기본"으로 생각하는 "데이터"와 "코드"의 엄격한 구분은 컴파일러에 의해 부과된다는 것입니다. 그래서 우리는 그것을 버릴 수 있고 자체 수정 코드 (실행 파일)를 생성하는 코드 (컴파일러)를 가질 수 있으며 모두 훌륭하게 작동 할 수 있습니다.

따라서 기계 아키텍처는 특정 의미에서 필수적이지만 언어는 기존의 C 스타일 흐름 제어처럼 보이지 않는 매우 놀라운 방식으로 언어에 매핑 될 수 있으며, 저수준으로 충분히 생각하면 이는 또한 실력 있는.

이 외에도 더 많은 범위의 "안전한"변환이 가능하기 때문에 특히 순도에 의해 다른 많은 최적화가 열립니다. 상황을 개선하고 나쁘게 만들지 않도록 이러한 변환을 적용하는시기와 방법은 물론 경험적 문제이며, 이것과 다른 많은 작은 선택에서 이론적 작업과 실제 벤치마킹에 수년간의 작업이 진행되었습니다. 물론 이것 또한 중요한 역할을합니다. 이런 종류의 연구에 대한 좋은 예를 제공하는 논문은 " 빠른 카레 만들기 : 푸시 / 엔터 vs. 평가 / 고급 언어 적용"입니다.

마지막으로,이 모델은 간접적으로 인해 여전히 오버 헤드를 발생시킵니다. 이것은 일을 엄격히 수행하여 그래프 간접 지시를 제거하는 것이 "안전"하다는 것을 알고있는 경우에는 피할 수 있습니다. 엄격 성 / 요구를 유추하는 메커니즘은 GHC Wiki 에 다시 자세히 설명되어 있습니다.


2
그 수요 분석기 링크는 금의 무게 가치가 있습니다! 마침내 기본적으로 설명 할 수없는 흑 마법처럼 행동하지 않는 주제에 관한 것입니다. 내가 어떻게 들어 본 적이 없습니까? 게으름으로 문제를 해결하는 방법을 묻는 모든 곳에서 연결되어야합니다!
Evi1M4chine

@ Evi1M4chine 수요 분석기와 관련된 링크가 표시되지 않습니다. 어쩌면 손실되었을 수 있습니다. 누군가가 링크를 복원하거나 참조를 명확히 할 수 있습니까? 꽤 흥미로운 것 같습니다.
Cris P

1
@CrisP 마지막 링크가 참조되고 있다고 생각합니다. GHC의 수요 분석기에 대한 GHC Wiki의 페이지로 이동합니다.
Serp C

@Serpentine Cougar, Chris P : 응, 그게 내 뜻이야.
Evi1M4chine

19

글쎄, 여기에 의견이 많습니다. 최대한 답변을 드리려고 노력하겠습니다.

올바르게 사용하면 저수준 언어에 가까워 질 수 있습니다.

내 경험상 대개 많은 경우 Rust의 성능을 2 배 이내로 유지할 수 있습니다. 그러나 저수준 언어에 비해 성능이 떨어지는 일부 (광범위한) 사용 사례도 있습니다.

GHC가 Haskell을 C로 컴파일하기 때문에 비효율적 인 C 프로그램을 사용하고 있음을 의미합니다)

그것은 완전히 정확하지 않습니다. Haskell은 C-(C의 하위 집합)로 컴파일 한 다음 네이티브 코드 생성기를 통해 어셈블리로 컴파일됩니다. 네이티브 코드 생성기는 일반적으로 C 컴파일러보다 빠른 코드를 생성합니다. 일반 C 컴파일러로는 불가능한 일부 최적화를 적용 할 수 있기 때문입니다.

튜링 머신을 기반으로하는 머신 아키텍처는 분명히 필수적입니다.

특히 최신 프로세서가 명령을 순서에 따라 동시에 평가할 수 있기 때문에 생각하기에 좋은 방법은 아닙니다.

실제로 Haskell은 특정 평가 순서조차 없습니다.

실제로 Haskell 평가 순서를 암시 적으로 정의합니다.

또한 머신 데이터 유형을 처리하는 대신 항상 대수 데이터 유형을 만듭니다.

충분히 고급 컴파일러가있는 경우 많은 경우에 해당합니다.

함수를 즉석에서 생성하고 던지면 프로그램 속도가 느려질 것이라고 생각할 것입니다.

Haskell이 컴파일되므로 실제로 고차 함수가 생성되지 않습니다.

Haskell 코드를 최적화하는 것처럼 보이므로 더 많은 기계 대신 더 우아하고 추상적으로 만들어야합니다.

일반적으로 코드를 "기계처럼"만드는 것은 Haskell에서 더 나은 성능을 얻는 비생산적인 방법입니다. 그러나 더 추상적으로 만드는 것이 항상 좋은 생각은 아닙니다. 무엇 이다 좋은 생각하는 것은 일반적인 데이터 구조 (예 : 연결리스트 등) 많이 최적화 된 기능을 사용하고 있습니다.

f x = [x]그리고 f = pure예를 들어, 하스켈에서 똑같은 일이 있습니다. 좋은 컴파일러는 전자의 경우 더 나은 성능을 얻지 못할 것입니다.

Haskell (GHC로 컴파일)이 추상적 인 특성과 물리적 시스템과의 차이점을 고려할 때 왜 그렇게 빠른가요?

짧은 대답은 "정확히 그렇게하도록 설계 되었기 때문"입니다. GHC는 스파인리스 태그리스 G- 머신 (STG)을 사용합니다. 여기 에 관한 논문을 읽을 수 있습니다 (매우 복잡합니다). GHC는 엄격 성 분석 및 낙관적 평가 와 같은 다른 많은 기능도 수행 합니다.

내가 C와 다른 명령형 언어가 Turing Machines와 다소 비슷하다고 말한 이유는 Haskell이 Lambda Calculus와 비슷한 정도는 아닙니다. 테이프 (램)를 사용하여 상태 및 현재 테이프가 테이프에 대해 수행 할 작업을 결정합니다.

혼란의 요점은 그 가변성이 코드의 속도를 늦춰야 하는가? Haskell의 게으름은 실제로 변경 가능성이 생각만큼 중요하지 않으며 컴파일러가 적용 할 수있는 많은 최적화가 있기 때문에 높은 수준입니다. 따라서, 내부에서 레코드를 수정하는 것이 C와 같은 언어에서보다 느릴 것입니다.


3

Haskell (GHC)이 왜 그렇게 빠른가?

Haskell의 성능을 마지막으로 측정 한 후 무언가가 크게 바뀌었을 것 입니다. 예를 들면 다음과 같습니다.

  • RRD 파일 처리 저자가 하스켈 개발하는 데 시간이 더 걸렸다 및 실행 더 느리게 (1,020s) 이동 (130S)와 OCaml의 (67s)에 비해 발견 벤치 마크, 즉 15 배 느린 OCaml의 이상.
  • 간단한 사전 벤치 마크는 하스켈 ~ 10 배 느린 F 번호에 비해 실행했다.
  • 레이 트레이서 언어 비교 하스켈은 C보다 느린했다 또 다른 예입니다 ++, OCaml로, 심지어 자바와 리스프.
  • Haskell 의 병렬 일반 퀵 정렬 은 F #보다 55 % 느리고 실질적으로 더 많은 코드가 필요합니다.

그래서 무엇이 바뀌 었습니까? 질문이나 현재 답변 중 어느 것도 검증 가능한 벤치 마크 또는 코드를 나타내는 것이 아닙니다.

Haskellers가 가장 좋아하는 일은 C의 5 % 이내로 시도하고 얻는 것입니다

누구든지 근처에서 얻은 검증 가능한 결과에 대한 언급이 있습니까?


6
누군가 거울 앞에서 Harrop의 이름을 다시 세 번이나 말했습니까?
척 아담스

2
10 배는 아니지만 여전히이 전체 항목 과대 광고 및 마케팅입니다. GHC 는 실제로 속도 측면에서 C에 가까워 지거나 때로는 극복 할 수 있지만, 일반적으로 C 자체의 프로그래밍과 크게 다르지 않은 매우 관련성이 높은 저수준의 프로그래밍 스타일이 필요합니다. 운수 나쁘게. 코드가 높을수록 일반적으로 느립니다. 공간 누출, 편리한 아직 underperformant ADT 유형 ( 대수 가 아닌 추상적 인 등이었다 약속) 등 등
윌 네스

1
오늘 그것을 보았 기 때문에 이것을 게시하고 있습니다 . chrispenner.ca/posts/wc . 그것은 아마도 C 버전을 능가하는 Haskell로 작성된 wc 유틸리티의 구현입니다.
Garrison

3
링크 에 대한 @Garrison 감사합니다 . 80 줄은 제가 "C 자체 프로그래밍과 크게 다르지 않은 저수준 프로그래밍 스타일"이라고했습니다. . "상위 코드"는 "stupid" fmap (length &&& length . words &&& length . lines) readFile입니다. 경우 보다 더 빨리했다 (또는 비교를 위해) C, 여기에 마약 중독 완전히 정당화 될 것입니다 . 우리는 여전히 C 에서처럼 Haskell에서 속도를 위해 열심히 노력해야합니다.
Will Ness

2
Reddit reddit.com/r/programming/comments/dj4if3/… 에 대한이 토론으로 판단 하면 Haskell 코드는 실제로 버그가 있습니다 (예 : 줄 바꿈은 공백으로 시작하거나 끝남, à에서 끊김).
Jon Harrop
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.