GHC가 확실하게 수행 할 수있는 최적화 방법은 무엇입니까?


183

GHC는 수행 할 수있는 많은 최적화 기능을 가지고 있지만, 모두가 무엇인지, 그들이 수행 될 가능성과 상황에 대해 알지 못합니다.

내 질문은 : 매번 또는 거의 적용 할 수있는 변형은 무엇입니까? 자주 실행되는 (평가 된) 코드 조각을보고 첫 번째 생각이 "흠, 아마도 최적화해야 할 것"이라면, 두 번째 생각은 "생각하지 않아도됩니다. GHC가 이걸 얻었나요? "

나는 Stream Fusion : Lists에서 Streams, Nothing at All 논문을 읽었으며 GHC의 일반 최적화가 간단한 루프로 안정적으로 최적화하는 다른 형식으로 목록 처리를 다시 작성하는 기술은 나에게 새롭습니다. 내 프로그램이 이러한 종류의 최적화에 적합한 지 어떻게 알 수 있습니까?

GHC 매뉴얼 에는 몇 가지 정보 가 있지만 질문에 대한 답변으로 향하는 길의 일부입니다.

편집 : 현상금을 시작합니다. 내가 원하는 것은 람다 / 렛트 / 케이스 플로팅, 타입 / 생성자 / 함수 인수 전문화, 엄격 성 분석 및 언 박싱, 작업자 / 래퍼 및 내가 제외시킨 중요한 GHC 가하는 것과 같은 하위 레벨 변환 목록입니다. , 입력 및 출력 코드에 대한 설명 및 예, 그리고 전체 효과가 해당 부분의 합보다 큰 경우의 상황을 설명하는 것이 이상적입니다. 변혁 이 일어나지 않을 때에 대한 언급우연히 있다. 큰 그림이있는 한 모든 변환에 대한 소설 길이의 설명, 두 개의 문장 및 인라인 1 라이너 코드 예제로 충분할 수 있습니다 (또는 과학 페이지 20 페이지가 아닌 경우 링크). 그것의 끝까지 분명하다. 코드 조각을보고 그것이 꽉 찬 루프로 컴파일되는지, 왜 그렇지 않은지, 또는 그것을 변경하기 위해 무엇을 변경 해야하는지에 대해 좋은 추측을 할 수 있기를 원합니다. (내가 스트림 융합과 같은 큰 최적화 프레임 워크에 너무 많은 여기에 관심 없어 (그냥) 그것에 대해 논문을 읽고, 더 많은 사람들이 사실을 알고의 종류에 쓰기 이러한 프레임 워크가 있습니다.)


10
이것은 가장 가치있는 질문입니다. 가치있는 답변을 작성하는 것은 까다 롭습니다.
MathematicalOrchid

1
: 정말 좋은 출발점이있다 aosabook.org/en/ghc.html
가브리엘 곤잘레스

7
어떤 언어로든, 당신의 첫 생각이 "어쩌면 내가 그것을 최적화해야 할 것"이라면, 두 번째 생각은 "먼저 그것을 프로파일 링 할 것"입니다.
John L

4
당신이 겪고있는 지식의 종류가 도움이 되겠지만 이것은 여전히 ​​좋은 질문이지만, 가능한 한 적은 최적화를 시도함으로써 실제로 더 나은 서비스를 제공한다고 생각 합니다. 당신이 무엇을 의미하는지, 그리고 그것을 분명하게하는 경우에만 당신이 할 필요가 쓰기 다음 성능을 위해 코드가 덜 간단 만들기에 대해 생각합니다. 코드를보고 "자주 실행되는 코드를 최적화해야 할 것"이라고 생각하기보다는 너무 느리게 실행되는 코드를 관찰 할 때 "자주 실행되는 것을 찾아서 최적화해야한다"고 생각해야합니다. .
Ben

14
나는 그 부분이“프로필하기”를 권유 할 것을 완전히 기대했다. :). 그러나 동전의 다른 측면은 프로파일 링을하고 느리면 다시 작성하거나 여전히 높은 수준의 양식으로 조정할 수는 있지만 GHC는 수동으로 최적화하는 대신 더 잘 최적화 할 수 있습니까? 같은 종류의 지식이 필요합니다. 그리고 처음에 그 지식을 가지고 있다면 편집 프로파일 사이클을 저축 할 수있었습니다.
glaebhoerl

답변:


110

이 GHC Trac 페이지 는 패스를 상당히 잘 설명합니다. 이 페이지 는 Trac Wiki의 대부분과 마찬가지로 최적화 순서를 설명합니다.

구체적으로, 가장 좋은 방법은 특정 프로그램이 어떻게 컴파일되는지 보는 것입니다. 어떤 최적화가 수행되고 있는지 확인하는 가장 좋은 방법은 -v플래그를 사용하여 프로그램을 자세하게 컴파일하는 것 입니다. 내 컴퓨터에서 찾을 수있는 첫 번째 Haskell 조각을 예로 들어 보자.

