2017-04-17 7 views
11

जुलिआ के माध्यम से जाने के दौरान, मैं पाइथन के dis मॉड्यूल के समान कार्यक्षमता चाहता था। नेट पर के माध्यम से जा रहे हैं, मुझे पता चला है कि जूलिया समुदाय इस मुद्दे पर काम किया है और दे दिया है इन (https://github.com/JuliaLang/julia/issues/218)जूलिया में @code_native, @code_typed और @code_llvm के बीच क्या अंतर है?

finfer -> code_typed 
methods(function, types) -> code_lowered 
disassemble(function, types, true) -> code_native 
disassemble(function, types, false) -> code_llvm 

मैं इन व्यक्तिगत रूप से जूलिया आरईपीएल उपयोग करने की कोशिश की है, लेकिन मैं काफी यह मुश्किल लगता है समझने के लिए।

पायथन में, मैं इस तरह के एक समारोह को अलग कर सकता हूं।

>>> import dis 
>>> dis.dis(lambda x: 2*x) 
    1   0 LOAD_CONST    1 (2) 
       3 LOAD_FAST    0 (x) 
       6 BINARY_MULTIPLY  
       7 RETURN_VALUE   
>>> 

क्या कोई भी जिसने इन सहायता के साथ काम किया है, क्या मुझे उन्हें और समझने में मदद मिल सकती है? धन्यवाद।

उत्तर

24

पायथन पार्स के मानक सीपीथन कार्यान्वयन स्रोत कोड का विश्लेषण करता है और इसके कुछ प्रसंस्करण और सरलीकरण करता है - उर्फ ​​"कम करना" - इसे मशीन के अनुकूल, आसानी से व्याख्या प्रारूप में बदलकर "bytecode" कहा जाता है। जब आप एक पायथन समारोह को "अलग" करते हैं तो यह तब प्रदर्शित होता है। यह कोड हार्डवेयर द्वारा निष्पादन योग्य नहीं है - यह CPython दुभाषिया द्वारा "निष्पादन योग्य" है। सीपीथन का बाइटकोड प्रारूप काफी सरल है, आंशिक रूप से क्योंकि दुभाषिया बहुत अच्छा करते हैं - यदि बाइटकोड बहुत जटिल है, तो यह दुभाषिया को धीमा कर देता है - और आंशिक रूप से क्योंकि पायथन समुदाय सादगी पर उच्च प्रीमियम डालता है, कभी-कभी लागत पर उच्च प्रदर्शन का।

जूलिया के कार्यान्वयन का अर्थ नहीं है, यह just-in-time (JIT) compiled है। इसका अर्थ यह है कि जब आप कोई फ़ंक्शन कॉल करते हैं, तो उसे मशीन कोड में बदल दिया जाता है जिसे मूल हार्डवेयर द्वारा सीधे निष्पादित किया जाता है। यह प्रक्रिया पार्सिंग की तुलना में काफी जटिल है और पाइथन द्वारा बाइटकोड को कम करने के लिए, लेकिन उस जटिलता के बदले में, जूलिया को इसकी हॉलमार्क गति मिलती है। (पाइथन के लिए पीपीपी जेआईटी सीपीथॉन की तुलना में भी अधिक जटिल है, लेकिन आम तौर पर बहुत तेज़ - बढ़ी जटिलता गति के लिए काफी आम लागत है।) जूलिया कोड के लिए "डिस्सेप्लिब्स" के चार स्तर आपको जूलिया विधि के प्रतिनिधित्व तक पहुंच प्रदान करते हैं स्रोत कोड से मशीन कोड में परिवर्तन के विभिन्न चरणों में विशेष तर्क प्रकारों के लिए कार्यान्वयन।

function nextfib(n) 
    a, b = one(n), one(n) 
    while b < n 
     a, b = b, a + b 
    end 
    return b 
end 

julia> nextfib(5) 
5 

julia> nextfib(6) 
8 

julia> nextfib(123) 
144 

घटी कोड: मैं निम्नलिखित समारोह जो एक उदाहरण के रूप में अपनी बहस के बाद अगले फिबोनैकी संख्या की गणना का उपयोग करेंगे।@code_lowered मैक्रो एक प्रारूप में कोड प्रदर्शित करता है जो पाइथन बाइट कोड के सबसे नज़दीक है, लेकिन एक दुभाषिया द्वारा निष्पादन के इरादे के बजाय, यह एक कंपाइलर द्वारा आगे परिवर्तन के लिए है। यह प्रारूप काफी हद तक आंतरिक है और मानव उपभोग के लिए नहीं है। कोड को "single static assignment" रूप में परिवर्तित किया गया है जिसमें "प्रत्येक चर को बिल्कुल एक बार असाइन किया जाता है, और प्रत्येक चर को इसका उपयोग करने से पहले परिभाषित किया जाता है"। लूप और सशर्त को एक unless/goto निर्माण (यह उपयोगकर्ता-स्तर जूलिया में प्रकट नहीं होता है) का उपयोग करके गेटोस और लेबल में परिवर्तित हो जाता है। यहाँ उतारा रूप में हमारे उदाहरण कोड (जूलिया में 0.6.0-pre.beta.134, जो सिर्फ मैं क्या उपलब्ध है के लिए हो है):

julia> @code_lowered nextfib(123) 
CodeInfo(:(begin 
     nothing 
     SSAValue(0) = (Main.one)(n) 
     SSAValue(1) = (Main.one)(n) 
     a = SSAValue(0) 
     b = SSAValue(1) # line 3: 
     7: 
     unless b < n goto 16 # line 4: 
     SSAValue(2) = b 
     SSAValue(3) = a + b 
     a = SSAValue(2) 
     b = SSAValue(3) 
     14: 
     goto 7 
     16: # line 6: 
     return b 
    end)) 

आप देख सकते हैं SSAValue नोड्स और unless/goto निर्माणों और लेबल संख्याएं यह पढ़ना मुश्किल नहीं है, लेकिन फिर भी, यह वास्तव में मानव उपभोग के लिए आसान नहीं है। कम कोड तर्क के प्रकारों पर निर्भर नहीं करता है, सिवाय इसके कि जब तक वे निर्धारित करते हैं कि कौन सी विधि शरीर को कॉल करना है - जब तक एक ही विधि कहा जाता है, वही कम कोड लागू होता है।

टाइप किया गया कोड।@code_typed मैक्रो type inference और inlining के बाद तर्क प्रकारों के किसी विशेष सेट के लिए एक विधि कार्यान्वयन प्रस्तुत करता है। कोड का यह अवतार कम आकार के समान है, लेकिन प्रकार की जानकारी के साथ एनोटेटेड अभिव्यक्तियों के साथ और कुछ सामान्य फ़ंक्शन कॉल उनके कार्यान्वयन के साथ प्रतिस्थापित होते हैं। one(n) को

julia> @code_typed nextfib(123) 
CodeInfo(:(begin 
     a = 1 
     b = 1 # line 3: 
     4: 
     unless (Base.slt_int)(b, n)::Bool goto 13 # line 4: 
     SSAValue(2) = b 
     SSAValue(3) = (Base.add_int)(a, b)::Int64 
     a = SSAValue(2) 
     b = SSAValue(3) 
     11: 
     goto 4 
     13: # line 6: 
     return b 
    end))=>Int64 

कॉल शाब्दिक Int64 मूल्य 1 (अपने सिस्टम पर डिफ़ॉल्ट पूर्णांक प्रकार Int64 है) के साथ प्रतिस्थापित किया गया है: उदाहरण के लिए, यहाँ हमारे उदाहरण समारोह के लिए प्रकार कोड है। अभिव्यक्ति b < n को slt_intintrinsic ("हस्ताक्षर किए गए पूर्णांक से कम") के संदर्भ में इसके कार्यान्वयन के साथ प्रतिस्थापित किया गया है और इसके परिणाम को रिटर्न प्रकार Bool के साथ एनोटेट किया गया है। अभिव्यक्ति a + b को add_int आंतरिक के संदर्भ में इसके क्रियान्वयन के साथ भी बदल दिया गया है और इसके परिणाम प्रकार Int64 के रूप में एनोटेटेड हैं। और पूरे फ़ंक्शन बॉडी का रिटर्न प्रकार Int64 के रूप में एनोटेट किया गया है।

उतारा कोड है, जो निर्धारित करने के लिए कौन सी विधि शरीर कहा जाता है केवल तर्क प्रकार पर निर्भर करता है के विपरीत, टाइप कोड के विवरण तर्क प्रकार पर निर्भर करते हैं:

julia> @code_typed nextfib(Int128(123)) 
CodeInfo(:(begin 
     SSAValue(0) = (Base.sext_int)(Int128, 1)::Int128 
     SSAValue(1) = (Base.sext_int)(Int128, 1)::Int128 
     a = SSAValue(0) 
     b = SSAValue(1) # line 3: 
     6: 
     unless (Base.slt_int)(b, n)::Bool goto 15 # line 4: 
     SSAValue(2) = b 
     SSAValue(3) = (Base.add_int)(a, b)::Int128 
     a = SSAValue(2) 
     b = SSAValue(3) 
     13: 
     goto 6 
     15: # line 6: 
     return b 
    end))=>Int128 

यह एक के लिए nextfib समारोह के टाइप संस्करण है Int128 तर्क। शाब्दिक 1 को Int128 पर विस्तारित किया जाना चाहिए और Int64 के बजाय परिणाम के प्रकार Int128 हैं। यदि टाइप का कार्यान्वयन काफी अलग है तो टाइप किया गया कोड काफी अलग हो सकता है। उदाहरण के लिए BigInts के लिए nextfib सरल "बिट्स प्रकार" के लिए की तुलना में काफी अधिक शामिल है Int64 और Int128 की तरह:

julia> @code_typed nextfib(big(123)) 
CodeInfo(:(begin 
     $(Expr(:inbounds, false)) 
     # meta: location number.jl one 164 
     # meta: location number.jl one 163 
     # meta: location gmp.jl convert 111 
     [email protected]_5 = $(Expr(:invoke, MethodInstance for BigInt(), :(Base.GMP.BigInt))) # line 112: 
     $(Expr(:foreigncall, (:__gmpz_set_si, :libgmp), Void, svec(Ptr{BigInt}, Int64), :(&[email protected]_5), :([email protected]_5), 1, 0)) 
     # meta: pop location 
     # meta: pop location 
     # meta: pop location 
     $(Expr(:inbounds, :pop)) 
     $(Expr(:inbounds, false)) 
     # meta: location number.jl one 164 
     # meta: location number.jl one 163 
     # meta: location gmp.jl convert 111 
     [email protected]_6 = $(Expr(:invoke, MethodInstance for BigInt(), :(Base.GMP.BigInt))) # line 112: 
     $(Expr(:foreigncall, (:__gmpz_set_si, :libgmp), Void, svec(Ptr{BigInt}, Int64), :(&[email protected]_6), :([email protected]_6), 1, 0)) 
     # meta: pop location 
     # meta: pop location 
     # meta: pop location 
     $(Expr(:inbounds, :pop)) 
     a = [email protected]_5 
     b = [email protected]_6 # line 3: 
     26: 
     $(Expr(:inbounds, false)) 
     # meta: location gmp.jl < 516 
     SSAValue(10) = $(Expr(:foreigncall, (:__gmpz_cmp, :libgmp), Int32, svec(Ptr{BigInt}, Ptr{BigInt}), :(&b), :(b), :(&n), :(n))) 
     # meta: pop location 
     $(Expr(:inbounds, :pop)) 
     unless (Base.slt_int)((Base.sext_int)(Int64, SSAValue(10))::Int64, 0)::Bool goto 46 # line 4: 
     SSAValue(2) = b 
     $(Expr(:inbounds, false)) 
     # meta: location gmp.jl + 258 
     [email protected]_7 = $(Expr(:invoke, MethodInstance for BigInt(), :(Base.GMP.BigInt))) # line 259: 
     $(Expr(:foreigncall, ("__gmpz_add", :libgmp), Void, svec(Ptr{BigInt}, Ptr{BigInt}, Ptr{BigInt}), :(&[email protected]_7), :([email protected]_7), :(&a), :(a), :(&b), :(b))) 
     # meta: pop location 
     $(Expr(:inbounds, :pop)) 
     a = SSAValue(2) 
     b = [email protected]_7 
     44: 
     goto 26 
     46: # line 6: 
     return b 
    end))=>BigInt 

इस तथ्य यह है कि BigInts पर कार्रवाई बहुत जटिल हैं और स्मृति आवंटन शामिल दर्शाता है और बाहरी जीएमपी पुस्तकालय के लिए कॉल (libgmp)।

एलएलवीएम आईआर। जूलिया मशीन कोड उत्पन्न करने के लिए LLVM compiler framework का उपयोग करता है। एलएलवीएम एक असेंबली जैसी भाषा को परिभाषित करता है जो इसे विभिन्न कंपाइलर ऑप्टिमाइज़ेशन पास और ढांचे में अन्य टूल्स के बीच साझा intermediate representation (आईआर) के रूप में उपयोग करता है। एलएलवीएम आईआर के तीन आइसोमोर्फिक रूप हैं:

  1. एक बाइनरी प्रतिनिधित्व जो कॉम्पैक्ट और मशीन पठनीय है।
  2. एक पाठपरक प्रतिनिधित्व जो वर्बोज़ और कुछ हद तक मानव पठनीय है।
  3. एलएलवीएम पुस्तकालयों द्वारा उत्पन्न और उपभोग में एक इन-मेमोरी प्रतिनिधित्व।

जूलिया एलएलवीएम के सी ++ एपीआई का उपयोग स्मृति में एलएलवीएम आईआर (फॉर्म 3) बनाने के लिए करता है और फिर उस फॉर्म पर कुछ एलएलवीएम अनुकूलन पास को कॉल करता है। जब आप @code_llvm करते हैं तो आप पीढ़ी के बाद एलएलवीएम आईआर और कुछ उच्च स्तरीय अनुकूलन देखते हैं। यहाँ चल रहे हमारे उदाहरण के लिए LLVM कोड है:

julia> @code_llvm nextfib(123) 

define i64 @julia_nextfib_60009(i64) #0 !dbg !5 { 
top: 
    br label %L4 

L4:            ; preds = %L4, %top 
    %storemerge1 = phi i64 [ 1, %top ], [ %storemerge, %L4 ] 
    %storemerge = phi i64 [ 1, %top ], [ %2, %L4 ] 
    %1 = icmp slt i64 %storemerge, %0 
    %2 = add i64 %storemerge, %storemerge1 
    br i1 %1, label %L4, label %L13 

L13:            ; preds = %L4 
    ret i64 %storemerge 
} 

