GHC'nin hangi optimizasyonların güvenilir bir şekilde çalışması beklenebilir?


183

GHC'nin gerçekleştirebileceği birçok optimizasyon var, ancak hepsinin ne olduğunu, ne kadar gerçekleştirileceğini ve hangi koşullarda olacağını bilmiyorum.

Sorum şu: her seferinde hangi dönüşümlerin uygulanmasını bekleyebilirim, ya da neredeyse böyle? Sık sık çalıştırılacak (değerlendirilecek) bir kod parçasına bakarsam ve ilk düşüncem "hmm, belki bunu optimize etmeliyim", hangi durumlarda ikinci düşüncem olmalı, "bunu düşünme bile, GHC bunu aldı "?

Gazeteyi okurken All Nothing olarak Akışları için listeler: Stream Füzyon ve GHC normal optimizasyonlar sonra güvenilir bir şekilde basit döngüler içine aşağı optimize olacaktır farklı bir forma liste işleme yeniden yazmak kullanılan tekniktir bana roman oldu. Kendi programlarımın bu tür bir optimizasyon için ne zaman uygun olduğunu nasıl anlayabilirim?

GHC kılavuzunda bazı bilgiler var , ancak bu sadece soruyu cevaplamanın yolunun bir parçası.

EDIT: Ben bir ödül başlıyorum. Ne istiyorum lambda / let / case-kayan, tip / yapıcı / fonksiyon argümanı uzmanlık, katılık analizi ve kutudan çıkarma, işçi / sarıcı ve dışında bıraktığım önemli GHC ne yapar gibi düşük seviyeli dönüşümlerin bir listesi , giriş ve çıkış kodunun açıklamaları ve örnekleri ve ideal olarak toplam etkinin parçalarının toplamından fazla olduğu durumların resimlemeleri. İdeal olarak, dönüşümlerin ne zaman yapılmayacağından bahsedilmesiolmak. Büyük resim olduğu sürece, her dönüşümün yeni uzunlukta açıklamalarını beklemiyorum, birkaç cümle ve satır içi tek satırlık kod örnekleri yeterli olabilir (veya bilimsel kağıda yirmi sayfalık bir bağlantı varsa). sonuna kadar net. Bir kod parçasına bakmak ve sıkı bir döngü aşağı derlemek ya da neden olmasın ya da bunu yapmak için değiştirmek zorunda kalacağım hakkında iyi bir tahmin yapabilmek istiyorum. (Burada, akış füzyonu gibi büyük optimizasyon çerçevelerine çok fazla ilgi duymuyorum (sadece bu konuda bir makale okudum); daha fazla bu çerçeveleri yazan insanların sahip olduğu bilgi türünde .)


10
Bu çok değerli bir soru. Değerli bir cevap yazmak zor.
Matematiksel

1
Gerçekten iyi bir başlangıç ​​noktası: aosabook.org/tr/ghc.html
Gabriel Gonzalez

7
Herhangi bir dilde, ilk düşünceniz "belki de bunu optimize etmeliyim" ise, ikinci düşünceniz "önce profil yapacağım" olmalıdır.
John L

4
Arkanızdaki bilgi türü yardımcı olsa da ve bu hala iyi bir soru olsa da, mümkün olduğunca az optimizasyon yapmaya çalışarak gerçekten daha iyi hizmet verdiğinizi düşünüyorum . Ne demek istediğini, ve onu görünür hale geçtiğinde gerektiğini yazın ardından performans uğruna kod az anlaşılır hale düşünün. Koda bakmak ve "bu sık sık çalıştırılacak, belki de onu optimize etmeliyim" diye düşünmektense, yalnızca kodun çok yavaş çalıştığını gözlemlediğinizde "Sık sık ne yapıldığını bulup optimize etmeliyim" diye düşünmelisiniz. .
Ben