Glasgow Haskell Compiler, Version 7.4.2, stage 2 booted by GHC version 7.4.1
Using binary package database: /usr/lib/ghc-7.4.2/package.conf.d/package.cache
wired-in package ghc-prim mapped to ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7
wired-in package integer-gmp mapped to integer-gmp-0.4.0.0-af3a28fdc4138858e0c7c5ecc2a64f43
wired-in package base mapped to base-4.5.1.0-6e4c9bdc36eeb9121f27ccbbcb62e3f3
wired-in package rts mapped to builtin_rts
wired-in package template-haskell mapped to template-haskell-2.7.0.0-2bd128e15c2d50997ec26a1eaf8b23bf
wired-in package dph-seq not found.
wired-in package dph-par not found.
Hsc static flags: -static
*** Chasing dependencies:
Chasing modules from: *SleepSort.hs
Stable obj: [Main]
Stable BCO: []
Ready for upsweep
  [NONREC
      ModSummary {
         ms_hs_date = Tue Oct 18 22:22:11 CDT 2011
         ms_mod = main:Main,
         ms_textual_imps = [import (implicit) Prelude, import Control.Monad,
                            import Control.Concurrent, import System.Environment]
         ms_srcimps = []
      }]
*** Deleting temp files:
Deleting: 
compile: input file SleepSort.hs
Created temporary directory: /tmp/ghc4784_0
*** Checking old interface for main:Main:
[1 of 1] Compiling Main             ( SleepSort.hs, SleepSort.o )
*** Parser:
*** Renamer/typechecker:
*** Desugar:
Result size of Desugar (after optimization) = 79
*** Simplifier:
Result size of Simplifier iteration=1 = 87
Result size of Simplifier iteration=2 = 93
Result size of Simplifier iteration=3 = 83
Result size of Simplifier = 83
*** Specialise:
Result size of Specialise = 83
*** Float out(FOS {Lam = Just 0, Consts = True, PAPs = False}):
Result size of Float out(FOS {Lam = Just 0,
                              Consts = True,
                              PAPs = False}) = 95
*** Float inwards:
Result size of Float inwards = 95
*** Simplifier:
Result size of Simplifier iteration=1 = 253
Result size of Simplifier iteration=2 = 229
Result size of Simplifier = 229
*** Simplifier:
Result size of Simplifier iteration=1 = 218
Result size of Simplifier = 218
*** Simplifier:
Result size of Simplifier iteration=1 = 283
Result size of Simplifier iteration=2 = 226
Result size of Simplifier iteration=3 = 202
Result size of Simplifier = 202
*** Demand analysis:
Result size of Demand analysis = 202
*** Worker Wrapper binds:
Result size of Worker Wrapper binds = 202
*** Simplifier:
Result size of Simplifier = 202
*** Float out(FOS {Lam = Just 0, Consts = True, PAPs = True}):
Result size of Float out(FOS {Lam = Just 0,
                              Consts = True,
                              PAPs = True}) = 210
*** Common sub-expression:
Result size of Common sub-expression = 210
*** Float inwards:
Result size of Float inwards = 210
*** Liberate case:
Result size of Liberate case = 210
*** Simplifier:
Result size of Simplifier iteration=1 = 206
Result size of Simplifier = 206
*** SpecConstr:
Result size of SpecConstr = 206
*** Simplifier:
Result size of Simplifier = 206
*** Tidy Core:
Result size of Tidy Core = 206
writeBinIface: 4 Names
writeBinIface: 28 dict entries
*** CorePrep:
Result size of CorePrep = 224
*** Stg2Stg:
*** CodeGen:
*** CodeOutput:
*** Assembler:
'/usr/bin/gcc' '-fno-stack-protector' '-Wl,--hash-size=31' '-Wl,--reduce-memory-overheads' '-I.' '-c' '/tmp/ghc4784_0/ghc4784_0.s' '-o' 'SleepSort.o'
Upsweep completely successful.
*** Deleting temp files:
Deleting: /tmp/ghc4784_0/ghc4784_0.c /tmp/ghc4784_0/ghc4784_0.s
Warning: deleting non-existent /tmp/ghc4784_0/ghc4784_0.c
link: linkables are ...
LinkableM (Sat Sep 29 20:21:02 CDT 2012) main:Main
   [DotO SleepSort.o]