यह nextfib(123) विधि लागू करने के लिए इन-स्मृति LLVM आईआर की शाब्दिक रूप है। एलएलवीएम पढ़ने में आसान नहीं है - इसका उद्देश्य ज्यादातर लोगों द्वारा लिखित या पढ़ना नहीं है - लेकिन यह पूरी तरह से specified and documented है।एक बार जब आप इसे लटका लेंगे, तो समझना मुश्किल नहीं है। i64 (LLVM के नाम के लिए Int64) मूल्य 1 (उनके मूल्यों को अलग ढंग से प्राप्त कर रहे हैं जब विभिन्न स्थानों से करने के लिए कूद गया - कि क्या phi अनुदेश करता है) यह कोड लेबल L4 लिए कूदता है और "रजिस्टर" %storemerge1 और %storemerge साथ initializes। यह icmp slt%storemerge की तुलना %0 के साथ करता है - जिसमें पूरे विधि निष्पादन के लिए तर्क नहीं है - और तुलना परिणाम %1 में तुलना परिणाम बचाता है। यह add i64%storemerge और %storemerge1 पर करता है और परिणाम %2 पर पंजीकृत करता है। यदि %1 सत्य है, तो यह L4 पर वापस शाखाओं और अन्यथा यह L13 पर शाखाओं। जब कोड L4 पर वापस लौटाता है तो रजिस्टर %storemerge1%storemerge और %storemerge के पिछले मान %2 के पिछले मान प्राप्त करता है।

