GHC คาดว่าจะสามารถเพิ่มประสิทธิภาพการทำงานได้อย่างน่าเชื่อถือ?


183

GHC มีการปรับแต่งมากมายที่สามารถทำงานได้ แต่ฉันไม่รู้ว่าพวกเขาทั้งหมดคืออะไรและมีแนวโน้มว่าจะต้องดำเนินการอย่างไรและอยู่ภายใต้สถานการณ์ใด

คำถามของฉันคืออะไรฉันสามารถคาดหวังการเปลี่ยนแปลงอะไรที่จะนำไปใช้ทุกครั้งหรือเกือบจะเป็นเช่นนั้น? ถ้าฉันดูรหัสที่จะถูกดำเนินการ (ประเมิน) บ่อยครั้งและความคิดแรกของฉันคือ "hmm บางทีฉันควรปรับให้เหมาะสม" ซึ่งในกรณีนี้ความคิดที่สองของฉันควรเป็น "ไม่ต้องคิดเลย GHC ได้รับสิ่งนี้ "?

ฉันกำลังอ่านStream Stream Fusionของกระดาษ: จากรายการไปจนถึงสตรีมไปยังไม่มีอะไรเลยและเทคนิคที่พวกเขาใช้ในการเขียนรายการประมวลผลในรูปแบบอื่นซึ่งการปรับตามปกติของ GHC จะเพิ่มประสิทธิภาพลงในลูปแบบง่าย ๆ ฉันจะบอกได้อย่างไรว่าโปรแกรมของฉันมีสิทธิ์ได้รับการเพิ่มประสิทธิภาพแบบนั้น?

มีข้อมูลบางอย่างในคู่มือ GHC แต่มีเพียงบางส่วนที่จะตอบคำถาม

แก้ไข: ฉันเริ่มได้รับรางวัล สิ่งที่ฉันต้องการคือรายการของการแปลงระดับล่างเช่นแลมบ์ดา / เคส / กรณีลอย, พิเศษ / ประเภทคอนสตรัค / ฟังก์ชั่นการโต้แย้งการวิเคราะห์ความเข้มงวดและ unboxing, คนงาน / ห่อหุ้มและสิ่งอื่น ๆ ที่สำคัญ GHC พร้อมด้วยคำอธิบายและตัวอย่างของอินพุตและเอาต์พุตโค้ดและภาพประกอบที่สมบูรณ์แบบของสถานการณ์เมื่อเอฟเฟกต์ทั้งหมดมากกว่าผลรวมของส่วนต่างๆ และเป็นที่กล่าวถึงเมื่อการเปลี่ยนแปลงไม่ได้ผลเกิดขึ้น ฉันไม่ได้คาดหวังว่าคำอธิบายที่มีความยาวนวนิยายของการเปลี่ยนแปลงทุกอย่างสองสามประโยคและตัวอย่างโค้ดอินไลน์ซับหนึ่งอาจเพียงพอ ชัดเจนในตอนท้ายของมัน ฉันต้องการที่จะดูรหัสและสามารถคาดเดาได้ดีว่ามันจะรวบรวมเป็นวงแคบหรือไม่หรือทำไมไม่หรือสิ่งที่ฉันจะต้องเปลี่ยนเพื่อให้ได้ (ฉันไม่สนใจมากที่นี่ในกรอบการเพิ่มประสิทธิภาพขนาดใหญ่เช่นการหลอมกระแส (ฉันเพิ่งอ่านบทความเกี่ยวกับเรื่องนั้น); ในความรู้ที่คนเขียนกรอบเหล่านี้มีมากขึ้น)


10
นี่เป็นคำถามที่คุ้มค่าที่สุด การเขียนคำตอบที่คู่ควรคือ ... หากิน
ทางคณิตศาสตร์

1
จุดเริ่มต้นที่ดีจริงๆคือ: aosabook.org/en/ghc.html
Gabriel Gonzalez

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 ปฏิเสธที่จะทำหน้าที่แบบเรียกซ้ำแบบอินไลน์และสิ่งนั้นจะต้องตั้งชื่ออย่างถูกต้องเพื่อให้ฟิวชั่นทำงาน