14
Tamamen bu kısmın "profilli!" :). Ama madalyonun diğer tarafı, eğer profil edersem ve yavaş olursa, belki de yeniden yüksek seviyede olan bir forma yeniden yazabilir veya değiştirebilirim ama GHC kendimi elle optimize etmek yerine daha iyi optimize edebilir mi? Bu da aynı bilgi türünü gerektirir. Ve eğer ilk etapta bu bilgiye sahip olsaydım kendime bir düzenleme profili döngüsü kaydedebilirdim.
glaebhoerl

Yanıtlar:


110

Bu GHC Trac sayfası da geçişleri oldukça iyi açıklıyor. Bu sayfa optimizasyon sırasını açıklamaktadır, ancak Trac Wiki'nin çoğu gibi güncelliğini yitirmiştir.

Özellikler için yapılacak en iyi şey muhtemelen belirli bir programın nasıl derlendiğine bakmaktır. Hangi optimizasyonların yapıldığını görmenin en iyi yolu, -vbayrağı kullanarak programı ayrıntılı olarak derlemektir . Bilgisayarımda bulabildiğim ilk Haskell parçasını örnek olarak alarak:

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:Tüm optimizasyon aşamalarının gerçekleştiği ilkinden sonuncuya baktığımızda, oldukça fazla görüyoruz.

Her şeyden önce, Sadeleştirici neredeyse tüm aşamalar arasında çalışır. Bu, birçok geçişi yazmayı çok daha kolay hale getirir. Örneğin, birçok optimizasyonu uygularken, değişiklikleri manuel olarak yapmak yerine yaymak için yeniden yazma kuralları oluştururlar. Basitleştirici, satır içi ve füzyon dahil olmak üzere bir dizi basit optimizasyonu kapsar. Bunun bildiğim temel kısıtlaması, GHC'nin satır içi özyinelemeli işlevleri reddettiğini ve füzyonun çalışması için işlerin doğru şekilde adlandırılması gerektiğidir.