मूल कोड। चूंकि जूलिया मूल कोड निष्पादित करता है, इसलिए आखिरी रूप में एक विधि कार्यान्वयन होता है जो मशीन वास्तव में निष्पादित करता है। यह स्मृति में बस बाइनरी कोड है, जो पढ़ने के लिए कठिन है, बहुत पहले लोगों ने "असेंबली भाषा" के विभिन्न रूपों का आविष्कार किया था, जो नामों के साथ निर्देशों और रजिस्टरों का प्रतिनिधित्व करते हैं और कुछ निर्देशों को व्यक्त करने में मदद के लिए सरल वाक्यविन्यास हैं। आम तौर पर, असेंबली भाषा मशीन कोड के साथ एक-से-एक पत्राचार के करीब बनी हुई है, विशेष रूप से, कोई भी हमेशा असेंबली कोड में मशीन कोड को "अलग" कर सकता है। यहां हमारा उदाहरण दिया गया है:

julia> @code_native nextfib(123) 
    .section __TEXT,__text,regular,pure_instructions 
Filename: REPL[1] 
    pushq %rbp 
    movq %rsp, %rbp 
    movl $1, %ecx 
    movl $1, %edx 
    nop 
L16: 
    movq %rdx, %rax 
Source line: 4 
    movq %rcx, %rdx 
    addq %rax, %rdx 
    movq %rax, %rcx 
