2015-07-29 5 views
17

मैंने सी में एक महत्वपूर्ण ओवरहेड कॉलिंग हास्केल फ़ंक्शंस को देखा है, जो देशी सी फ़ंक्शन कॉल के ओवरहेड से काफी बड़ा है। इस मुद्दे को अपने सार में दूर करने के लिए, मैंने एक प्रोग्राम लिखा है जो केवल हास्केल रनटाइम को प्रारंभ करता है, एक लूप चलाता है जो खाली कार्य को 100,000,000 बार कॉल करता है, और रिटर्न देता है।सी से हास्केल फ़ंक्शन को ओवरहेड क्यों कर रहा है?

फ़ंक्शन इनलाइन के साथ, प्रोग्राम 0.003 लेता है। सी में लिखे गए एक खाली फ़ंक्शन को कॉल करना 0.18 लेता है। हास्केल में लिखे गए खाली फ़ंक्शन को कॉल करना 15.5 लेता है। (आश्चर्यजनक रूप से, अगर मैं लिंक करने से पहले खाली हास्केल फ़ाइल को अलग से संकलित करता हूं, तो इसमें कुछ और सेकंड लगते हैं। उप-प्रश्न: यह क्यों है?)

तो ऐसा लगता है कि सी फ़ंक्शन को कॉल करने के बीच लगभग 100 गुना मंदी है और एक हास्केल समारोह बुला रहा है। इसके लिए क्या कारण है, और क्या इस मंदी को कम करने का कोई तरीका है?

कोड

संपादित करें: मैं NoFib benchmark suite, callback002 में इस परीक्षण का एक संस्करण की खोज की है। एडवर्ड जेड यांग द्वारा nice blog post पर जीएचसी शेड्यूलर के संदर्भ में इस परीक्षण का जिक्र है। मैं अभी भी जेता के बहुत अच्छे जवाब के साथ इस ब्लॉग पोस्ट को ग्रोक करने की कोशिश कर रहा हूं। मुझे अभी तक विश्वास नहीं है कि इसे तेजी से करने का कोई तरीका नहीं है!

"धीमी" हास्केल संस्करण संकलन करने के लिए,

ghc -no-hs-main -O2 -optc-O3 test.c Test.hs -o test

"तेज" सी संस्करण संकलन करने के लिए चलाने के लिए, चलाने

ghc -no-hs-main -O2 -optc-O3 test.c test2.c TestDummy.hs -o test

test.c:

#include "HsFFI.h" 
extern void __stginit_Test(void); 

extern void test(); 

int main(int argc, char *argv[]) { 
    hs_init(&argc, &argv); 
    hs_add_root(__stginit_Test); 
    int i; 
    for (i = 0; i < 100000000; i++) { 
    test(); 
    } 
    hs_exit(); 
    return 0; 
} 

test2.c:

void test() { 
} 

Test.hs:

{-# LANGUAGE ForeignFunctionInterface #-} 

module Test where 

foreign export ccall test ::() 

test ::() 
test =() 

TestDummy.hs:

module Test where 
+2

यह सुनिश्चित करने के लिए बहुत कुछ काम है कि आप हास्केल फ़ंक्शन में मेमोरी इत्यादि आवंटित कर सकते हैं। – augustss

+6

हास्केल कोड द्वारा उत्पादित 'टेस्ट' प्रतीक को अलग करना शैक्षिक और एक महान ब्लॉग पोस्ट दोनों हो सकता है। –

+0

प्रतिकूल जिज्ञासा से बाहर ... 'थ्रेड' के साथ संकलित करता है किसी भी तरह से कोई फर्क पड़ता है? – MathematicalOrchid

उत्तर

16

टी एल; डॉ: कारण: आरटीएस और एसटीजी कहता है। समाधान: सी


इस का कारण क्या है ... से तुच्छ हास्केल कार्यों कहें?

अस्वीकरण: मैं कभी नहीं हास्केल सी से मैं सी और हास्केल से परिचित हूँ कहा जाता है, लेकिन मैं शायद ही कभी दोनों बटना, जब तक कि मैं एक आवरण लिख रहा हूँ। अब जब मैंने अपनी विश्वसनीयता खो दी है, तो हम बेंचमार्किंग, डिससेम्बलिंग और अन्य निफ्टी डरावनी इस साहसिक कार्य को शुरू करते हैं।

क्या अपने संसाधनों के सभी खाने है की जाँच करने के gprof

एक आसान तरीका के साथ

बेंचमार्किंग gprof उपयोग करने के लिए है। हम आपकी संकलन रेखा को थोड़ा बदल देंगे ताकि -pg दोनों कंपाइलर और लिंकर द्वारा उपयोग किया जा सके (नोट: मैंने टेस्ट.c को main.c और test2.c का परीक्षण करने के लिए नाम दिया है।ग):

$ ghc -no-hs-main -O2 -optc-O3 -optc-pg -optl-pg -fforce-recomp \ 
    main.c Test.hs -o test 
$ ./test 
$ gprof ./test 

यह हमें निम्नलिखित (फ्लैट) प्रोफ़ाइल देता है:

Flat profile: 

Each sample counts as 0.01 seconds. 
    % cumulative self    self  total   
time seconds seconds calls Ts/call Ts/call name  
16.85  2.15  2.15        scheduleWaitThread 
11.78  3.65  1.50        createStrictIOThread 
    7.66  4.62  0.98        createThread 
    6.68  5.47  0.85        allocate 
    5.66  6.19  0.72        traverseWeakPtrList 
    5.34  6.87  0.68        isAlive 
    4.12  7.40  0.53        newBoundTask 
    3.06  7.79  0.39        stg_ap_p_fast 
    2.36  8.09  0.30        stg_ap_v_info 
    1.96  8.34  0.25        stg_ap_0_fast 
    1.85  8.57  0.24        rts_checkSchedStatus 
    1.81  8.80  0.23        stg_PAP_apply 
    1.73  9.02  0.22        rts_apply 
    1.73  9.24  0.22        stg_enter_info 
    1.65  9.45  0.21        stg_stop_thread_info 
    1.61  9.66  0.21        test 
    1.49  9.85  0.19        stg_returnToStackTop 
    1.49  10.04  0.19        move_STACK 
    1.49  10.23  0.19        stg_ap_v_fast 
    1.41  10.41  0.18        rts_lock 
    1.18  10.56  0.15        boundTaskExiting 
    1.10  10.70  0.14        StgRun 
    0.98  10.82  0.13        rts_evalIO 
    0.94  10.94  0.12        stg_upd_frame_info 
    0.79  11.04  0.10        blockedThrowTo 
    0.67  11.13  0.09        StgReturn 
    0.63  11.21  0.08        createIOThread 
    0.63  11.29  0.08        stg_bh_upd_frame_info 
    0.63  11.37  0.08        c5KU_info 
    0.55  11.44  0.07        stg_stk_save_n 
    0.51  11.50  0.07        threadPaused 
    0.47  11.56  0.06        dirty_TSO 
    0.47  11.62  0.06        ghczmprim_GHCziCString_unpackCStringzh_info 
    0.47  11.68  0.06        stopHeapProfTimer 
    0.39  11.73  0.05        stg_threadFinished 
    0.39  11.78  0.05        allocGroup 
    0.39  11.83  0.05        base_GHCziTopHandler_runNonIO1_info 
    0.39  11.88  0.05        stg_catchzh 
    0.35  11.93  0.05        freeMyTask 
    0.35  11.97  0.05        rts_eval_ 
    0.31  12.01  0.04        awakenBlockedExceptionQueue 
    0.31  12.05  0.04        stg_ap_2_upd_info 
    0.27  12.09  0.04        s5q4_info 
    0.24  12.12  0.03        markStableTables 
    0.24  12.15  0.03        rts_getSchedStatus 
    0.24  12.18  0.03        s5q3_info 
    0.24  12.21  0.03        scavenge_stack 
    0.24  12.24  0.03        stg_ap_7_upd_info 
    0.24  12.27  0.03        stg_ap_n_fast 
    0.24  12.30  0.03        stg_gc_noregs 
    0.20  12.32  0.03        base_GHCziTopHandler_runIO1_info 
    0.20  12.35  0.03        stat_exit 
    0.16  12.37  0.02        GarbageCollect 
    0.16  12.39  0.02        dirty_STACK 
    0.16  12.41  0.02        freeGcThreads 
    0.16  12.43  0.02        rts_mkString 
    0.16  12.45  0.02        scavenge_capability_mut_lists 
    0.16  12.47  0.02        startProfTimer 
    0.16  12.49  0.02        stg_PAP_info 
    0.16  12.51  0.02        stg_ap_stk_p 
    0.16  12.53  0.02        stg_catch_info 
    0.16  12.55  0.02        stg_killMyself 
    0.16  12.57  0.02        stg_marked_upd_frame_info 
    0.12  12.58  0.02        interruptAllCapabilities 
    0.12  12.60  0.02        scheduleThreadOn 
    0.12  12.61  0.02        waitForReturnCapability 
    0.08  12.62  0.01        exitStorage 
    0.08  12.63  0.01        freeWSDeque 
    0.08  12.64  0.01        gcStableTables 
    0.08  12.65  0.01        resetTerminalSettings 
    0.08  12.66  0.01        resizeNurseriesEach 
    0.08  12.67  0.01        scavenge_loop 
    0.08  12.68  0.01        split_free_block 
    0.08  12.69  0.01        startHeapProfTimer 
    0.08  12.70  0.01        stg_MVAR_TSO_QUEUE_info 
    0.08  12.71  0.01        stg_forceIO_info 
    0.08  12.72  0.01        zero_static_object_list 
    0.04  12.73  0.01        frame_dummy 
    0.04  12.73  0.01        rts_evalLazyIO_ 
    0.00  12.73  0.00  1  0.00  0.00 stginit_export_Test_zdfstableZZC0ZZCmainZZCTestZZCtest 

ओह, कि कार्यों का एक समूह बुलाया हो रही है। यह आपके सी संस्करण की तुलना कैसे करता है?

$ ghc -no-hs-main -O2 -optc-O3 -optc-pg -optl-pg -fforce-recomp \ 
    main.c TestDummy.hs test.c -o test_c 
$ ./test_c 
$ gprof ./test_c 
Flat profile: 

Each sample counts as 0.01 seconds. 
    % cumulative self    self  total   
time seconds seconds calls Ts/call Ts/call name  
75.00  0.05  0.05        test 
25.00  0.06  0.02        frame_dummy 

यह एक बहुत आसान है। पर क्यों?

क्या पीछे हो रहा है?

शायद तुम सोच किया है कि क्यों test भी पिछले प्रोफ़ाइल में दिखाया। ठीक है, अपने आप में gprof, कुछ भूमि के ऊपर कहते हैं के रूप में objdump साथ देखा जा सकता है:

$ objdump -D ./test_c | grep -A5 "<test>:" 
0000000000405630 <test>: 
    405630: 55      push %rbp 
    405631: 48 89 e5    mov %rsp,%rbp 
    405634: e8 f7 d4 ff ff   callq 402b30 <[email protected]> 
    405639: 5d      pop %rbp 
    40563a: c3      retq 

mcount की कॉल जीसीसी से जोड़ा जाता है। तो अगले भाग के लिए आप -pg विकल्पों को हटाना चाहते हैं। की पहली सी में disassembled test नियमित जाँच करें:

$ ghc -no-hs-main -O2 -optc-O3 -fforce-recomp \ 
    main.c TestDummy.hs test.c -o test_c 
$ objdump -D ./test_c | grep -A2 "<test>:" 
0000000000405510 <test>: 
    405510: f3 c3     repz retq 

repz retq वास्तव में some optimisation magic है, लेकिन इस मामले में आप एक (अधिकतर) कोई-op वापसी के रूप में इसके बारे में सोच सकते हैं।

यह कैसे हास्केल संस्करण की तुलना में कैसा

$ ghc -no-hs-main -O2 -optc-O3 -fforce-recomp \ 
    main.c Test.hs -o test_hs  
$ objdump -D ./Test.o | grep -A18 "<test>:" 
0000000000405520 <test>: 
    405520: 48 83 ec 18    sub $0x18,%rsp 
    405524: e8 f7 3a 05 00   callq 459020 <rts_lock> 
    405529: ba 58 24 6b 00   mov $0x6b2458,%edx 
    40552e: be 80 28 6b 00   mov $0x6b2880,%esi 
    405533: 48 89 c7    mov %rax,%rdi 
    405536: 48 89 04 24    mov %rax,(%rsp) 
    40553a: e8 51 36 05 00   callq 458b90 <rts_apply> 
    40553f: 48 8d 54 24 08   lea 0x8(%rsp),%rdx 
    405544: 48 89 c6    mov %rax,%rsi 
    405547: 48 89 e7    mov %rsp,%rdi 
    40554a: e8 01 39 05 00   callq 458e50 <rts_evalIO> 
    40554f: 48 8b 34 24    mov (%rsp),%rsi 
    405553: bf 64 57 48 00   mov $0x485764,%edi 
    405558: e8 23 3a 05 00   callq 458f80 <rts_checkSchedStatus> 
    40555d: 48 8b 3c 24    mov (%rsp),%rdi 
    405561: e8 0a 3b 05 00   callq 459070 <rts_unlock> 
    405566: 48 83 c4 18    add $0x18,%rsp 
    40556a: c3      retq 
    40556b: 0f 1f 44 00 00   nopl 0x0(%rax,%rax,1) 
    405570: d8 ce     fmul %st(6),%st 

यह नहीं बल्कि अलग दिखता है। दरअसल, RTS फ़ंक्शन संदिग्ध प्रतीत होते हैं। के उन पर एक नजर है:

  • rts_checkSchedStatus बस की जाँच करता है स्थिति ठीक है, और नहीं तो बाहर निकल जाता है या नहीं। Success पथ में अधिक ओवरहेड नहीं है, इसलिए यह फ़ंक्शन वास्तव में अपराधी नहीं है।
  • rts_unlock and rts_lock मूल रूप से दावा करते हैं और capability (वर्चुअल सीपीयू) जारी करते हैं। वे newBoundTask और boundTaskExiting पर कॉल करते हैं, जिसमें कुछ समय लगता है (ऊपर प्रोफ़ाइल देखें)।
  • rts_apply कॉल allocate, कार्यक्रम के दौरान अक्सर उपयोग किए जाने वाले कार्यों में से एक (जो वास्तव में आश्चर्यजनक नहीं है, हैस्केल को कचरा इकट्ठा किया जा रहा है)।
  • rts_evalIO अंततः वास्तविक धागा बनाता है और इसके पूरा होने के लिए प्रतीक्षा करता है। तो हम अनुमान लगा सकते हैं कि rts_evalIO अकेले में लगभग 27% लगता है।

तो हमें हर समय लेने वाले सभी कार्यों को मिला। एसटीजी और आरटीएस प्रति कॉल 150ns के ओवरहेड के लिए सभी क्रेडिट लेते हैं।

... और क्या इस मंदी को कम करने का कोई तरीका है?

ठीक है, आपका test मूल रूप से नो-ऑप है। आप इसे 1500 के कुल रनटाइम के साथ 100000000 बार बुला रहे हैं। सी संस्करण की तुलना में, यह प्रति कॉल ~ 14 9ns का ओवरहेड है।

समाधान काफी सरल है: छोटे सी कार्यों के लिए अपनी सी दुनिया में हास्केल फ़ंक्शंस का उपयोग न करें। सही स्थिति के लिए सही उपकरण का प्रयोग करें। आखिरकार, आप जीएमपी लाइब्रेरी का उपयोग नहीं करते हैं, यदि आपको दो संख्याओं को जोड़ने की आवश्यकता है जो 10 से कम होने की गारंटी है।

इस पैराडाइमैटिक समाधान के अलावा: नहीं। ऊपर दिखाया गया असेंबली जीएचसी द्वारा बनाया गया है, और इस समय आरटीएस कॉल के बिना एक संस्करण बनाना संभव नहीं है।

+0

तो संक्षेप में, यह जीएचसी का हरा थ्रेड शेड्यूलिंग है जो समय ले रहा है। (?) – MathematicalOrchid

+1

तरह। यह बुरा नहीं होगा अगर सीपीयू (उर्फ क्षमता) या धागा पहले से मौजूद होगा, लेकिन वे नहीं करते हैं। और ['createThread'] (https://github.com/ghc/ghc/blob/892c3e98bcef50aa56ec818f4d001aee36e05bbc/rts/Threads.c) वास्तव में प्रत्येक आमंत्रण के लिए बुलाया जाने वाला नहीं है। एक नियमित हास्केल कार्यक्रम में, ऐसे कई धागे नहीं हैं, इसलिए यह कोई समस्या नहीं है। हालांकि, प्रत्येक कॉल के लिए ओएस थ्रेड बनाने से अभी भी बेहतर है। – Zeta

+0

काफी दिलचस्प है, मुझे लगता है कि एक असीमित वादा इस समस्या को कम करेगा। आरटीएस इसे (मुख्य) क्षमता और धागा रख सकता है। हालांकि, यह अन्य समस्याओं का परिचय देता है। – Zeta

संबंधित मुद्दे