Ardından, gerçekleştirilen tüm optimizasyonların tam bir listesini görüyoruz:

  • Uzmanlaşmak

    Temel uzmanlık fikri, işlevin çağrıldığı yerleri belirleyerek ve polimorfik olmayan işlevin sürümlerini oluşturarak polimorfizmi ve aşırı yüklenmeyi kaldırmaktır. Derleyiciye bunu SPECIALISEpragma ile yapmasını söyleyebilirsiniz . Örnek olarak, faktöriyel bir işlev alın:

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

    Derleyici, kullanılacak çarpmanın herhangi bir özelliğini bilmediğinden, bunu hiç optimize edemez. Ancak, bir üzerinde kullanıldığını görürse Int, şimdi yalnızca türünde farklılık gösteren yeni bir sürüm oluşturabilir:

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

    Daha sonra, aşağıda belirtilen kurallar tetiklenebilir Intve orijinal kutusundan çok daha hızlı olan kutusuz s üzerinde çalışan bir şey elde edersiniz . Uzmanlığa bakmanın bir başka yolu da tür sınıfı sözlükleri ve tür değişkenleri üzerinde kısmi uygulamadır.

    Buradaki kaynakta bir sürü not var.

  • Yüzmek

    EDIT: Görünüşe göre bunu daha önce yanlış anladım. Açıklamam tamamen değişti.

    Bunun temel fikri, tekrarlanmaması gereken hesaplamaları fonksiyonların dışına taşımaktır. Örneğin, şunu yaşadığımızı varsayalım:

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

    Yukarıdaki lambda'da fonksiyon her çağrıldığında yyeniden hesaplanır. Dışarıda yüzen daha iyi bir fonksiyon

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

    Süreci kolaylaştırmak için başka dönüşümler uygulanabilir. Örneğin, bu olur:

     \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

    Yine, tekrarlanan hesaplama kaydedilir.

    Kaynak , bu durumda çok okunabilir.

    Şu anda iki bitişik lambda arasındaki bağlar yüzülmemektedir. Örneğin, bu gerçekleşmez:

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

    gidiyor

     \x -> let t = x+x in \y -> ...
  • İçeri doğru yüzer

    Kaynak koddan alıntı yaparak,

    Temel amacı, floatInwardsbir vakanın dallarına dalmaktır, böylece şeyleri tahsis etmiyoruz, onları yığına kaydetmiyoruz ve daha sonra seçilen dalda gerekli olmadıklarını keşfediyoruz.

    Örnek olarak, şu ifadeye sahip olduğumuzu varsayalım:

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

    Eğer vdeğerlendirir için False, daha sonra ayırarak xmuhtemelen bazı büyük parça var ki, biz boşa zaman ve mekanın var. İçeri doğru yüzer bunu düzelterek bunu üretir:

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

    daha sonra basitleştirici ile değiştirilir.

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

    Bu yazı , diğer konuları kapsamakla birlikte, oldukça açık bir giriş sunmaktadır. İsimlerine rağmen, yüzen ve yüzen iki nedenden dolayı sonsuz bir döngüye girmediğini unutmayın:

    1. Şamandıralarda yüzer caseifadelere izin verirken, şamandıra işlevlerle ilgilenir.
    2. Sabit bir geçiş sırası vardır, bu yüzden sonsuz bir şekilde değişmemelidirler.

  • Talep analizi

    Talep analizi veya doğruluk analizi, bir dönüşümden daha az ve adından da anlaşılacağı gibi bir bilgi toplama geçişinden daha fazlasıdır. Derleyici, argümanlarını (veya en azından bazılarını) her zaman değerlendiren işlevler bulur ve bu bağımsız değişkenleri, çağrı gereği yerine çağrı-değer kullanarak iletir. Thunkların genel giderlerinden kaçacağınız için, bu genellikle çok daha hızlıdır. Haskell'deki birçok performans sorunu ya bu geçişin başarısız olmasından ya da kodun yeterince katı olmamasından kaynaklanıyor. Basit bir örnek kullanarak arasındaki fark foldr, foldlvefoldl'bir tamsayı listesi toplamak için - birincisi yığın taşmasına neden olurken, ikincisi yığın taşmasına neden olur ve sonuncusu katılık nedeniyle iyi çalışır. Bu muhtemelen en kolay anlaşılan ve en iyi belgelendirilmiş olanıdır. Ben polimorfizm ve CPS kodu genellikle bu yenmek inanıyorum.

  • İşçi Sarıcı

    İşçi / sargı dönüşümünün temel fikri, basit bir yapı üzerinde uçlarda bu yapıya ve yapıdan dönüşerek sıkı bir döngü yapmaktır. Örneğin, bir sayının faktöriyelini hesaplayan bu işlevi ele alalım.

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

    IntGHC tanımını kullanarak,

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

    Kodun I#s ile nasıl kaplandığına dikkat edin ? Bunları yaparak bunları kaldırabiliriz:

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

    Bu özel örnek SpecConstr tarafından da yapılmış olsa da, çalışan / sarıcı dönüşümü yapabileceği şeylerde çok geneldir.

  • Ortak alt ifade

    Bu, katılık analizi gibi çok etkili olan başka bir basit optimizasyon. Temel fikir, aynı olan iki ifadeniz varsa, bunların aynı değere sahip olacağıdır. Örneğin fib, bir Fibonacci sayı hesaplayıcısı ise CSE

    fib x + fib x

    içine

    let fib_x = fib x in fib_x + fib_x

    hesaplamayı yarıya indirir. Ne yazık ki, bu bazen diğer optimizasyonların önüne geçebilir. Diğer bir problem, iki ifadenin aynı yerde olması ve sözdizimsel olarak aynı olması, değerle aynı olmamasıdır. Örneğin, CSE, bir grup satır içi satır olmadan aşağıdaki kodda tetiklenmez:

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

    Bununla birlikte, llvm yoluyla derlerseniz, Global Değer Numaralandırma geçişi nedeniyle bunların bir kısmını birleştirebilirsiniz.

  • Davayı kurtar

    Bu, kod patlamasına neden olabileceği gibi, çok belgelenmiş bir dönüşüm gibi görünüyor. İşte bulduğum küçük belgelerin yeniden biçimlendirilmiş (ve biraz yeniden yazılmış) bir versiyonu:

    Bu modül yürür Coreve caseserbest değişkenleri arar . Kriter şöyledir: caseözyinelemeli çağrıya giden yolda serbest bir değişken varsa, özyinelemeli çağrı bir katlama ile değiştirilir. Örneğin,

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

    fkısım değiştirilir. yapmak

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

    Gölgeleme ihtiyacına dikkat edin. Basitleştiriyoruz

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

    Bu daha iyi bir koddur, çünkü projeksiyona ihtiyaç duymak yerine aiç kısımda serbesttir . Not bununla bu fırsatlar serbest değişkenler ile ilgilenen SpecConstr, aksine argümanlar bilinen biçimdedir.letrecv

    SpecConstr hakkında daha fazla bilgi için aşağıya bakın.

  • SpecConstr - bu gibi programları dönüştürür

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

    içine

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

    Genişletilmiş bir örnek olarak, şu tanımı alın last:

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

    İlk önce

    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

    Sonra, basitleştirici çalışır ve

    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

    Listenin önünü tekrar tekrar boks ve kutudan çıkarmadığımız için programın daha hızlı olduğuna dikkat edin. Ayrıca, yeni, daha verimli tanımların gerçekte kullanılmasına izin verdiği ve özyinelemeli tanımları daha iyi hale getirdiği için, satırlamanın çok önemli olduğunu unutmayın.

    SpecConstr birkaç buluşsal yöntemle kontrol edilir. Makalede belirtilenler şöyledir:

    1. Lambdalar açık ve arity a.
    2. Sağ taraf bir bayrak tarafından kontrol edilen bir şey "yeterince küçük".
    3. İşlev özyinelemelidir ve özelleştirilebilir çağrı sağ tarafta kullanılır.
    4. İşlevin tüm argümanları mevcuttur.
    5. Argümanlardan en az biri bir yapıcı uygulamasıdır.
    6. Bu argüman fonksiyonun herhangi bir yerinde büyük / küçük harf analiz edilir.

    Bununla birlikte, buluşsal yöntemler neredeyse kesinlikle değişti. Aslında makale alternatif bir altıncı buluşsal yöntemden bahsediyor:

    Bağımsız değişken üzerinde uzmanlaşmak xyalnızca xedilir sadece bir tarafından incelenip case, ve sıradan bir işleve geçirilen değildir veya sonucunun bir parçası olarak döndü.

