पायथन पार्स के मानक सीपीथन कार्यान्वयन स्रोत कोड का विश्लेषण करता है और इसके कुछ प्रसंस्करण और सरलीकरण करता है - उर्फ "कम करना" - इसे मशीन के अनुकूल, आसानी से व्याख्या प्रारूप में बदलकर "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_int
intrinsic ("हस्ताक्षर किए गए पूर्णांक से कम") के संदर्भ में इसके कार्यान्वयन के साथ प्रतिस्थापित किया गया है और इसके परिणाम को रिटर्न प्रकार 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 (आईआर) के रूप में उपयोग करता है। एलएलवीएम आईआर के तीन आइसोमोर्फिक रूप हैं:
- एक बाइनरी प्रतिनिधित्व जो कॉम्पैक्ट और मशीन पठनीय है।
- एक पाठपरक प्रतिनिधित्व जो वर्बोज़ और कुछ हद तक मानव पठनीय है।
- एलएलवीएम पुस्तकालयों द्वारा उत्पन्न और उपभोग में एक इन-मेमोरी प्रतिनिधित्व।
जूलिया एलएलवीएम के सी ++ एपीआई का उपयोग स्मृति में एलएलवीएम आईआर (फॉर्म 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
पर वापस कूदता है या फ़ंक्शन से वापस लौटता रहता है। एक तंग लूप में पूर्णांक मशीन निर्देशों का यह मुट्ठी भर ठीक है जो आपके जूलिया फ़ंक्शन कॉल को निष्पादित करता है, जो थोड़ा अधिक सुखद मानव-पठनीय रूप में प्रस्तुत किया जाता है। यह देखना आसान है कि यह तेजी से क्यों चलता है।
यदि आप व्याख्यात्मक कार्यान्वयन की तुलना में सामान्य रूप से जेआईटी संकलन में रुचि रखते हैं, तो एली बेंडरस्की में ब्लॉग पोस्ट की एक बड़ी जोड़ी है, जहां वह एक भाषा के सरल दुभाषिया कार्यान्वयन से (सरल) के लिए जेआईटी अनुकूलित करने के लिए जाता है भाषा:
- http://eli.thegreenplace.net/2017/adventures-in-jit-compilation-part-1-an-interpreter/
- http://eli.thegreenplace.net/2017/adventures-in-jit-compilation-part-2-an-x64-jit.html
मैं गलती से पोस्ट को हिट इससे पहले कि मैं किया गया था, तो वहाँ आने के लिए अधिक है। – StefanKarpinski
इसके लिए प्रतीक्षा कर रहा है। एक महान स्पष्टीकरण के लिए धन्यवाद।यदि मैं गलत नहीं हूं एलएलवीएम आईआर वह है जिसे एमपीआई द्वारा उपयोग किया जाता है? –
अब हो गया। मुझे यकीन नहीं है कि सवाल करता है। एमपीआई एक पुस्तकालय है - यह आंतरिक कोड प्रतिनिधित्व के किसी भी स्तर का उपयोग नहीं करता है। – StefanKarpinski