Source line: 3 
    cmpq %rdi, %rax 
    jl L16 
Source line: 6 
    popq %rbp 
    retq 
    nopw %cs:(%rax,%rax) 

यह इंटेल कोर i7 पर है, जो x86_64 CPU परिवार में है। यह केवल मानक पूर्णांक निर्देशों का उपयोग करता है, इसलिए इससे परे कोई फर्क नहीं पड़ता कि आर्किटेक्चर क्या है, लेकिन के विशिष्ट आर्किटेक्चर के आधार पर आप मशीन के विशिष्ट आर्किटेक्चर के आधार पर कुछ कोड प्राप्त कर सकते हैं, क्योंकि जेआईटी कोड अलग-अलग सिस्टम पर अलग हो सकता है। शुरुआत में pushq और movq निर्देश एक मानक फ़ंक्शन प्रीम्बल हैं, जो स्टैक पर रजिस्टरों को सहेजते हैं; इसी तरह, popq रजिस्ट्रार को पुनर्स्थापित करता है और फ़ंक्शन से retq रिटर्न देता है; nopw एक 2-बाइट निर्देश है जो कुछ भी नहीं करता है, केवल फ़ंक्शन की लंबाई पैड करने के लिए शामिल है। तो कोड के मांस सिर्फ यह है:

movl $1, %ecx 
    movl $1, %edx 
    nop 
L16: 
    movq %rdx, %rax 
Source line: 4 
    movq %rcx, %rdx 
    addq %rax, %rdx 
    movq %rax, %rcx 
Source line: 3 
    cmpq %rdi, %rax 
    jl L16 

शीर्ष पर movl निर्देश 1 मूल्यों के साथ रजिस्टर आरंभ कर देगा। movq निर्देश रजिस्टरों के बीच मूल्यों को स्थानांतरित करते हैं और addq निर्देश रजिस्टरों को जोड़ता है। cmpq निर्देश दो रजिस्टरों की तुलना करता है और jl या तो L16 पर वापस कूदता है या फ़ंक्शन से वापस लौटता रहता है। एक तंग लूप में पूर्णांक मशीन निर्देशों का यह मुट्ठी भर ठीक है जो आपके जूलिया फ़ंक्शन कॉल को निष्पादित करता है, जो थोड़ा अधिक सुखद मानव-पठनीय रूप में प्रस्तुत किया जाता है। यह देखना आसान है कि यह तेजी से क्यों चलता है।

यदि आप व्याख्यात्मक कार्यान्वयन की तुलना में सामान्य रूप से जेआईटी संकलन में रुचि रखते हैं, तो एली बेंडरस्की में ब्लॉग पोस्ट की एक बड़ी जोड़ी है, जहां वह एक भाषा के सरल दुभाषिया कार्यान्वयन से (सरल) के लिए जेआईटी अनुकूलित करने के लिए जाता है भाषा:

  1. http://eli.thegreenplace.net/2017/adventures-in-jit-compilation-part-1-an-interpreter/
  2. http://eli.thegreenplace.net/2017/adventures-in-jit-compilation-part-2-an-x64-jit.html
+1

मैं गलती से पोस्ट को हिट इससे पहले कि मैं किया गया था, तो वहाँ आने के लिए अधिक है। – StefanKarpinski

+0

इसके लिए प्रतीक्षा कर रहा है। एक महान स्पष्टीकरण के लिए धन्यवाद।यदि मैं गलत नहीं हूं एलएलवीएम आईआर वह है जिसे एमपीआई द्वारा उपयोग किया जाता है? –

+0

अब हो गया। मुझे यकीन नहीं है कि सवाल करता है। एमपीआई एक पुस्तकालय है - यह आंतरिक कोड प्रतिनिधित्व के किसी भी स्तर का उपयोग नहीं करता है। – StefanKarpinski

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