ต่อไปเราจะเห็นรายการทั้งหมดของการปรับให้เหมาะสมทั้งหมดที่ทำ:

  • เชี่ยวชาญ

    แนวคิดพื้นฐานของความเชี่ยวชาญคือการลบ polymorphism และ overloading โดยการระบุตำแหน่งที่ฟังก์ชันถูกเรียกใช้และสร้างเวอร์ชันของฟังก์ชันที่ไม่ใช่ polymorphic - พวกมันเฉพาะกับประเภทที่เรียกด้วย คุณสามารถบอกคอมไพเลอร์ให้ทำเช่นนี้ด้วย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ทำซึ่งเร็วกว่าของเดิมมาก อีกวิธีในการดูความเชี่ยวชาญคือการใช้งานบางส่วนกับพจนานุกรมประเภทคลาสและตัวแปรประเภท

    แหล่งที่มาที่นี่มีการโหลดของบันทึกในนั้น

  • ลอยออกไป

    แก้ไข: เห็นได้ชัดว่าฉันเข้าใจผิดนี้มาก่อน คำอธิบายของฉันมีการเปลี่ยนแปลงอย่างสมบูรณ์

    แนวคิดพื้นฐานของสิ่งนี้คือการย้ายการคำนวณที่ไม่ควรซ้ำออกจากฟังก์ชั่น ตัวอย่างเช่นสมมติว่าเรามีสิ่งนี้:

    \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

    การคำนวณซ้ำจะถูกบันทึกอีกครั้ง

    แหล่งที่สามารถอ่านได้มากในกรณีนี้

    ในขณะนี้การผูกระหว่าง lambdas ที่อยู่ติดกันจะไม่ถูกลอย ตัวอย่างเช่นสิ่งนี้ไม่ได้เกิดขึ้น:

    \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

    ซึ่งต่อมาถูกแทนที่ด้วย simplifier ด้วย

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

    บทความนี้แม้ว่าจะครอบคลุมหัวข้ออื่น ๆ แต่ก็มีการแนะนำที่ชัดเจนพอสมควร โปรดทราบว่าแม้ชื่อจะลอยอยู่และลอยออกไปก็ไม่ได้อยู่ในวงวนไม่ จำกัด ด้วยเหตุผลสองประการ:

    1. ลอยในลอยให้เป็นcaseคำสั่งในขณะที่ลอยออกข้อตกลงกับฟังก์ชั่น
    2. มีลำดับการส่งผ่านที่แน่นอนดังนั้นพวกเขาไม่ควรสลับกันเป็นอนันต์

  • การวิเคราะห์อุปสงค์

    การวิเคราะห์ความต้องการหรือการวิเคราะห์ความเข้มงวดน้อยกว่าการเปลี่ยนแปลงและอื่น ๆ เช่นชื่อของการรวบรวมข้อมูล คอมไพเลอร์ค้นหาฟังก์ชันที่ประเมินอาร์กิวเมนต์ของตน (หรืออย่างน้อยก็บางส่วน) และส่งผ่านอาร์กิวเมนต์เหล่านั้นโดยใช้การโทรตามค่าแทนการโทรตามความต้องการ เนื่องจากคุณหลบเลี่ยงค่าโสหุ้ยของ Thunks นี่จึงเร็วกว่ามาก ปัญหาประสิทธิภาพการทำงานจำนวนมากใน Haskell เกิดจากความล้มเหลวของรหัสผ่านนี้หรือรหัสไม่เข้มงวดเพียงพอ ตัวอย่างง่ายๆคือความแตกต่างระหว่างการใช้foldr, foldlและfoldl'เพื่อรวมรายการของจำนวนเต็ม - สาเหตุแรกคือโอเวอร์โฟลว์สแต็กอันดับที่สองทำให้ฮีพโอเวอร์โฟลว์และรายการสุดท้ายทำงานได้ดีเนื่องจากความเข้มงวด นี่อาจเป็นวิธีที่ง่ายที่สุดที่จะเข้าใจและจัดทำเอกสารเหล่านี้ได้ดีที่สุด ฉันเชื่อว่า polymorphism และรหัส CPS มักจะเอาชนะสิ่งนี้

  • Wrapper คนงานผูก

    แนวคิดพื้นฐานของการเปลี่ยนแปลงของคนงาน / ผู้ห่อหุ้มคือการวนรอบที่แน่นหนาบนโครงสร้างที่เรียบง่ายการแปลงไปและกลับจากโครงสร้างนั้นในตอนท้าย ตัวอย่างเช่นใช้ฟังก์ชันนี้ซึ่งคำนวณค่าแฟกทอเรียลของตัวเลข

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

    ใช้คำจำกัดความของIntใน GHC เรามี

    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

    ซึ่งลดการคำนวณลงครึ่งหนึ่ง น่าเสียดายที่บางครั้งสิ่งนี้อาจทำให้เกิดการปรับปรุงอื่น ๆ ได้เป็นครั้งคราว อีกปัญหาคือว่านิพจน์ทั้งสองนั้นจะต้องอยู่ในที่เดียวกันและพวกเขาจะต้องมีsyntacticallyเหมือนกันไม่เหมือนกันตามค่า ตัวอย่างเช่น CSE จะไม่เริ่มทำงานในรหัสต่อไปนี้หากไม่มี inlining จำนวนมาก:

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

    อย่างไรก็ตามหากคุณรวบรวมผ่าน llvm คุณอาจได้รับข้อมูลบางส่วนนี้เนื่องจากรหัสผ่านการกำหนดหมายเลขทั่วโลก

  • ปลดปล่อยคดี

    นี่น่าจะเป็นการแปลงเอกสารที่น่ากลัวนอกจากความจริงที่ว่ามันสามารถทำให้เกิดการระเบิดของรหัสได้ นี่คือเอกสารเล็ก ๆ น้อย ๆ ที่ฉันพบในรูปแบบ (และเขียนใหม่เล็กน้อย):

    โมดูลนี้เดินไป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)

    นี่คือรหัสดีกว่าเพราะaเป็นอิสระภายในภายในแทนที่จะต้องประมาณการจาก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

    โปรดทราบว่าโปรแกรมนี้เร็วขึ้นเนื่องจากเราไม่ได้ทำการชกมวยซ้ำ ๆ และยกเลิกการทำกล่องด้านหน้าของรายการ นอกจากนี้โปรดทราบว่าการอินไลน์เป็นสิ่งสำคัญเนื่องจากจะช่วยให้คำจำกัดความใหม่ที่มีประสิทธิภาพมากขึ้นสามารถใช้งานได้จริงเช่นเดียวกับการทำให้คำจำกัดความซ้ำซ้อนดีขึ้น

    SpecConstr ควบคุมโดยฮิวริสติกจำนวนมาก สิ่งที่กล่าวถึงในเอกสารมีดังนี้:

    1. lambdas นั้นชัดเจนและ arity คือ aคือ
    2. ด้านขวามือคือ "เล็กพอสมควร" บางสิ่งบางอย่างที่ควบคุมโดยธง
    3. ฟังก์ชั่นแบบเรียกซ้ำและการโทรแบบพิเศษสามารถใช้งานได้ทางด้านขวามือ
    4. อาร์กิวเมนต์ทั้งหมดของฟังก์ชันมีอยู่
    5. อย่างน้อยหนึ่งข้อโต้แย้งคือแอปพลิเคชันตัวสร้าง
    6. อาร์กิวเมนต์นั้นได้รับการวิเคราะห์ตัวพิมพ์เล็ก ๆ น้อย ๆ ในฟังก์ชัน

    อย่างไรก็ตามฮิวริสติกมีการเปลี่ยนแปลงอย่างแน่นอน ในความเป็นจริงกระดาษกล่าวถึงการแก้ปัญหาที่หกทางเลือก:

    เชี่ยวชาญในการโต้แย้งxแต่ถ้าxเป็นเพียงการพิจารณาด้วยcaseและไม่ได้ส่งผ่านไปยังฟังก์ชั่นสามัญหรือกลับมาเป็นส่วนหนึ่งของผล