Bu çok küçük bir dosyaydı (12 satır) ve bu yüzden muhtemelen tüm optimizasyonları (ancak hepsini yaptığını düşünüyorum) tetiklemedi. Bu aynı zamanda neden bu geçişleri seçtiğini ve onları neden bu sıraya koyduğunu da söylemez.


Şimdi bir yere gidiyoruz! Yorumlar: Uzmanlık bölümünde kısmi bir cümleniz var gibi görünüyor. Kayan noktaya bakmıyorum: ne için? İçeri girip çıkmayacağına nasıl karar veriyor (neden bir döngüye girmiyor)? Ben GHC ÖAM'sini yapmadığı bir yere gelen izlenimi vardı hiç ama görünüşe göre yanlış olduğunu söyledi. Büyük bir resim görmek yerine ayrıntılarla kaybolmuş gibi hissediyorum ... konu düşündüğümden daha da karmaşık. Belki sorum imkansızdır ve bu sezgiyi kazanmanın bir ton deneyimi veya GHC üzerinde kendiniz çalışmak dışında bir yolu yoktur?
glaebhoerl

Bilmiyorum, ama GHC üzerinde hiç çalışmadım, bu yüzden biraz sezgi alabilmelisin .
gereeter

Bahsettiğiniz sorunları düzelttim.
gereeter