Linking SleepSort ...
*** C Compiler:
'/usr/bin/gcc' '-fno-stack-protector' '-Wl,--hash-size=31' '-Wl,--reduce-memory-overheads' '-c' '/tmp/ghc4784_0/ghc4784_0.c' '-o' '/tmp/ghc4784_0/ghc4784_0.o' '-DTABLES_NEXT_TO_CODE' '-I/usr/lib/ghc-7.4.2/include'
*** C Compiler:
'/usr/bin/gcc' '-fno-stack-protector' '-Wl,--hash-size=31' '-Wl,--reduce-memory-overheads' '-c' '/tmp/ghc4784_0/ghc4784_0.s' '-o' '/tmp/ghc4784_0/ghc4784_1.o' '-DTABLES_NEXT_TO_CODE' '-I/usr/lib/ghc-7.4.2/include'
*** Linker:
'/usr/bin/gcc' '-fno-stack-protector' '-Wl,--hash-size=31' '-Wl,--reduce-memory-overheads' '-o' 'SleepSort' 'SleepSort.o' '-L/usr/lib/ghc-7.4.2/base-4.5.1.0' '-L/usr/lib/ghc-7.4.2/integer-gmp-0.4.0.0' '-L/usr/lib/ghc-7.4.2/ghc-prim-0.2.0.0' '-L/usr/lib/ghc-7.4.2' '/tmp/ghc4784_0/ghc4784_0.o' '/tmp/ghc4784_0/ghc4784_1.o' '-lHSbase-4.5.1.0' '-lHSinteger-gmp-0.4.0.0' '-lgmp' '-lHSghc-prim-0.2.0.0' '-lHSrts' '-lm' '-lrt' '-ldl' '-u' 'ghczmprim_GHCziTypes_Izh_static_info' '-u' 'ghczmprim_GHCziTypes_Czh_static_info' '-u' 'ghczmprim_GHCziTypes_Fzh_static_info' '-u' 'ghczmprim_GHCziTypes_Dzh_static_info' '-u' 'base_GHCziPtr_Ptr_static_info' '-u' 'base_GHCziWord_Wzh_static_info' '-u' 'base_GHCziInt_I8zh_static_info' '-u' 'base_GHCziInt_I16zh_static_info' '-u' 'base_GHCziInt_I32zh_static_info' '-u' 'base_GHCziInt_I64zh_static_info' '-u' 'base_GHCziWord_W8zh_static_info' '-u' 'base_GHCziWord_W16zh_static_info' '-u' 'base_GHCziWord_W32zh_static_info' '-u' 'base_GHCziWord_W64zh_static_info' '-u' 'base_GHCziStable_StablePtr_static_info' '-u' 'ghczmprim_GHCziTypes_Izh_con_info' '-u' 'ghczmprim_GHCziTypes_Czh_con_info' '-u' 'ghczmprim_GHCziTypes_Fzh_con_info' '-u' 'ghczmprim_GHCziTypes_Dzh_con_info' '-u' 'base_GHCziPtr_Ptr_con_info' '-u' 'base_GHCziPtr_FunPtr_con_info' '-u' 'base_GHCziStable_StablePtr_con_info' '-u' 'ghczmprim_GHCziTypes_False_closure' '-u' 'ghczmprim_GHCziTypes_True_closure' '-u' 'base_GHCziPack_unpackCString_closure' '-u' 'base_GHCziIOziException_stackOverflow_closure' '-u' 'base_GHCziIOziException_heapOverflow_closure' '-u' 'base_ControlziExceptionziBase_nonTermination_closure' '-u' 'base_GHCziIOziException_blockedIndefinitelyOnMVar_closure' '-u' 'base_GHCziIOziException_blockedIndefinitelyOnSTM_closure' '-u' 'base_ControlziExceptionziBase_nestedAtomically_closure' '-u' 'base_GHCziWeak_runFinalizzerBatch_closure' '-u' 'base_GHCziTopHandler_flushStdHandles_closure' '-u' 'base_GHCziTopHandler_runIO_closure' '-u' 'base_GHCziTopHandler_runNonIO_closure' '-u' 'base_GHCziConcziIO_ensureIOManagerIsRunning_closure' '-u' 'base_GHCziConcziSync_runSparks_closure' '-u' 'base_GHCziConcziSignal_runHandlers_closure'
link: done
*** Deleting temp files:
Deleting: /tmp/ghc4784_0/ghc4784_1.o /tmp/ghc4784_0/ghc4784_0.s /tmp/ghc4784_0/ghc4784_0.o /tmp/ghc4784_0/ghc4784_0.c
*** Deleting temp dirs:
Deleting: /tmp/ghc4784_0

*** Simplifier:모든 최적화 단계가 발생 하는 처음 부터 끝까지 살펴보면 꽤 많이 볼 수 있습니다.

우선 Simplifier는 거의 모든 단계에서 실행됩니다. 이렇게하면 많은 패스를 훨씬 쉽게 작성할 수 있습니다. 예를 들어 많은 최적화를 구현할 때 변경 사항을 수동으로 수행하는 대신 전파하기 위해 다시 쓰기 규칙을 작성하기 만하면됩니다. 단순화 기는 인라인 및 퓨전을 포함한 여러 간단한 최적화를 포함합니다. 내가 아는 주요 제한 사항은 GHC가 재귀 함수를 인라인하지 않고 융합이 작동하도록 이름을 올바르게 지정해야한다는 것입니다.