นี่เป็นไฟล์ขนาดเล็กมาก (12 บรรทัด) และอาจไม่ได้ทำให้เกิดการปรับแต่งมากมาย (แม้ว่าฉันคิดว่ามันทำได้ทั้งหมด) สิ่งนี้ยังไม่ได้บอกคุณว่าทำไมมันถึงเลือกบัตรเหล่านั้นและทำไมมันถึงสั่งให้มัน


ตอนนี้เรากำลังจะไปที่ไหนซักแห่ง! ความคิดเห็น: ดูเหมือนว่าคุณจะมีประโยคตัดในส่วนที่เกี่ยวกับความเชี่ยวชาญ ฉันไม่เห็นจุดลอยตัวออกมาเพื่ออะไร มันจะตัดสินใจได้อย่างไรว่าจะลอยเข้าหรือออก (ทำไมมันไม่เข้าสู่วง)? ฉันประทับใจจากที่ไหนสักแห่งที่ GHC ไม่ได้ทำ CSE เลยแต่ดูเหมือนจะเข้าใจผิด ฉันรู้สึกว่าฉันหลงทางในรายละเอียดแทนที่จะเห็นภาพใหญ่ ... หัวข้อนั้นซับซ้อนกว่าที่ฉันคิด บางทีคำถามของฉันเป็นไปไม่ได้และไม่มีทางที่จะได้รับสัญชาตญาณนี้ยกเว้นประสบการณ์มากมายหรือทำงานกับ GHC ด้วยตัวคุณเอง?
glaebhoerl