1
Ayrıca, büyük resim hakkında, bence gerçekten bir tane yok. Hangi optimizasyonların yapılacağını tahmin etmek istediğimde, bir kontrol listesine gidiyorum. Sonra her geçişin bir şeyleri nasıl değiştireceğini görmek için tekrar yaparım. Ve yeniden. Aslında derleyiciyi oynuyorum. Gerçekten "büyük bir resim" olduğunu bildiğim tek optimizasyon düzeni süper derleme olduğunu.
gereeter

1
"Füzyonun çalışması için işler doğru bir şekilde adlandırılmalıdır" ile ne demek istiyorsun?
Vincent Beffara

65

Tembellik

Bir "derleyici optimizasyonu" değildir, ancak dil spesifikasyonu tarafından garanti edilen bir şeydir, böylece her zaman gerçekleşmesine güvenebilirsiniz. Esasen, bu, sonuçla "bir şeyler yapana" kadar iş yapılmadığı anlamına gelir. (Tembelliği kasten kapatmak için birkaç şeyden birini yapmadığınız sürece.)

Bu, açıkçası, kendi başına bütün bir konudur ve SO'nun zaten bu konuda birçok soru ve cevabı vardır.

Naçizane deneyimlerime göre, kodunuz çok tembel ya da çok sıkı hale sahiptir ölçüde (zaman içinde daha büyük performans cezalar ve ben hakkında konuşmak üzere olduğum diğer şeyler hepsinden daha uzayda) ...

Sıkılık analizi

Tembellik gerekli olmadıkça işten kaçınmakla ilgilidir. Derleyici belirli bir sonucun "her zaman" gerekli olacağını belirleyebilirse, hesaplamayı saklayıp daha sonra gerçekleştirmeye zahmet etmez; sadece doğrudan gerçekleştirir, çünkü bu daha verimlidir. Buna "katılık analizi" denir.

Açıkçası, açıkçası, derleyici bir şeyin ne zaman katılabileceğini her zaman tespit edemez . Bazen derleyiciye küçük ipuçları vermeniz gerekir. (Sıkılık analizinin, Çekirdek çıktısı üzerinden geçmekten başka, ne düşündüğünü yapıp yapmadığını belirlemenin kolay bir yolunun farkında değilim.)

satır içine almak

Bir işlevi çağırırsanız ve derleyici hangi işlevi çağırdığınızı söyleyebilirse, bu işlevi "satır içi" yapmayı deneyebilir - yani işlev çağrısını işlevin kendisinin bir kopyasıyla değiştirmek. Bir işlev çağrısının yükü genellikle oldukça küçüktür, ancak satır içi çizgi genellikle başka türlü gerçekleşmeyecek olan diğer optimizasyonların gerçekleşmesini sağlar, bu nedenle satır içi işlem büyük bir kazanç olabilir.

İşlevler yalnızca "yeterince küçük" ise (veya özellikle satır içi sormayı gerektiren bir pragma eklerseniz) satır içine alınır. Ayrıca, işlevler yalnızca derleyici hangi işlevi çağırdığınızı söyleyebiliyorsa satır içine alınabilir. Derleyicinin söyleyememesinin iki ana yolu vardır:

  • Aradığınız işlev başka bir yerden iletilirse. Örneğin, filterişlev derlendiğinde, kullanıcı tarafından sağlanan bir argüman olduğu için filtre yüklemini satır içi yapamazsınız.

  • Aradığınız işlev bir sınıf yöntemiyse ve derleyici hangi türün dahil olduğunu bilmiyorsa. Örneğin, sumişlev derlendiğinde, derleyici +işlevi satır içine alamaz , çünkü sumher biri farklı bir +işleve sahip olan birkaç farklı sayı türüyle çalışır.