다음으로 수행 된 모든 최적화의 전체 목록이 표시됩니다.

  • 전문화

    전문화의 기본 아이디어는 함수가 호출되는 위치를 식별하고 다형성이 아닌 함수의 버전을 작성하여 다형성 및 과부하를 제거하는 것입니다. 이들은 호출되는 유형에 따라 다릅니다. 컴파일러에게 SPECIALISEpragma를 사용하여이를 수행하도록 지시 할 수도 있습니다 . 예를 들어, 계승 함수를 수행하십시오.

    fac :: (Num a, Eq a) => a -> a
    fac 0 = 1
    fac n = n * fac (n - 1)

    컴파일러는 사용할 곱셈의 속성을 알지 못하므로이를 최적화 할 수 없습니다. 그러나에 사용되는 것으로 확인 Int되면 이제 유형 만 다른 새 버전을 만들 수 있습니다.

    fac_Int :: Int -> Int
    fac_Int 0 = 1
    fac_Int n = n * fac_Int (n - 1)

    다음으로, 아래 언급 된 규칙이 실행될 수 있으며 Int, 원래보다 훨씬 빠른 unboxed에서 작동하는 것으로 끝납니다 . 전문화를 보는 또 다른 방법은 유형 클래스 사전 및 유형 변수에 대한 부분 적용입니다.

    소스는 여기에서 노트의 부하를 가지고있다.

  • 플로트 아웃

    편집 : 나는 분명히 이것을 전에 오해했습니다. 내 설명이 완전히 바뀌 었습니다.

    이것의 기본 아이디어는 함수에서 반복해서는 안되는 계산을 옮기는 것입니다. 예를 들어, 우리가 이것을 가지고 있다고 가정하십시오 :

    \x -> let y = expensive in x+y

    위의 람다에서는 함수가 호출 될 때마다 y다시 계산됩니다. 떠 다니는 더 나은 기능은

    let y = expensive in \x -> x+y

    프로세스를 용이하게하기 위해 다른 변환이 적용될 수있다. 예를 들어, 다음이 발생합니다.

     \x -> x + f 2
     \x -> x + let f_2 = f 2 in f_2
     \x -> let f_2 = f 2 in x + f_2
     let f_2 = f 2 in \x -> x + f_2

    다시 반복 계산이 저장됩니다.

    소스는 이 경우에 매우 읽을 수 있습니다.

    현재 인접한 두 람다 사이의 바인딩은 플로팅되지 않습니다. 예를 들어, 이것은 발생하지 않습니다 :

    \x y -> let t = x+x in ...

    가다

     \x -> let t = x+x in \y -> ...
  • 안쪽으로 뜬다

    소스 코드 인용

    주요 목적은 floatInwards케이스의 브랜치에 떠 다니는 것이므로 할당하지 않고 스택에 저장 한 다음 선택한 브랜치에서 필요하지 않은 것을 발견합니다.

    예를 들어 다음과 같은 표현이 있다고 가정합니다.

    let x = big in
        case v of
            True -> x + 1
            False -> 0

    v평가 되면 아마도 큰 썽크 인 False을 할당함으로써 x시간과 공간을 낭비한 것입니다. 안쪽으로 부동하면 이것을 수정하여 다음을 생성합니다.

    case v of
        True -> let x = big in x + 1
        False -> let x = big in 0

    을 사용한 단순화로 대체됩니다.

    case v of
        True -> big + 1
        False -> 0

    이 문서 는 다른 주제를 다루지 만 상당히 명확한 소개를 제공합니다. 이름에도 불구하고 부동 및 부동은 두 가지 이유로 무한 루프에 빠지지 않습니다.

    1. float in float는 case문장 으로 들어가고 float out은 함수를 처리합니다.
    2. 고정 된 순서의 패스가 있으므로 무한대로 번갈아 가면 안됩니다.

  • 수요 분석

    수요 분석 또는 엄격 성 분석은 변형이 적고 이름에서 알 수 있듯이 정보 수집 단계가 더 많습니다. 컴파일러는 항상 인수 (또는 적어도 일부)를 평가하는 함수를 찾고 필요에 따라 호출하는 대신 값별 호출을 사용하여 해당 인수를 전달합니다. 썽크의 오버 헤드를 피하기 때문에 종종 훨씬 빠릅니다. Haskell의 많은 성능 문제는이 패스 실패 또는 코드가 충분히 엄격하지 않아 발생합니다. 간단한 예는를 사용하는 것의 차이점입니다 foldr.foldlfoldl'정수 목록을 합산하려면-첫 번째는 스택 오버플로를 유발하고 두 번째는 힙 오버플로를 유발하며 마지막은 엄격함으로 인해 잘 실행됩니다. 이것은 아마도 가장 이해하기 쉽고 가장 잘 설명되어 있습니다. 나는 다형성과 CPS 코드가 종종 이것을 패배한다고 생각합니다.

  • 작업자 래퍼 바인딩

    작업자 / 래퍼 변환의 기본 아이디어는 간단한 구조에서 긴 루프를 수행하여 끝에서 해당 구조로 변환하는 것입니다. 예를 들어,이 함수를 사용하면 숫자의 계승을 계산할 수 있습니다.

    factorial :: Int -> Int
    factorial 0 = 1
    factorial n = n * factorial (n - 1)

    IntGHC 에서 정의를 사용하면

    factorial :: Int -> Int
    factorial (I# 0#) = I# 1#
    factorial (I# n#) = I# (n# *# case factorial (I# (n# -# 1#)) of
        I# down# -> down#)

    코드가 I#s에서 어떻게 다루어 졌는지 주목하십시오 . 다음을 수행하여 제거 할 수 있습니다.

    factorial :: Int -> Int
    factorial (I# n#) = I# (factorial# n#)
    
    factorial# :: Int# -> Int#
    factorial# 0# = 1#
    factorial# n# = n# *# factorial# (n# -# 1#)

    이 특정 예제는 SpecConstr에 의해 수행 될 수도 있지만 작업자 / 래퍼 변환은 수행 할 수있는 작업에서 매우 일반적입니다.

  • 공통 하위 표현식

    이것은 엄격도 분석과 같이 매우 효과적인 또 다른 간단한 최적화입니다. 기본 개념은 동일한 두 개의 표현식이있는 경우 동일한 값을 갖게된다는 것입니다. 예를 들어 fib피보나치 수 계산기 인 경우 CSE는

    fib x + fib x

    으로

    let fib_x = fib x in fib_x + fib_x

    계산을 반으로 줄입니다. 불행히도, 이것은 때때로 다른 최적화를 방해 할 수 있습니다. 또 다른 문제는 두 표현이 같은 장소에 있어야하며 구문 적으로 동일해야하며 값이 같지 않아야한다는 것입니다. 예를 들어 CSE는 많은 인라인없이 다음 코드에서 실행되지 않습니다.

    x = (1 + (2 + 3)) + ((1 + 2) + 3)
    y = f x
    z = g (f x) y

    그러나 llvm을 통해 컴파일하는 경우 Global Value Numbering 패스로 인해이 중 일부가 결합 될 수 있습니다.

  • 해방 사건

    이것은 코드 폭발을 일으킬 수 있다는 사실 외에도 끔찍하게 문서화 된 변환 인 것 같습니다. 여기 내가 찾은 작은 문서의 재구성 된 (그리고 약간 다시 작성된) 버전이 있습니다.

    이 모듈은를 Core살펴보고 case자유 변수를 찾습니다 . case재귀 호출 경로에 비어있는 변수 가 있으면 재귀 호출이 펼치기로 바뀝니다. 예를 들어

    f = \ t -> case v of V a b -> a : f t

    내부 f가 교체됩니다. 만들다

    f = \ t -> case v of V a b -> a : (letrec f = \ t -> case v of V a b -> a : f t in f) t

    섀도 잉의 필요성에 유의하십시오. 단순화, 우리는 얻는다

    f = \ t -> case v of V a b -> a : (letrec f = \ t -> a : f t in f t)

    에서보다 프로젝션 할 필요없이 ainner 내부에서 자유 로워 지기 때문에 더 나은 코드 letrec입니다 v. 참고로이 상품의 자유 변수 를 다루는 SpecConstr 달리, 인수 알려진 형태이다.

    SpecConstr에 대한 자세한 내용은 아래를 참조하십시오.

  • SpecConstr-다음과 같은 프로그램을 변환합니다

    f (Left x) y = somthingComplicated1
    f (Right x) y = somethingComplicated2

    으로

    f_Left x y = somethingComplicated1
    f_Right x y = somethingComplicated2
    
    {-# INLINE f #-}
    f (Left x) = f_Left x
    f (Right x) = f_Right x

    확장 된 예로 다음과 같은 정의를 사용하십시오 last.

    last [] = error "last: empty list"
    last (x:[]) = x
    last (x:x2:xs) = last (x2:xs)

    우리는 먼저 그것을

    last_nil = error "last: empty list"
    last_cons x [] = x
    last_cons x (x2:xs) = last (x2:xs)
    
    {-# INLINE last #-}
    last [] = last_nil
    last (x : xs) = last_cons x xs

    다음으로 단순화가 실행되고

    last_nil = error "last: empty list"
    last_cons x [] = x
    last_cons x (x2:xs) = last_cons x2 xs
    
    {-# INLINE last #-}
    last [] = last_nil
    last (x : xs) = last_cons x xs

    우리는 목록의 앞면을 반복적으로 권투하고 unboxing하지 않기 때문에 프로그램이 더 빨라졌습니다. 또한 새로운보다 효율적인 정의를 실제로 사용할 수있을뿐만 아니라 재귀적인 정의를 향상시킬 수 있으므로 인라이닝이 중요합니다.

    SpecConstr은 여러 휴리스틱에 의해 제어됩니다. 논문에 언급 된 내용은 다음과 같습니다.

    1. 람다는 명백하고 arity는 a입니다.
    2. 오른쪽은 "충분히 작은"플래그로 제어되는 것입니다.
    3. 이 함수는 재귀 적이며 특수 호출이 오른쪽에 사용됩니다.
    4. 함수에 대한 모든 인수가 있습니다.
    5. 적어도 하나의 인수는 생성자 응용 프로그램입니다.
    6. 이 주장은 함수 어딘가에서 사례 분석됩니다.

    그러나 휴리스틱은 거의 확실하게 변경되었습니다. 실제로, 논문은 대안적인 여섯 번째 휴리스틱을 언급합니다 :

    인수에 전문 x경우에만 x됩니다 a로 자세히 조사 case하고, 일반 함수에 전달되지 않았거나 결과의 일부로 반환.

이것은 매우 작은 파일 (12 줄)이므로 많은 최적화를 유발하지는 않았을 것입니다. 또한 왜 패스를 선택했는지 그리고 왜 순서대로 넣었는지 알려주지 않습니다.


이제 우리는 어딘가에 도착하고 있습니다! 의견 : 전문 분야에 대한 문장이 잘린 것 같습니다. 플로트 아웃의 요점을 보지 못했습니다. 무엇을위한 것입니까? 플로트 또는 플로트 여부를 어떻게 결정합니까 (루프에 들어 가지 않습니까)? 나는 어딘가에서 GHC가 CSE 를 전혀 하지 않았다는 인상을 받았지만 분명히 잘못되었습니다. 큰 그림을 보지 않고 세부 사항을 잃어버린 것 같은 느낌이 듭니다 ... 주제가 생각보다 훨씬 복잡합니다. 어쩌면 내 질문은 불가능하며 많은 경험이나 GHC 작업을 제외 하고는이 직감을 얻을 수있는 방법이 없습니까?
glaebhoerl

글쎄, 난 잘 모르겠지만, 당신이 얻을 수 있어야합니다, 그래서 나는, GHC에서 일한 적이 몇 가지 직관을.
gereeter

언급 한 문제를 해결했습니다.
gereeter

1
또한 큰 그림에 대해서는 실제로 하나도 없다는 의견입니다. 어떤 최적화가 수행 될지 추측하고 싶을 때 체크리스트로 내려갑니다. 그런 다음 각 패스가 어떻게 변경되는지 확인하기 위해 다시 수행합니다. 다시 한번. 본질적으로 컴파일러를 사용합니다. 내가 아는 유일한 최적화 체계는 실제로 "큰 그림"을 가지고있는 것입니다. 슈퍼 컴파일입니다.
gereeter

1
"융합이 작동하려면 정확히 이름을 붙여야한다"는 것은 무엇을 의미합니까?
Vincent Beffara

65

게으름

"컴파일러 최적화"는 아니지만 언어 사양에 의해 보장되는 것이므로 언제든지 발생할 수 있습니다. 기본적으로 이는 결과에 "무언가를"할 때까지 작업이 수행되지 않음을 의미합니다. 게으름을 고의로 끄는 데 여러 가지 중 하나를 수행하지 않는 한.

이것은 분명히 그 자체로 전체 주제이며, 이에 대해 이미 많은 질문과 답변이 있습니다.

제한된 경험으로, 코드를 너무 게 으르거나 너무 엄격 하게 만들면 내가 이야기하려는 다른 것보다 훨씬 큰 성능 페널티 (시간 공간)가 있습니다 ...

엄격 성 분석

게으름은 필요하지 않은 한 일을 피하는 것입니다. 컴파일러가 주어진 결과가 "항상 필요하다"고 판단하면 계산을 저장하고 나중에 수행하지 않아도됩니다. 더 효율적이기 때문에 직접 수행합니다. 이것을 소위 "엄격 성 분석"이라고합니다.

분명한 것은 컴파일러가 무언가를 엄격하게 할 수있는시기를 항상 감지 할 수 없다는 것입니다. 때때로 컴파일러에게 작은 힌트를 주어야합니다. (핵심 출력을 넘어가는 것 외에 엄격 성 분석이 생각한 것을 수행했는지 여부를 쉽게 확인할 수있는 방법을 모르겠습니다.)

인라인

함수를 호출하고 컴파일러가 호출중인 함수를 알 수있는 경우 해당 함수를 "인라인"하려고 시도 할 수 있습니다. 즉, 함수 호출을 함수 자체의 복사본으로 대체하려고합니다. 함수 호출의 오버 헤드는 일반적으로 매우 작지만 인라인을 사용하면 그렇지 않은 다른 최적화가 발생할 수 있으므로 인라인이 큰 승리가 될 수 있습니다.

함수는 "충분히 작"거나 인라인을 요구하는 pragma를 추가 한 경우에만 인라인됩니다. 또한 컴파일러가 호출하는 함수를 컴파일러가 알 수있는 경우에만 함수를 인라인 할 수 있습니다. 컴파일러가 알 수없는 두 가지 주요 방법이 있습니다.

  • 호출하는 함수가 다른 곳에서 전달 된 경우. 예를 들어, filter함수가 컴파일되면 사용자가 제공 한 인수이므로 필터 술어를 인라인 할 수 없습니다.

  • 호출하는 함수가 클래스 메소드 이고 컴파일러가 어떤 유형을 포함하는지 알 수없는 경우 예를 들어, sum함수가 컴파일 될 때 컴파일러는 +함수를 인라인 할 수 없습니다 sum. 각각의 +함수 가 다른 여러 가지 숫자 유형으로 작동 하기 때문 입니다.

후자의 경우, {-# SPECIALIZE #-}pragma를 사용하여 특정 유형으로 하드 코딩 된 버전의 함수를 생성 할 수 있습니다 . 예를 들어, 유형 {-# SPECIALIZE sum :: [Int] -> Int #-}에 맞게 sum하드 코딩 된 버전을 컴파일하면 이 버전에서 인라인 될 수 있습니다.Int+

그러나 새로운 특수 sum함수는 컴파일러가 작업 중임을 알 수있는 경우에만 호출됩니다 Int. 그렇지 않으면 원래의 다형성 sum이 호출됩니다. 다시 말하지만 실제 함수 호출 오버 헤드는 상당히 작습니다. 인라인이 도움이 될 수있는 추가적인 최적화입니다.

공통 하위 식 제거

특정 코드 블록이 동일한 값을 두 번 계산하면 컴파일러는이 값을 동일한 계산의 단일 인스턴스로 바꿀 수 있습니다. 예를 들어

(sum xs + 1) / (sum xs + 2)

컴파일러는 이것을 최적화 할 수 있습니다.

let s = sum xs in (s+1)/(s+2)

컴파일러가 항상이 작업을 수행 할 것으로 예상 할 수 있습니다 . 그러나 분명히 어떤 상황에서는 성능이 좋지 않을 수 있지만 GHC가 항상 그렇게 하지는 않습니다 . 솔직히, 나는 이것의 세부 사항을 실제로 이해하지 못합니다. 그러나 결론은이 변환이 중요한 경우 수동으로 수행하는 것이 어렵지 않다는 것입니다. (그리고 중요하지 않다면 왜 걱정하고 있습니까?)

사례 표현

다음을 고려하세요:

foo (0:_ ) = "zero"
foo (1:_ ) = "one"
foo (_:xs) = foo xs
foo (  []) = "end"

처음 세 방정식은 모두 목록이 비어 있지 않은지 여부를 확인합니다 (다른 것들 중에서). 그러나 같은 것을 세 번 확인하는 것은 낭비입니다. 다행스럽게도 컴파일러는이를 여러 개의 중첩 된 케이스 표현식으로 최적화하는 것이 매우 쉽습니다. 이 경우에는

foo xs =
  case xs of
    y:ys ->
      case y of
        0 -> "zero"
        1 -> "one"
        _ -> foo ys
    []   -> "end"

이것은 다소 직관적이지 않지만 더 효율적입니다. 컴파일러는이 변환을 쉽게 수행 할 수 있으므로 걱정할 필요가 없습니다. 가능한 가장 직관적 인 방식으로 패턴 일치를 작성하십시오. 컴파일러는 이것을 재정렬하고 재정렬하는 데 매우 능숙하여 가능한 빨리 작성합니다.

퓨전

리스트 처리를위한 표준 Haskell 관용구는 하나의리스트를 가져 와서 새로운리스트를 생성하는 함수들을 서로 연결하는 것입니다. 정식 예는

map g . map f

불행히도 게으름은 불필요한 작업을 건너 뛰는 것을 보장하지만 중간 목록 수액 성능에 대한 모든 할당 및 할당 해제입니다. "Fusion"또는 "Deforestation"은 컴파일러가 이러한 중간 단계를 제거하려고하는 곳입니다.

문제는 이러한 기능의 대부분이 재귀 적이라는 것입니다. 재귀가 없으면 모든 함수를 하나의 큰 코드 블록으로 뭉개고 단순화기를 실행하고 중간 목록없이 실제로 최적의 코드를 생성하는 것이 기본 연습입니다. 그러나 재귀 때문에 작동하지 않습니다.

{-# RULE #-}pragma를 사용 하여이 중 일부를 수정할 수 있습니다 . 예를 들어

{-# RULES "map/map" forall f g xs. map f (map g xs) = map (f.g) xs #-}

이제 GHC가에 map적용되는 것을 볼 때마다 map목록을 한 번에 통과시켜 중간 목록을 제거합니다.

문제는 map다음에 대해서만 작동합니다 map. - 다른 많은 가능성이있다 map다음에 filter, filter다음 map등 오히려 "스트림 융합"소위 그들 각각에 대한 해결책이 발명 한 손으로 코드를보다가. 이것은 더 복잡한 트릭이므로 여기서는 설명하지 않습니다.

그것의 길고 짧은 : 이것은 프로그래머가 작성한 특별한 최적화 트릭입니다 . GHC 자체는 융합에 대해 아무것도 모른다. 모두 목록 라이브러리 및 기타 컨테이너 라이브러리에 있습니다. 따라서 최적화 작업은 컨테이너 라이브러리를 작성하는 방법 (또는보다 현실적으로 사용하려는 라이브러리)에 따라 다릅니다.

예를 들어, Haskell '98 어레이로 작업하는 경우 어떤 종류의 융합도 기대하지 마십시오. 그러나 vector라이브러리에는 광범위한 융합 기능이 있다는 것을 알고 있습니다. 라이브러리에 관한 모든 것입니다. 컴파일러는 단지 RULESpragma를 제공합니다 . (어쨌든 매우 강력합니다. 라이브러리 작성자는 클라이언트 코드를 다시 작성하는 데 사용할 수 있습니다!)


메타 :

  • 나는 사람들이 "코드 우선, 프로파일 두 번째, 세 번째 최적화"라는 말에 동의합니다.

  • 또한 사람들에게 "주어진 설계 결정에 얼마의 비용이 드는지에 대한 정신적 모델을 갖는 것이 유용하다"는 의견에 동의합니다.

모든 것의 균형과 그 모든 것 ...


9
it's something guaranteed by the language specification ... work is not performed until you "do something" with the result.- 정확히. 언어 사양 은 엄격하지 않은 의미론을 약속합니다 . 불필요한 작업 수행 여부에 대해서는 약속하지 않습니다.
Dan Burton

1
@DanBurton 물론입니다. 그러나 그것은 몇 문장으로 설명하기가 쉽지 않습니다. 게다가, GHC가 현존하는 유일한 Haskell 구현이기 때문에 GHC가 게으르다는 사실은 대부분의 사람들에게 충분합니다.
MathematicalOrchid

@MathematicalOrchid : 투기 평가는 흥미로운 반례이지만 초보자에게는 너무 많을 것입니다.
Ben Millwood

5
CSE 소개 : 제 생각에는 원치 않는 공유가 생길 수 있고 따라서 공간 누출이 발생할 수 있기 때문에 거의 수행되지 않습니다.
Joachim Breitner

2
(a) 지금 응답하지 않고 (b) 귀하의 답변을 수락하지 않아서 죄송합니다. 길고 인상적이지만 내가 원하는 영역을 다루지 않습니다. 내가 원하는 것은 람다 / 렛 / 케이스 플로팅, 유형 / 생성자 / 함수 인수 전문화, 엄격 성 분석 및 언 박싱 (당신이 언급 한), 작업자 / 래퍼 및 GHC 가하는 다른 모든 것과 같은 하위 수준 변환 목록입니다. 입력 및 출력 코드에 대한 설명과 예, 그리고 조합 된 효과와 변형 발생 하지 않는 효과의 예가 이상적 입니다. 현상금을 만들어야한다고 생각합니까?
glaebhoerl

8

let 바인딩 v = rhs가 한 곳에서만 사용되는 경우 rhs가 큰 경우에도 컴파일러에서 인라인하도록 계산할 수 있습니다.

예외는 (현재 질문의 맥락에서 거의 그렇지 않은) 작업 복제를 위험에 빠뜨리는 람다입니다. 치다:

let v = rhs
    l = \x-> v + x
in map l [1..100]

v를 인라이닝하는 것은 위험 할 것이다. 왜냐하면 하나의 (구문 적) 사용은 99 개의 추가 rhs 평가로 해석되기 때문이다. 그러나이 경우 수동으로 인라인하지 않을 것입니다. 따라서 기본적으로 규칙을 사용할 수 있습니다.

한 번만 나타나는 이름을 인라이닝하려는 경우 컴파일러는 어쨌든 그렇게합니다.

행복한 추론으로, 긴 문장을 분해하기 위해 let 바인딩을 사용하는 것은 (명확성을 얻기 위해) 본질적으로 무료입니다.

이것은 인라인에 대한 더 많은 정보를 포함하는 community.haskell.org/~simonmar/papers/inline.pdf에서 온 것입니다.

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