ดีฉันไม่ทราบ แต่ผมไม่เคยทำงานใน GHC ดังนั้นคุณจะต้องสามารถที่จะได้รับบางสัญชาตญาณ
gereeter

ฉันแก้ไขปัญหาที่คุณพูดถึง
gereeter

1
นอกจากนี้เกี่ยวกับภาพรวมมันเป็นความคิดของฉันที่ไม่มีจริงๆ เมื่อฉันต้องการที่จะคาดเดาการเพิ่มประสิทธิภาพจะดำเนินการฉันไปลงรายการตรวจสอบ จากนั้นฉันก็ทำอีกครั้งเพื่อดูว่าแต่ละรอบจะเปลี่ยนสิ่งต่าง ๆ ได้อย่างไร และอีกครั้ง. โดยพื้นฐานแล้วฉันเล่นคอมไพเลอร์ รูปแบบการปรับให้เหมาะสมเพียงอย่างเดียวที่ฉันรู้ว่ามี "ภาพรวม" อย่างแท้จริงคือการคอมไพล์พิเศษ
gereeter

1
คุณหมายถึง "สิ่งต่าง ๆ ต้องมีชื่ออย่างถูกต้องเพื่อให้ฟิวชั่นทำงาน"
Vincent Beffara

65

ความเกียจคร้าน

ไม่ใช่ "การเพิ่มประสิทธิภาพคอมไพเลอร์" แต่เป็นสิ่งที่รับประกันโดยข้อกำหนดภาษาดังนั้นคุณจึงสามารถวางใจได้ว่าจะเกิดอะไรขึ้น โดยพื้นฐานแล้วนี่หมายความว่างานจะไม่ถูกดำเนินการจนกว่าคุณจะ "ทำอะไรบางอย่าง" กับผลลัพธ์ (เว้นเสียแต่ว่าคุณจะทำหลายอย่างเพื่อปิดความขี้เกียจ)

เห็นได้ชัดว่านี่เป็นหัวข้อทั้งหมดในสิทธิของตนเองและดังนั้นมีคำถามและคำตอบมากมายเกี่ยวกับเรื่องนี้แล้ว

จากประสบการณ์ที่ จำกัด ของฉันการทำให้โค้ดของคุณขี้เกียจเกินไปหรือเข้มงวดเกินไปมีบทลงโทษที่มีประสิทธิภาพอย่างมาก (ในเวลาและสถานที่) มากกว่าสิ่งอื่นใดที่ฉันกำลังจะพูดถึง ...

การวิเคราะห์ความเข้มงวด