İkinci durumda, {-# SPECIALIZE #-}belirli bir türe sabit kodlanmış bir işlevin sürümlerini oluşturmak için pragmayı kullanabilirsiniz . Örneğin, tür için sabit kodlanmış {-# SPECIALIZE sum :: [Int] -> Int #-}bir sürümü derler , yani bu sürümde satır içine alınabilir.sumInt+

Bununla birlikte, yeni özel sumfonksiyonumuzun yalnızca derleyici ile birlikte çalıştığımızı söyleyebileceğini unutmayın Int. Aksi takdirde orijinal, polimorfik sumçağrılır. Yine, gerçek fonksiyon çağrısı yükü oldukça küçüktür. Satırlamanın sağlayabileceği ek optimizasyonlar faydalıdır.

Ortak alt ifade eleme

Belirli bir kod bloğu aynı değeri iki kez hesaplarsa, derleyici bunu aynı hesaplamanın tek bir örneğiyle değiştirebilir. Örneğin,

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

derleyici bunu optimize edebilir

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

Derleyicinin her zaman bunu yapmasını bekleyebilirsiniz . Ancak, görünüşe göre bazı durumlarda bu daha kötü performansa neden olabilir, daha iyi değil, bu yüzden GHC her zaman bunu yapmaz . Açıkçası, bunun arkasındaki detayları gerçekten anlamıyorum. Ancak sonuçta, eğer bu dönüşüm sizin için önemliyse, bunu manuel olarak yapmak zor değildir. (Ve eğer önemli değilse, neden endişeleniyorsun?)

Vaka ifadeleri

Aşağıdakileri göz önünde bulundur:

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

İlk üç denklem listenin boş olup olmadığını kontrol eder (diğer şeylerin yanı sıra). Ama aynı şeyi üç kez kontrol etmek israftır. Neyse ki, derleyicinin bunu birkaç iç içe kasa ifadesine optimize etmesi çok kolaydır. Bu durumda,

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

Bu daha az sezgisel, ancak daha verimlidir. Derleyici bu dönüşümü kolayca yapabildiğinden, endişelenmenize gerek yoktur. Sadece desen eşleşmenizi mümkün olan en sezgisel bir şekilde yazın; derleyici bunu mümkün olduğunca hızlı yapmak için yeniden sıralamak ve yeniden düzenlemekte çok iyidir.

Füzyon

Liste işleme için standart Haskell deyimi, bir liste alan ve yeni bir liste üreten işlevleri birbirine zincirlemektir. Kanonik örnek

map g . map f

Ne yazık ki, tembellik gereksiz işi atlamayı garanti ederken, ara liste öz performansı için tüm tahsisler ve anlaşmaların yapılması. "Füzyon" veya "ormansızlaşma" derleyicinin bu ara adımları ortadan kaldırmaya çalıştığı yerdir.

Sorun, bu işlevlerin çoğunun özyinelemeli olmasıdır. Özyineleme olmadan, tüm işlevleri tek bir büyük kod bloğunda ezmek, sadeleştiriciyi çalıştırmak ve ara liste olmadan gerçekten en uygun kodu üretmek için satır içi bir alıştırma olacaktır. Ancak özyineleme nedeniyle bu işe yaramaz.

{-# RULE #-}Bunlardan bazılarını düzeltmek için pragmalar kullanabilirsiniz . Örneğin,

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

Şimdi GHC her mapbaşvuruda bulunduğunda map, listeyi tek bir geçişte ezerek ara listeyi ortadan kaldırıyor.

Sorun şu ki, bu sadece maptakip için işe yarıyor map. - Diğer birçok olasılık vardır mapve ardından filter, filterardından map, vb daha çok "akış füzyonu" olarak adlandırılan, her biri için bir çözelti, icat edilmiştir elle kodu daha. Bu, burada tarif etmeyeceğim daha karmaşık bir numara.

Uzun ve kısa: Bunların hepsi programcı tarafından yazılan özel optimizasyon hileleridir . GHC'nin kendisi füzyon hakkında hiçbir şey bilmiyor; hepsi liste kütüphanelerinde ve diğer kap kütüphanelerinde. Bu nedenle, hangi optimizasyonların gerçekleştiği, kapsayıcı kitaplıklarınızın nasıl yazıldığına (veya daha gerçekçi olarak, hangi kitaplıkları kullanmayı seçtiğinize) bağlıdır.

Örneğin, Haskell '98 dizileriyle çalışıyorsanız, herhangi bir füzyon beklemeyin. Ancak vectorkütüphanenin geniş füzyon özelliklerine sahip olduğunu anlıyorum . Her şey kütüphanelerle ilgili; derleyici sadece RULESpragmayı sağlar. (Bu arada, son derece güçlü. Bir kütüphane yazarı olarak, bunu istemci kodunu yeniden yazmak için kullanabilirsiniz!)


Meta:

  • "Önce kod, ikinci profil, üçüncü optimize" diyen insanlara katılıyorum.

  • "Verilen bir tasarım kararının maliyeti için zihinsel bir modele sahip olmak yararlı" diyen insanlarla da aynı fikirdeyim.

Her şeyde ve her şeyde denge ...


9
it's something guaranteed by the language specification ... work is not performed until you "do something" with the result.- tam olarak değil. Dil özelliği katı olmayan anlambilim vaat ediyor ; gereksiz işlerin yapılıp yapılmayacağı hakkında hiçbir şey vaat etmez.
Dan Burton

1
@DanBurton Tabii. Ancak bu birkaç cümle ile açıklamak çok kolay değil. Ayrıca, GHC neredeyse tek Haskell uygulaması olduğundan, GHC'nin tembel olması çoğu insan için yeterince iyidir.
Matematiksel

@ Matematiksel Orkide: spekülatif değerlendirmeler ilginç bir karşı örnek, ancak muhtemelen yeni başlayanlar için çok fazla olduğunu kabul ediyorum.
Ben Millwood

5
ÖAM hakkında: Benim izlenimim neredeyse hiç yapılmadığı için, istenmeyen paylaşım ve dolayısıyla ara hatlar oluşturabilir.
Joachim Breitner

2
(A) Şimdiye kadar cevap vermediğiniz ve (b) cevabınızı kabul etmediğim için üzgünüm. Bu uzun ve etkileyici, ama istediğim bölgeyi kapsamıyor. Ne istiyorum lambda / let / case-kayan, tip / yapıcı / fonksiyon argümanı uzmanlığı, katılık analizi ve kutudan çıkarma (bahsettiğiniz), işçi / sarmalayıcı ve GHC'nin yaptığı her şey gibi daha düşük seviyeli dönüşümlerin bir listesi açıklamalar ve giriş ve çıkış kod örnekleri ve bunların kombine etkisi ve dönüşümler olanlar ideal örneklerle yok olur. Sanırım bir ödül yapmalıyım?
glaebhoerl

8

Eğer izin verilen bir v = rhs bağlaması sadece tek bir yerde kullanılıyorsa, rhs büyük olsa bile, onu derlemek için derleyiciye güvenebilirsiniz.

(Mevcut soru bağlamında neredeyse bir tane olmayan) istisna, iş kopyalarını riske atan lambdaslardır. Düşünmek:

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

orada satır içi v tehlikeli olacaktır çünkü tek (sözdizimsel) kullanım, rh'lerin ekstra 99 değerlendirmesine dönüşecektir. Ancak, bu durumda, bunu manuel olarak da satır içine almak istemezsiniz. Yani aslında kuralı kullanabilirsiniz:

Yalnızca bir kez görünen bir adı satır içine almayı düşünüyorsanız derleyici bunu yine de yapar.

Mutlu bir sonuç olarak, uzun bir ifadeyi (netlik kazanma umuduyla) ayrıştırmak için basitçe bir bağlama kullanmak aslında ücretsizdir.

Bu, inlining hakkında daha fazla bilgi içeren community.haskell.org/~simonmar/papers/inline.pdf adresinden gelir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.