ความเกียจคร้านเป็นเรื่องเกี่ยวกับการหลีกเลี่ยงงานเว้นแต่จำเป็น หากคอมไพเลอร์สามารถระบุได้ว่าผลลัพธ์ที่ต้องการจะ "เสมอ" เป็นสิ่งจำเป็นแล้วมันจะไม่รบกวนการจัดเก็บการคำนวณและดำเนินการในภายหลัง มันจะทำการแสดงโดยตรงเนื่องจากมีประสิทธิภาพมากขึ้น สิ่งนี้เรียกว่า "การวิเคราะห์ความเข้มงวด"

เห็นได้ชัดว่า gotcha คือคอมไพเลอร์ไม่สามารถตรวจพบได้ตลอดเวลาเมื่อมีบางสิ่งที่เข้มงวด บางครั้งคุณจำเป็นต้องให้คำแนะนำเพียงเล็กน้อยสำหรับคอมไพเลอร์ (ฉันไม่ทราบวิธีง่ายๆในการพิจารณาว่าการวิเคราะห์ความเข้มงวดได้ทำในสิ่งที่คุณคิดว่ามันมีหรือไม่นอกจากลุยผ่าน Core output)

การจัด

หากคุณเรียกใช้ฟังก์ชันและคอมไพเลอร์สามารถบอกได้ว่าคุณกำลังเรียกใช้ฟังก์ชันใดมันอาจลอง "inline" ฟังก์ชั่นนั้น - นั่นคือเพื่อแทนที่การเรียกใช้ฟังก์ชันด้วยสำเนาของฟังก์ชันเอง โอเวอร์เฮดของการเรียกใช้ฟังก์ชันมักจะค่อนข้างเล็ก แต่การอินไลน์มักทำให้เกิดการเพิ่มประสิทธิภาพอื่น ๆ ซึ่งจะไม่เกิดขึ้นเป็นอย่างอื่นดังนั้นการอินไลน์อาจเป็นชัยชนะครั้งใหญ่

ฟังก์ชั่นจะถูกขีดเส้นใต้หากพวกเขา "เล็กพอ" (หรือถ้าคุณเพิ่ม pragma โดยเฉพาะขออินไลน์) นอกจากนี้ยังสามารถใช้ฟังก์ชันอินไลน์ได้หากคอมไพเลอร์สามารถบอกได้ว่าคุณกำลังเรียกใช้ฟังก์ชันใด มีสองวิธีหลักที่คอมไพเลอร์ไม่สามารถบอกได้:

  • หากฟังก์ชั่นที่คุณโทรถูกส่งผ่านจากที่อื่น เช่นเมื่อfilterรวบรวมฟังก์ชั่นคุณจะไม่สามารถอินไลน์เพรดิเคตกรองได้เนื่องจากเป็นอาร์กิวเมนต์ที่ผู้ใช้ระบุ

  • หากฟังก์ชั่นที่คุณโทรเป็นวิธีการเรียนและคอมไพเลอร์ไม่ทราบว่ามีส่วนเกี่ยวข้องประเภทใด เช่นเมื่อsumรวบรวมฟังก์ชั่นคอมไพเลอร์ไม่สามารถอินไลน์+ฟังก์ชั่นได้เพราะsumทำงานกับตัวเลขหลายประเภทซึ่งแต่ละอันมี+ฟังก์ชั่นที่แตกต่างกัน

ในกรณีหลังคุณสามารถใช้{-# SPECIALIZE #-}pragma เพื่อสร้างรุ่นของฟังก์ชั่นที่ฮาร์ดโค้ดให้เป็นประเภทเฉพาะ เช่น{-# SPECIALIZE sum :: [Int] -> Int #-}จะคอมไพล์เวอร์ชันของsumฮาร์ดโค้ดสำหรับIntประเภทซึ่งหมายความว่า+สามารถอินไลน์ในเวอร์ชันนี้

หมายเหตุ แต่ที่พิเศษใหม่ของเราฟังก์ชั่นจะถูกเรียกเมื่อคอมไพเลอร์สามารถบอกได้ว่าเรากำลังทำงานร่วมกับsum Intมิฉะนั้นsumจะได้รับการเรียกว่าpolymorphic ดั้งเดิม อีกครั้งค่าใช้จ่ายในการเรียกใช้ฟังก์ชั่นจริงมีขนาดค่อนข้างเล็ก มันเป็นการเพิ่มประสิทธิภาพเพิ่มเติมที่อินไลน์สามารถเปิดใช้งานซึ่งเป็นประโยชน์

กำจัด subexpression ทั่วไป

หากบล็อกของโค้ดคำนวณค่าเดียวกันสองครั้งคอมไพเลอร์อาจแทนที่ด้วยอินสแตนซ์เดียวของการคำนวณเดียวกัน ตัวอย่างเช่นถ้าคุณทำ

(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

น่าเสียดายที่ในขณะที่ความเกียจคร้านรับประกันว่าจะข้ามงานที่ไม่จำเป็น แต่การจัดสรรและยกเลิกการจัดสรรทั้งหมดสำหรับประสิทธิภาพของรายการระดับกลาง "ฟิวชั่น" หรือ "การทำลายป่า" เป็นที่ที่คอมไพเลอร์พยายามกำจัดขั้นตอนกลางเหล่านี้

ปัญหาคือฟังก์ชั่นเหล่านี้ส่วนใหญ่จะเรียกซ้ำ หากไม่มีการเรียกซ้ำมันจะเป็นการฝึกเบื้องต้นในการซับฟังก์ชั่นทั้งหมดลงในบล็อคโค้ดขนาดใหญ่หนึ่งเรียกใช้ซิมป์มันร์และสร้างโค้ดที่ดีที่สุดโดยไม่มีลิสต์กลาง แต่เนื่องจากการเรียกซ้ำจึงไม่ทำงาน

คุณสามารถใช้{-# RULE #-}pragmas เพื่อแก้ไขปัญหานี้ได้ ตัวอย่างเช่น,

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

ตอนนี้ทุกครั้งที่ GHC เห็นว่ามีmapการนำไปใช้mapงานมันจะทำให้มันกลายเป็น pass pass อันเดียวในรายการ

ปัญหาคืองานนี้เฉพาะตามมาด้วยmap mapมีความเป็นไปอื่น ๆ อีกมากมาย - mapตามมาด้วยfilter, filterตามมาด้วยmapฯลฯ แทนที่จะมือรหัสทางออกสำหรับแต่ละของพวกเขาเรียกว่า "สตรีมฟิวชั่น" ถูกคิดค้น นี่เป็นกลอุบายที่ซับซ้อนมากขึ้นซึ่งฉันจะไม่อธิบายที่นี่

ยาวและระยะสั้นของมันคือเหล่านี้ล้วนเป็นเทคนิคการเพิ่มประสิทธิภาพพิเศษที่เขียนโดยโปรแกรมเมอร์ GHC เองไม่รู้อะไรเลยเกี่ยวกับการหลอมรวม; มันคือทั้งหมดในไลบรารีรายการและไลบรารีคอนเทนเนอร์อื่น ๆ ดังนั้นสิ่งที่ออปติไมซ์เกิดขึ้นนั้นขึ้นอยู่กับวิธีเขียนไลบรารีคอนเทนเนอร์ของคุณ (หรือสมจริงยิ่งขึ้นซึ่งเป็นไลบรารี่ที่คุณเลือกใช้)

ตัวอย่างเช่นหากคุณทำงานกับอาร์เรย์ Haskell '98 อย่าคาดหวังว่าจะมีการรวมฟิวชั่นใด ๆ แต่ฉันเข้าใจว่าvectorห้องสมุดมีความสามารถในการหลอมรวมอย่างกว้างขวาง มันเกี่ยวกับห้องสมุด คอมไพเลอร์ให้RULESpragma (ซึ่งมีประสิทธิภาพมากโดยวิธีการในฐานะผู้เขียนห้องสมุดคุณสามารถใช้มันเพื่อเขียนรหัสลูกค้า!)


Meta:

  • ฉันเห็นด้วยกับผู้คนที่พูดว่า "รหัสแรกโปรไฟล์ที่สองเพิ่มประสิทธิภาพที่สาม"

  • ฉันเห็นด้วยกับคนที่พูดว่า "มันมีประโยชน์ที่จะมีแบบจำลองทางจิตสำหรับค่าใช้จ่ายในการตัดสินใจออกแบบที่กำหนด"

ยอดคงเหลือในทุกสิ่งและทุกสิ่งที่ ...


9
it's something guaranteed by the language specification ... work is not performed until you "do something" with the result.- ไม่แน่นอน ข้อมูลจำเพาะภาษาสัญญาความหมายที่ไม่เข้มงวด ; มันไม่ได้สัญญาอะไรเลยเกี่ยวกับการทำงานที่เกินความจำเป็นหรือไม่
Dan Burton

1
@Banton แน่นอน แต่นั่นไม่ใช่เรื่องง่ายที่จะอธิบายในสองสามประโยค นอกจากนี้เนื่องจาก GHC เป็นเพียงการดำเนินการของ Haskell ที่ยังหลงเหลืออยู่ความจริงที่ว่า GHC นั้นขี้เกียจนั้นดีพอสำหรับคนส่วนใหญ่
ทางคณิตศาสตร์

@ MathematicalOrchid: การประเมินการเก็งกำไรเป็นตัวอย่างที่น่าสนใจแม้ว่าฉันจะเห็นด้วยว่ามันอาจจะมากเกินไปสำหรับผู้เริ่มต้น
Ben Millwood

5
เกี่ยวกับ CSE: ความประทับใจของฉันคือแทบจะไม่เคยทำเลยเพราะมันสามารถแนะนำการแชร์ที่ไม่พึงประสงค์และด้วยเหตุนี้ spaceleaks
Joachim Breitner

2
ขออภัยสำหรับ (a) ไม่ตอบกลับก่อนหน้านี้และ (b) ไม่ยอมรับคำตอบของคุณ ซึ่งยาวและน่าประทับใจ แต่ไม่ครอบคลุมพื้นที่ที่ฉันต้องการ สิ่งที่ฉันต้องการคือรายการของการแปลงในระดับต่ำกว่าเช่นแลมบ์ดา / ให้ / กรณี - ลอยความเชี่ยวชาญอาร์กิวเมนต์ / ประเภท / คอนสตรัคเตอร์ / ฟังก์ชั่นการวิเคราะห์ความเข้มงวดและ unboxing (ที่คุณพูดถึง) คนงาน / เสื้อคลุม พร้อมคำอธิบายและตัวอย่างของอินพุตและเอาต์พุตโค้ดและตัวอย่างที่ดีเลิศของเอฟเฟกต์รวมและอันที่การเปลี่ยนแปลงไม่ได้เกิดขึ้น ฉันเดาว่าฉันควรจะทำเงินรางวัล?
glaebhoerl

8

หากใช้การโยงให้ v = rhs ในที่เดียวเท่านั้นคุณสามารถพึ่งพาคอมไพเลอร์เพื่ออินไลน์ได้แม้ว่า rhs จะใหญ่

ข้อยกเว้น (ที่เกือบจะไม่ใช่หนึ่งในบริบทของคำถามปัจจุบัน) คือ lambdas เสี่ยงต่อการทำซ้ำงาน พิจารณา:

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

การ inlining v จะเป็นอันตรายเพราะการใช้ (การสร้างประโยค) จะแปลผลการประเมินพิเศษของ rhs 99 รายการ อย่างไรก็ตามในกรณีนี้คุณอาจไม่ต้องการอินไลน์ด้วยตนเองเช่นกัน ดังนั้นโดยพื้นฐานแล้วคุณสามารถใช้กฎนี้ได้:

หากคุณต้องการพิจารณาการสร้างชื่อที่ปรากฏเพียงครั้งเดียวคอมไพเลอร์จะทำมันต่อไป

ในฐานะที่เป็นข้อพิสูจน์ที่มีความสุขการใช้การผูกมัดแบบง่ายๆเพื่อสลายคำแถลงที่ยาว (ด้วยความหวังว่าจะได้ความชัดเจน) นั้นฟรี

สิ่งนี้มาจาก community.haskell.org/~simonmar/papers/inline.pdf ซึ่งมีข้อมูลเพิ่มเติมมากมายเกี่ยวกับอินไลน์

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.