तेज़ dynamic language interpreter कैसे बनाएँ
(zef-lang.dev)- AST को सीधे traverse करने वाला interpreter भी value representation, inline cache, object model, watchpoint, और बार-बार की गई सूक्ष्म optimization के जरिए बड़ा performance सुधार हासिल कर सकता है
- performance को लगभग नज़रअंदाज़ करके बनाया गया Zef baseline CPython 3.10 से 35 गुना, Lua 5.4.7 से 80 गुना, और QuickJS-ng 0.14.0 से 23 गुना धीमा था, लेकिन 21 चरणों की optimization के बाद 16.646 गुना speedup हासिल किया गया
- सबसे बड़ी छलांग object model के redesign और inline cache के संयोजन से आई, और Storage व Offsets आधारित access, cached AST specialization, तथा name override की निगरानी के लिए watchpoint लागू करने से आगे 4.55 गुना सुधार हुआ
- अतिरिक्त सुधारों में string-आधारित dispatch हटाना, Symbol को शामिल करना, argument passing structure बदलना, getter और setter specialization, hash table fast path, और array literal के साथ
sqrtवtoStringspecialization तक को क्रमिक रूप से लागू किया गया - Yolo-C++ port तक शामिल करने पर baseline की तुलना में 66.962 गुना तेज़ प्रदर्शन दर्ज किया गया; यह CPython 3.10 से 1.889 गुना तेज़ और QuickJS-ng 0.14.0 से 2.968 गुना तेज़ है, लेकिन memory deallocation न होने के कारण लंबे समय तक चलने वाले workload के लिए उपयुक्त नहीं है
परिचय और मूल्यांकन पद्धति
- optimization का लक्ष्य AST को सीधे traverse करने वाला interpreter है, और मज़े के लिए बनाई गई dynamic language Zef को Lua, QuickJS, और CPython के साथ प्रतिस्पर्धा करने लायक स्तर तक ले जाना उद्देश्य है
- JIT compiler या mature GC की सूक्ष्म tuning से अधिक, ऐसी optimization पर ध्यान है जिन्हें बिना मज़बूत बुनियाद वाले शुरुआती implementation पर भी लागू किया जा सके
- जिन तकनीकों पर चर्चा है वे हैं value representation, inline caching, object model, watchpoint, और व्यावहारिक optimization का बार-बार प्रयोग
- लेख में दी गई तकनीकों से ही SSA, GC, bytecode, या machine code के बिना भी बड़ा performance सुधार हासिल किया गया
- लेख के दायरे में 16 गुना speedup
- अधूरे Yolo-C++ port को शामिल करने पर 67 गुना speedup
- performance evaluation के लिए ScriptBench1 benchmark suite का उपयोग किया गया
- शामिल benchmark हैं Richards OS scheduler, DeltaBlue constraint solver, N-Body physics simulation, और Splay binary tree test
- JavaScript, Python, और Lua के मौजूदा port का उपयोग किया गया
- Splay के Python और Lua port Claude से बनाए गए थे
- प्रयोग का वातावरण Ubuntu 22.04.5, Intel Core Ultra 5 135U, 32GB RAM, और Fil-C++ 0.677 था
- Lua 5.4.7 को GCC 11.4.0 से compile किया गया
- QuickJS-ng 0.14.0 के लिए GitHub releases binary का उपयोग किया गया
- CPython 3.10 के लिए Ubuntu का default version इस्तेमाल किया गया
- सभी प्रयोगों में random क्रम में मिलाकर किए गए 30 रन के औसत का उपयोग किया गया
- अधिकांश तुलना Fil-C++ से compile किए गए Zef interpreter और Yolo-C compiler से build किए गए अन्य interpreter के बीच की गई
मूल Zef interpreter
- इसे performance पर लगभग ध्यान दिए बिना लिखा गया था, और स्पष्ट रूप से कहा गया कि performance को ध्यान में रखकर केवल दो ही चुनाव किए गए थे
-
value representation
- 64-bit tagged value का उपयोग
- इसमें रखे जा सकने वाले मान हैं double, 32-bit integer, और
Object*
- इसमें रखे जा सकने वाले मान हैं double, 32-bit integer, और
- double को
0x1000000000000offset तरीके से दर्शाया गया- इसे JavaScriptCore से सीखी गई तकनीक के रूप में बताया गया
- साहित्य में इसे NuN tagging कहा जाता है
- integer और pointer के लिए native representation का उपयोग
- यह इस मान्यता पर निर्भर है कि pointer value
0x100000000से छोटी नहीं होगी - इसे स्वयं एक जोखिमभरा विकल्प बताया गया
- विकल्प के रूप में integer पर
0xffff000000000000upper-bit tag लगाने का उल्लेख किया गया
- यह इस मान्यता पर निर्भर है कि pointer value
- इस representation से number arithmetic में bit test आधारित fast path लागू करना संभव हुआ
- इससे भी अधिक महत्वपूर्ण लाभ था numbers के लिए heap allocation से बचाव
- नया interpreter बनाते समय शुरुआत में सही default value representation चुनना महत्वपूर्ण है, क्योंकि बाद में इसे बदलना बहुत कठिन होता है
- dynamic type language implementation की शुरुआत के लिए 32-bit या 64-bit tagged value का सुझाव दिया गया
- 64-bit tagged value का उपयोग
-
implementation language का चयन
- ऐसी भाषा के रूप में C++ परिवार को चुना गया जिसमें optimization को पर्याप्त रूप से व्यक्त किया जा सके
- स्पष्ट रूप से कहा गया कि Java को low-level optimization की सीमा के कारण नहीं चुना जाएगा
- यह भी बताया गया कि Rust को GC language implementation के लिए आवश्यक global mutable state और circular reference वाले heap representation के कारण नहीं चुना जाएगा
- हालांकि, multi-language setup स्वीकार करने या बहुत सारा
unsafecode अनुमति देने पर Rust के आंशिक या पूर्ण उपयोग की संभावना का उल्लेख किया गया
- हालांकि, multi-language setup स्वीकार करने या बहुत सारा
-
performance engineering के दृष्टिकोण से गलत विकल्प
- Fil-C++ का उपयोग
- इससे तेज़ी से विकास संभव हुआ और GC मुफ्त में मिला
- memory safety violation को diagnostic information और stack trace के साथ रिपोर्ट किया जाता है
- undefined behavior नहीं है
- performance लागत आम तौर पर लगभग 4 गुना है
- recursive AST-walking interpreter
- कई जगह override होने वाली virtual
Node::evaluatemethod संरचना
- कई जगह override होने वाली virtual
- string का अत्यधिक उपयोग
GetAST node variable name बताने वालाstd::stringसंग्रहीत करता है- हर variable access पर उसी string का उपयोग होता है
- hash table का अत्यधिक उपयोग
Getexecute होने पर string key के साथstd::unordered_maplookup किया जाता है
- recursive call chain आधारित scope lookup
- लगभग हर तरह की nesting और closure की अनुमति
- function F के भीतर class A, और class B के भीतर function G जैसी nesting में A की method A field, F local variable, B field, और G local variable देख सकती है
- मूल implementation इसे अलग-अलग scope object पर query करने वाले C++ recursive function के जरिए संभालता था
- Fil-C++ का उपयोग
-
मूल implementation की विशेषताएँ
- गलत विकल्पों के बावजूद कम code में काफ़ी जटिल language interpreter implement करना संभव था
- सबसे बड़ा module parser है
- बाकी हिस्सा अपेक्षाकृत सरल और स्पष्ट है
-
शुरुआती performance
- मूल interpreter CPython 3.10 से 35 गुना धीमा था
-
Lua 5.4.7 से 80 गुना धीमा
- QuickJS-ng 0.14.0 से 23 गुना धीमा
पूरी optimization प्रगति तालिका
- तालिका में Zef Baseline से Zef Change #21: No Asserts, और Zef in Yolo-C++ तक के performance बदलाव को संक्षेप में दिखाया गया है
- comparison column हैं vs Zef Baseline, vs Python 3.10, vs Lua 5.4.7, और vs QuickJS-ng 0.14.0
- अंतिम पंक्ति के अनुसार Zef Change #21: No Asserts baseline की तुलना में 16.646 गुना तेज़ है
-
Python 3.10 से 2.13 गुना धीमा
-
Lua 5.4.7 से 4.781 गुना धीमा
- QuickJS-ng 0.14.0 से 1.355 गुना धीमा
-
-
Zef in Yolo-C++** baseline की तुलना में 66.962 गुना तेज़ है
-
Python 3.10 से 1.889 गुना तेज़
-
Lua 5.4.7 से 1.189 गुना धीमा
- QuickJS-ng 0.14.0 से 2.968 गुना तेज़
-
शुरुआती optimization चरण
-
Optimization #1: operator को सीधे call करना
- parser अब operator को operator नाम वाले
DotCallnode के रूप में नहीं बनाता, बल्कि हर operator के लिए अलग AST node बनाता है - Zef में
a + bऔरa.add(b)एक जैसे हैं- पहले
a + bकोDotCall(a, "add")और argumentbके रूप में parse किया जाता था - हर arithmetic operation पर operator method name string lookup होता था
DotCallstring कोValue::callMethodतक भेजता थाValue::callMethodकई string comparison करता था
- पहले
- बदलाव के बाद parser
Binary<>,Unary<>node बनाता है- template और lambda का उपयोग करके हर operator के लिए अलग
Node::evaluateoverride दिया जाता है - हर node उस operator के लिए संबंधित
Valuefast path को सीधे call करता है - उदाहरण के लिए
a + bमेंBinary<lambda for add>::evaluatecall होता है और फिरValue::addcall होता है
- template और lambda का उपयोग करके हर operator के लिए अलग
- performance पर असर 17.5% सुधार
- इस बिंदु पर performance CPython 3.10 से 30 गुना धीमा है
- Lua 5.4.7 से 67 गुना धीमा
- QuickJS-ng 0.14.0 से 19 गुना धीमा
- parser अब operator को operator नाम वाले
-
Optimization #2: RMW operator को सीधे call करना
- सामान्य operator तेज़ हो गए, लेकिन
a += bजैसे RMW रूप अब भी string-based dispatch इस्तेमाल करते थे - parser को हर RMW case के लिए अलग node बनाने के लिए बदला गया
- parser, LValue node से
makeRMWvirtual call के ज़रिए खुद को RMW से replace करने का अनुरोध करता है - RMW में बदलने वाले LValue हैं Get, Dot, Subscript
- Get variable read
idसे मेल खाता है - Dot
expr.idसे मेल खाता है - Subscript
expr[index]से मेल खाता है
- Get variable read
- हर virtual call में
SPECIALIZE_NEW_RMWmacro इस्तेमाल होता है- SetRMW
id += valueके लिए है - DotSetRMW
expr.id += valueके लिए है - SubscriptRMW
expr[index] += valueके लिए है
- SetRMW
- बदलाव #1 में operator specialization lambda dispatch का उपयोग करती है
- RMW में enum का उपयोग किया गया
get,dot,subscriptतीनों path को संभालना था और enum को कई जगह पास करना पड़ता था, इसलिए यह चुना गया- अंत में
Value::callRMW<>template function असली RMW operator call dispatch करता है
- performance पर असर 3.7% सुधार
- इस बिंदु पर performance CPython 3.10 से 29 गुना धीमा है
- Lua 5.4.7 से 65 गुना धीमा
- QuickJS-ng 0.14.0 से 18.5 गुना धीमा
- शुरुआती बिंदु की तुलना में 1.22 गुना तेज़
- सामान्य operator तेज़ हो गए, लेकिन
-
Optimization #3: IntObject जांच से बचना
- bottleneck यह था कि
Valueका fast pathisInt()इस्तेमाल करता है, और उसके अंदरisIntSlow()Object::isInt()virtual call करता है - मूल value representation में चार मामले थे
- tagged int32
- tagged double
- int32 में represent न हो सकने वाले int64 के लिए IntObject
- और बाकी सभी object
- IntObject के मामले में भी integer method dispatch
Valueही संभालता था- ताकि सभी arithmetic operation implementation एक ही जगह, यानी
Valueमें रहें
- ताकि सभी arithmetic operation implementation एक ही जगह, यानी
- optimization के बाद
Valuefast path सिर्फ int32 और double को देखता है- IntObject handling logic को
IntObjectके अंदर ले जाया गया - हर method dispatch पर होने वाली
isInt()call से बचाव हुआ
- IntObject handling logic को
- performance पर असर 1% सुधार
- इस बिंदु पर performance CPython 3.10 से 29 गुना धीमा है
- Lua 5.4.7 से 65 गुना धीमा
- QuickJS-ng 0.14.0 से 18 गुना धीमा
- शुरुआती बिंदु की तुलना में 1.23 गुना तेज़
- bottleneck यह था कि
-
Optimization #4: Symbol
- मूल interpreter लगभग हर जगह
std::stringका उपयोग करता था - महंगे string उपयोग वाले स्थान थे
Context::get,Context::set,Context::callFunction,Value::callMethod,Value::dot,Value::setDot,Value::callOperator<>,Object::callMethodपरिवार - इस संरचना में यह साधारण hash table lookup नहीं था, बल्कि string key hash table lookup था, इसलिए runtime में string hashing और comparison बार-बार होते थे
- optimization में string-based lookup को hash-consed
Symbolobject pointer से बदला गया - नया
Symbolclass जोड़ा गयाsymbol.h,symbol.cppमें implementation- Symbol और string एक-दूसरे में बदले जा सकते हैं
- string को Symbol में बदलते समय global hash table से hash consing की जाती है
- नतीजतन, सिर्फ
Symbol*pointer identity comparison से यह तय हो जाता है कि symbol वही है या नहीं
- string literal की जगह पहले से तैयार symbol इस्तेमाल किए गए
- उदाहरण के लिए
"subscript"की जगहSymbol::subscript
- उदाहरण के लिए
- कई function signature में
const std::string&की जगहSymbol*इस्तेमाल होने लगा - performance पर असर 18% सुधार
- इस बिंदु पर performance CPython 3.10 से 24 गुना धीमा है
- Lua 5.4.7 से 54 गुना धीमा
- QuickJS-ng 0.14.0 से 15 गुना धीमा
- शुरुआती बिंदु की तुलना में 1.46 गुना तेज़
- मूल interpreter लगभग हर जगह
-
Optimization #5:
Valueinlining- मुख्य बात थी महत्वपूर्ण function की inlining को संभव बनाना
- लगभग सभी बदलावों का केंद्र नया header
valueinlines.hजोड़ना था - इसे
value.hसे अलग header में रखने का कारण यह था कि यह उन headers का उपयोग करता है जिन्हेंvalue.hinclude करना पड़ता है - performance पर असर 2.8% सुधार
- इस बिंदु पर performance CPython 3.10 से 24 गुना धीमा है
- Lua 5.4.7 से 53 गुना धीमा
- QuickJS-ng 0.14.0 से 15 गुना धीमा
- शुरुआती बिंदु की तुलना में 1.5 गुना तेज़
ऑब्जेक्ट मॉडल और कैश संरचना का पुनर्रचना
-
ऑप्टिमाइज़ेशन #6: ऑब्जेक्ट मॉडल, inline cache, Watchpoint
Object,ClassObject,Contextके काम करने के तरीके को बड़े पैमाने पर फिर से बनाया गया, ताकि ऑब्जेक्ट allocation लागत घटे और access के समय hash table lookup से बचा जा सके- यह बदलाव ऑब्जेक्ट मॉडल, inline cache, और watchpoint इन तीन फीचरों के संयोजन से बना है
-
ऑब्जेक्ट मॉडल
- पहले हर lexical scope के लिए एक
Contextऑब्जेक्ट allocate किया जाता था- हर
Contextके पास उस scope के variables रखने वाली एक hash table होती थी
- हर
- ऑब्जेक्ट की संरचना इससे भी अधिक जटिल थी
- हर ऑब्जेक्ट के पास उन classes को
Contextसे map करने वाली hash table होती थी जिनका वह instance होता था
- हर ऑब्जेक्ट के पास उन classes को
- इस तरह की संरचना inheritance और nested scopes की वजह से ज़रूरी थी
- जब
Bar,Fooको inherit करता है, तबBarऔरFooअलग-अलग scopes को close over कर सकते हैं - और एक ही नाम के अलग-अलग private fields भी रख सकते हैं
- जब
- नई संरचना
Storageकी अवधारणा लाती है- डेटा को Offsets के अनुसार store किया जाता है
- offset किस तरह होगा, यह कोई
Contextतय करता है
Contextअब भी मौजूद है, लेकिन ऑब्जेक्ट या scope बनने के समय नहीं, बल्कि AST केresolvepass में पहले से बनाया जाता है- वास्तविक ऑब्जेक्ट या scope बनाते समय, उस
Contextद्वारा निकाले गए आकार के अनुसार सिर्फ Storage allocate किया जाता है
- पहले हर lexical scope के लिए एक
-
inline cache
expr.nameजैसे code location पर, आख़िरी बार देखे गएexprके dynamic type औरnameके resolve हुए आख़िरी offset को याद रखने की तकनीक- यह एक क्लासिक तकनीक है जिसे आमतौर पर JIT संदर्भ में समझाया जाता है, लेकिन यहाँ इसे interpreter पर लागू किया गया है
- याद रखी गई जानकारी को सामान्य AST node के ऊपर specialized AST node को placement construct करने के तरीके से लागू किया गया
-
inline cache के घटक
CacheRecipe- कोई खास access क्या कर रहा था, और क्या वह cache किया जा सकता है, यह track करता है
Context,ClassObject,Packageआदि जगहों परCacheRecipecalls डाले गए- access प्रक्रिया की जानकारी इकट्ठा करने के लिए
Dot::evaluateजैसी AST evaluation functions, अपने द्वारा किए गए polymorphic operation से मिलेCacheRecipeकोthisके साथconstructCache<>को देती हैंconstructCacheCacheRecipeके आधार पर नई AST node specialization compile करता है- template machinery से कई तरह के specialized AST nodes बनते हैं
- अगर local variable access है, तो दिए गए
storageसे direct load करता है - आख़िरी बार देखी गई class से समानता जांचने वाला class check करता है
- उसके बाद आख़िरी बार देखे गए function के लिए direct function call करता है
- ज़रूरत पड़ने पर chain step और watchpoint का संयोजन भी करता है
- cache किए जाने वाले हर AST node के पास अपनी cached variant होती है
- पहले
cacheऑब्जेक्ट के ज़रिए तेज़ call की कोशिश की जाती है cacheऑब्जेक्ट का typeconstructCache<>तय करता है
- पहले
-
watchpoint
- एक उदाहरण दिया गया है जहाँ lexical scope में variable
xहै, उसके भीतर classFooहै, औरFooका methodxको access करता है - अगर
Fooके अंदरxनाम का कोई function या variable नहीं है, तो ऐसा लगता है कि वह सीधे बाहर वालेxको पढ़ सकता है - लेकिन subclass getter
xजोड़ सकता है - उस स्थिति में access का परिणाम बाहर वाला
xनहीं, बल्कि getter होना चाहिए - inline cache इस तरह के बदलाव की संभावना को संभालने के लिए runtime पर
Watchpointसेट करता है - इस उदाहरण में, यह नाम override हुआ है या नहीं इसे monitor करने वाला watchpoint इस्तेमाल किया गया है
- एक उदाहरण दिया गया है जहाँ lexical scope में variable
-
इन तीन फीचरों को साथ में लागू करने की वजह
- सिर्फ नया ऑब्जेक्ट मॉडल होने से, अगर inline cache अच्छी तरह काम न करे, तो अर्थपूर्ण सुधार पाना मुश्किल है
- inline cache भी watchpoint के बिना कई cache conditions को सुरक्षित रूप से संभाल नहीं सकता, इसलिए उसका व्यावहारिक लाभ कम हो जाता है
- नया ऑब्जेक्ट मॉडल और watchpoint को साथ में ठीक से काम करना ही था
-
इम्प्लिमेंटेशन की प्रगति और कठिन हिस्से
- शुरुआत एक साधारण
CacheRecipeसंस्करण लिखने से हुई, और फिर अंतिम रूप के क़रीब Storage, Offsets डिज़ाइन पर काम हुआ - सबसे कठिन कामों में से एक था intrinsic class implementation के तरीके को बदलना
- array उदाहरण
- पहले
ArrayObject::tryCallMethod,Object::tryCallMethodvirtual call को intercept करने के तरीके से सभी methods implement करता था - नए ऑब्जेक्ट मॉडल में
Objectमें न तो vtable है और न virtual methods - उसकी जगह
Object::tryCallMethodobject->classObject()->tryCallMethod(object, ...)को delegate करता है - इसलिए
Arraymethods देने के लिए, उन methods वाली Array की class खुद बनानी पड़ती है
- पहले
- नतीजतन intrinsic functionality का बड़ा हिस्सा, जो पहले implementation में जगह-जगह फैला था, अब
makerootcontext.cppके केंद्र में आ गया - इसे सकारात्मक परिणाम इसलिए माना गया क्योंकि ऑब्जेक्ट की native/intrinsic functions पर भी inline cache वैसे ही लागू हो जाता है
- performance प्रभाव 4.55 गुना सुधार था
- इस बिंदु पर performance CPython 3.10 से 5.2 गुना धीमी थी
- Lua 5.4.7 से 11.7 गुना धीमी थी
- QuickJS-ng 0.14.0 से 3.3 गुना धीमी थी
- शुरुआती बिंदु की तुलना में 6.8 गुना तेज़ थी
- मूल्यांकन यह था कि दूसरे interpreters की तुलना में Fil-C++ का घाटा कुल मिलाकर Fil-C लागत के स्तर तक सिमट गया था
- शुरुआत एक साधारण
कॉल और access path optimization
-
optimization #7: argument passing structure में सुधार
- बदलाव से पहले Zef interpreter function arguments को
const std::optional<std::vector<Value>>&के रूप में पास करता था optionalकी ज़रूरत इसलिए थी क्योंकि कुछ edge cases में नीचे दिए गए दोनों को अलग करना पड़ता थाo.gettero.function()
- Zef में आम तौर पर दोनों ही function call हैं, लेकिन अपवाद के तौर पर नीचे दिया गया कोड मौजूद है
o.NestedClasso.NestedClass()
- पहला
NestedClassobject खुद return करता है - दूसरा instance बनाता है
- इसलिए बिना arguments वाला function call और getter-जैसे call के रूप में empty argument array वाला case अलग करना ज़रूरी है
- लेकिन पुरानी संरचना inefficient थी
- caller
vectorallocate करता था - callee उस vector को copy करके arguments scope फिर से allocate करता था
- caller
- बदलाव के तहत
Argumentstype जोड़ा गया- इसका रूप callee द्वारा बनाए जा रहे arguments scope के बिल्कुल समान है
- अब caller सीधे उसी रूप में allocate करता है
- Yolo-C++ में भी vector backing store
mallocहटने से allocations की संख्या घटी - Fil-C++ में
std::optionalखुद heap allocation करता हैstd::optionalन हो तब भीconst std::vector<>&पास करना भी allocation करता है- जो stack allocation दिखता है, उसे भी heap allocation माना जाता है
- यह भी बताया गया कि caller side vector का size पहले से तय नहीं कर रहा था, इसलिए कई बार reallocation हो रहा था
- बदलाव का बड़ा हिस्सा function signatures को
Arguments*से बदलने का काम था - performance effect 1.33x सुधार
- इस बिंदु पर performance CPython 3.10 से 3.9x धीमी थी
- Lua 5.4.7 से 8.8x धीमी
- QuickJS-ng 0.14.0 से 2.5x धीमी
- शुरुआती बिंदु की तुलना में 9.05x तेज़
- बदलाव से पहले Zef interpreter function arguments को
-
optimization #8: Getter specialization
- Zef, Ruby की तरह, instance fields को default रूप से private रखता है
- उदाहरण
class Foo { my f fn (inF) f = inF }- constructor में मिला मान instance को ही दिखने वाले local variable
fमें store होता है
- constructor में मिला मान instance को ही दिखने वाले local variable
- एक ही type के instances भी दूसरे object के
fतक access नहीं कर सकते- उदाहरण
fn nope(o) o.f println(Foo(42).nope(Foo(666)))nopeके अंदरo.f,oकेfतक access नहीं कर सकता
- उदाहरण
- वजह यह है कि field class member की scope chain में दिखाई देने के तरीके से काम करता है
o.ffield read नहीं बल्किfनाम के method call का अनुरोध है
- इसलिए नीचे दिया गया pattern अक्सर दिखता है
my ffn f f- यानी local variable
freturn करने वालाfनाम का method
- इसके लिए छोटा syntax
readable fहैmy fऔरfn f fका shorthand
- कई method calls असल में getter call होते हैं
- हर getter का AST evaluate करके चलना wasteful है
- optimization है getter specialization
- इसका केंद्र
UserFunctionहै - नए
Node::inferGettermethod से यह infer किया जाता है कि function body simple getter है या नहीं
- इसका केंद्र
- inference rules
Block::inferGetterतभी getter infer करता है जब उसके भीतर की सारी चीज़ें getter के रूप में infer की जा सकेंGet::inferGetterखुद को getter के रूप में infer करता है और load किए जाने वाला offset लौटाता हैContext::tryGetFieldOffsetsतभी non-emptyOffsetsलौटाता है जब getter के चलने वाले lexical scope में वह field पक्का मौजूद होUserFunctionfunction body को getter के रूप में infer किया जा सके तो ज्ञात offset से सीधे read करने वाली specialFunctionsubclass में resolve होता है
- performance effect 5.6% सुधार
- इस बिंदु पर performance CPython 3.10 से 3.7x धीमी थी
- Lua 5.4.7 से 8.3x धीमी
- QuickJS-ng 0.14.0 से 2.4x धीमी
- शुरुआती बिंदु की तुलना में 9.55x तेज़
-
optimization #9: Setter specialization
- setter inference में
fn set_fieldName(newValue) fieldName = newValuepattern matching की ज़रूरत होती है UserFunctionके inference stage में setter के parameter name को पास करना पड़ता हैSetके inference stage में यह जाँचना होता है कि यह ClassObject पर write नहीं है, और साथ ही यह भी कि setter parameter set के source के रूप में इस्तेमाल हो रहा है या नहीं- performance effect 3.4% सुधार
- इस बिंदु पर Zef CPython 3.10 से 3.6x धीमा था
- Lua 5.4.7 से 8x धीमा
- QuickJS-ng 0.14.0 से 2.3x धीमा
- शुरुआती बिंदु की तुलना में 9.87x तेज़
- setter inference में
-
optimization #10:
callMethodका inlining- एक महत्वपूर्ण function को एक पंक्ति के बदलाव से inline किया गया
- performance effect 3.2% सुधार
- इस बिंदु पर Zef CPython 3.10 से 3.5x धीमा था
- Lua 5.4.7 से 7.8x धीमा
- QuickJS-ng 0.14.0 से 2.2x धीमा
- शुरुआती बिंदु की तुलना में 10.2x तेज़
-
optimization #11: hash table
- method call में inline cache miss होने पर
ClassObject::tryCallMethodऔरClassObject::TryCallMethodDirectतक नीचे जाना पड़ता था, और दोनों paths बड़े और जटिल थे - पहले lookup cost hierarchy depth के अनुपात में O(hierarchy depth) थी
- hierarchy की हर class में यह देखने के लिए hash table lookup होता था कि call को member function के रूप में resolve किया जा सकता है या नहीं
- hierarchy की हर class में यह देखने के लिए भी hash table lookup होता था कि call को nested class के रूप में resolve किया जा सकता है या नहीं
- नए बदलाव में receiver class और symbol को key के रूप में इस्तेमाल करने वाली global hash table जोड़ी गई
- एक ही lookup में callee सीधे return हो जाता है
classobject.hमें पूरेtryCallMethodSlowpath पर जाने से पहले इस global table को पहले lookup किया जाता हैclassobject.cppमें सफल lookup result को global table में record किया जाता है- global hash table खुद अपेक्षाकृत simple implementation है
- performance effect 15% सुधार
- इस बिंदु पर Zef CPython 3.10 से 3x धीमा था
- Lua 5.4.7 से 6.8x धीमा
- QuickJS-ng 0.14.0 से 1.9x धीमा
- शुरुआती बिंदु की तुलना में 11.8x तेज़
- method call में inline cache miss होने पर
-
optimization #12:
std::optionalसे बचाव- Fil-C++ में union से जुड़ी compiler pathology के कारण
std::optionalके लिए heap allocation की ज़रूरत पड़ती है - आम तौर पर LLVM union memory access type को ढीले ढंग से संभालता है, लेकिन यह invisicaps के साथ टकराता है
- union के भीतर pointer, programmer के नज़रिए से अनुमान लगाना मुश्किल हो ऐसे तरीके से capability खो सकता है
- नतीजतन Fil-C में programmer की गलती न होने पर भी null capability वाले object dereference पर panic हो सकता है
- इसे कम करने के लिए Fil-C++ compiler union type local variables को संभालते समय LLVM को अधिक conservative तरीके से काम कराने हेतु intrinsics insert करता है
- इसके बाद
FilPizlonatorpass अपनी escape analysis चलाकर union type local variables को register allocation योग्य बनाने की कोशिश करता है- हालांकि यह analysis सामान्य LLVM की SROA analysis जितनी पूर्ण नहीं है
- नतीजतन
std::optionalजैसे union शामिल करने वाले class passing के मामले Fil-C++ में अक्सर memory allocation तक पहुँच जाते हैं - इस बदलाव में hot path में
std::optionalतक जाने वाले code path से बचा गया - performance effect 1.7% सुधार
- इस बिंदु पर Zef CPython 3.10 से 3x धीमा था
- Lua 5.4.7 से 6.65x धीमा
- QuickJS-ng 0.14.0 से 1.9x धीमा
- Fil-C++ में union से जुड़ी compiler pathology के कारण
-
शुरुआती बिंदु की तुलना में 12 गुना तेज़
-
अनुकूलन #13: specialized arguments
- Zef के सभी built-in functions 1 या 2 arguments लेते हैं, और native implementation में इन्हें रखने के लिए
Argumentsobject allocate करने की ज़रूरत नहीं - setter भी हमेशा एक argument लेता है, और अगर setter inference हो चुका हो तो specialized setter implementation भी
Argumentsobject के बिना केवल value argument को सीधे ले लेना पर्याप्त है - इस बदलाव के साथ
ZeroArguments,OneArgument,TwoArgumentsspecialized argument types जोड़े गए- जब callee को इसकी ज़रूरत न हो, तो caller
Argumentsobject allocation से बच सकता है
- जब callee को इसकी ज़रूरत न हो, तो caller
ZeroArgumentsकी ज़रूरत(Arguments*)nullptrसे अलग पहचान के लिए है- पहले
(Arguments*)nullptrका उपयोग getter call के अर्थ में किया जाता था, और वही logic बरकरार रखा गया - अब
ZeroArgumentsका अर्थ है बिना argument वाला function call
- पहले
- कई बदलावों में arguments लेने वाले functions को template बनाना शामिल था
ZeroArguments,OneArgument,TwoArguments,Arguments*में से प्रत्येक के लिए explicit instantiation किया गया- मौजूदा code का बड़ा हिस्सा argument extraction helper के रूप में
Value::getArgइस्तेमाल करता था, और इसमें specialized argument overloads जोड़े गए - arguments इस्तेमाल करने वाले native code में बदलाव अपेक्षाकृत सीधे थे
- प्रदर्शन प्रभाव 3.8% सुधार
- इस बिंदु पर Zef CPython 3.10 से 2.9 गुना धीमा है
- Lua 5.4.7 से 6.4 गुना धीमा है
- QuickJS-ng 0.14.0 से 1.8 गुना धीमा है
- शुरुआती बिंदु की तुलना में 12.4 गुना तेज़
- Zef के सभी built-in functions 1 या 2 arguments लेते हैं, और native implementation में इन्हें रखने के लिए
Fil-C pathologies को बायपास करना और detailed specialization
-
ऑप्टिमाइज़ेशन #14: बेहतर
Valueslow path- एक और Fil-C pathology workaround से बड़ी speedup मिली
- बदलाव से पहले
Valueका out-of-line slow path,Valueका member function था और उसे implicitconst Value*argument चाहिए था - इस संरचना में caller को
Valueको stack पर allocate करना पड़ता था - Fil-C++ में हर stack allocation, heap allocation होती है
- इसलिए slow path को call करने वाला code,
Valueको heap पर allocate करता था
- इसलिए slow path को call करने वाला code,
- बदलाव के बाद इन methods को
staticबनाया गया औरValueको by value pass किया गया- नतीजतन अलग allocation की ज़रूरत नहीं रही
- performance प्रभाव 10% improvement रहा
- इस बिंदु पर Zef, CPython 3.10 से 2.6 गुना धीमा था
- Lua 5.4.7 से 5.8 गुना धीमा
- QuickJS-ng 0.14.0 से 1.65 गुना धीमा
- शुरुआती बिंदु की तुलना में 13.6 गुना तेज़
-
ऑप्टिमाइज़ेशन #15:
DotSetRMWduplication हटाना- कुछ duplicate code हटाया गया
- उम्मीद थी कि
constructCache<>द्वारा specialize होने वाले template functions में machine code reduction फायदेमंद हो सकता है - वास्तविक नतीजा: performance पर कोई असर नहीं
-
ऑप्टिमाइज़ेशन #16:
sqrtspecialization- inline cache call को सही function तक अच्छी तरह route करता है, लेकिन सिर्फ objects पर काम करता है
- non-object मामलों में
Binary<>,Unary<>,Value::callRMW<>fast path इस बात पर निर्भर करते हैं कि receiverintयाdoubleहै या नहीं - यह तरीका सिर्फ parser द्वारा पहचाने जाने वाले operators पर लागू होता है
value.sqrtजैसे रूपों पर लागू नहीं होता
- इस बदलाव से
Dot,value.sqrtके लिए specialize कर सकता है - performance प्रभाव 1.6% improvement रहा
- इस बिंदु पर Zef, CPython 3.10 से 2.6 गुना धीमा था
- Lua 5.4.7 से 5.75 गुना धीमा
- QuickJS-ng 0.14.0 से 1.6 गुना धीमा
- शुरुआती बिंदु की तुलना में 13.8 गुना तेज़
-
ऑप्टिमाइज़ेशन #17:
toStringspecialization- लगभग पिछले optimization जैसी ही विधि से
toStringspecialization लागू की गई - इस बदलाव में
intको string में बदलते समय होने वाली allocation count reduction logic भी शामिल थी - performance प्रभाव 2.7% improvement रहा
- इस बिंदु पर Zef, CPython 3.10 से 2.5 गुना धीमा था
- Lua 5.4.7 से 5.6 गुना धीमा
- QuickJS-ng 0.14.0 से 1.6 गुना धीमा
- शुरुआती बिंदु की तुलना में 14.2 गुना तेज़
- लगभग पिछले optimization जैसी ही विधि से
-
ऑप्टिमाइज़ेशन #18: array literal specialization
my whatever = [1, 2, 3]जैसे code में Zef में array aliasable और mutable होने के कारण नया array allocate करना ज़रूरी है- बदलाव से पहले हर execution पर AST के अंदर उतरकर
1,2,3को हर बार recursively evaluate किया जाता था - इस बदलाव में
ArrayLiteralnode को constant array allocation cases के लिए specialize किया गया - performance प्रभाव 8.1% improvement रहा
- इस बिंदु पर Zef, CPython 3.10 से 2.3 गुना धीमा था
- Lua 5.4.7 से 5.2 गुना धीमा
- QuickJS-ng 0.14.0 से 1.5 गुना धीमा
- शुरुआती बिंदु की तुलना में 15.35 गुना तेज़
-
ऑप्टिमाइज़ेशन #19:
Value::callOperatorमें सुधार- पहले
Valueको reference से pass न करने पर जो speedup मिला था, वही optimizationcallOperatorslow path पर भी लागू किया गया - performance प्रभाव 6.5% improvement रहा
- इस बिंदु पर Zef, CPython 3.10 से 2.2 गुना धीमा था
- Lua 5.4.7 से 4.9 गुना धीमा
- QuickJS-ng 0.14.0 से 1.4 गुना धीमा
- शुरुआती बिंदु की तुलना में 16.3 गुना तेज़
- पहले
-
ऑप्टिमाइज़ेशन #20: बेहतर C++ options
- Fil-C++ में अनावश्यक RTTI और libc++ hardening को disable किया गया
- C++ code में खुद कोई बदलाव नहीं था, सिर्फ build system configuration changes शामिल थे
- performance प्रभाव 1.8% improvement रहा
- इस बिंदु पर Zef, CPython 3.10 से 2.1 गुना धीमा था
- Lua 5.4.7 से 4.8 गुना धीमा
- QuickJS-ng 0.14.0 से 1.35 गुना धीमा
- शुरुआती बिंदु की तुलना में 16.6 गुना तेज़
-
ऑप्टिमाइज़ेशन #21: assert disable करना
- आख़िरी optimization के रूप में assertion को default रूप से disable किया गया
- मौजूदा code, Fil-C-विशिष्ट
ZASSERTmacro का इस्तेमाल करता था- यानी assert हमेशा चलती थी
- बदलाव के बाद अंदरूनी
ASSERTmacro का उपयोग किया गया- assert सिर्फ तब चलती है जब
ASSERTS_ENABLEDसेट हो
- assert सिर्फ तब चलती है जब
- इस बदलाव में code को Yolo-C++ में build होने लायक बनाने वाले अन्य fixes भी शामिल थे
- उम्मीद के उलट कोई speedup नहीं मिली
Yolo-C++ के नतीजे और सीमाएँ
- code को Yolo-C++ से compile करने पर 4 गुना speedup मिली
- लेकिन यह तरीका sound नहीं है और suboptimal है
- sound न होने की वजह यह है कि मौजूदा Fil-C++ GC calls,
calloccalls में बदल जाती हैं - नतीजतन memory free नहीं होती, और काफ़ी देर तक चलने वाले workloads में interpreter memory exhaustion तक पहुँच जाता है
- ScriptBench1 में test duration छोटी होने के कारण memory exhaustion नहीं हुई
- sound न होने की वजह यह है कि मौजूदा Fil-C++ GC calls,
- suboptimal होने की वजह यह है कि असली GC allocator, glibc 2.35 के
callocसे तेज़ है - इसलिए कहा गया कि अगर Yolo-C++ port में real GC जोड़ दी जाए, तो 4 गुना से भी ज़्यादा speedup संभव हो सकती है
- इस प्रयोग में GCC 11.4.0 का उपयोग किया गया
- इस बिंदु पर Zef
-
CPython 3.10 से 1.9 गुना तेज़
-
Lua 5.4.7 से 1.2 गुना धीमा
-
QuickJS-ng 0.14.0 से 3 गुना तेज़
- शुरुआती बिंदु की तुलना में 67 गुना तेज़
-
कच्चा benchmark डेटा
- benchmark के execution time की इकाई सेकंड है
- तालिका में प्रत्येक interpreter के लिए
nbody,splay,richards,deltablue,geomeanशामिल हैं -
Python 3.10
nbody0.0364splay0.8326richards0.0822deltablue0.1135geomean0.1296
-
Lua 5.4.7
nbody0.0142splay0.4393richards0.0217deltablue0.0832geomean0.0577
-
QuickJS-ng 0.14.0
nbody0.0214splay0.7090richards0.7193deltablue0.1585geomean0.2036
-
Zef Baseline
nbody2.9573splay13.0286richards1.9251deltablue5.9997geomean4.5927
-
Zef Change #1: Direct Operators
nbody2.1891splay12.0233richards1.6935deltablue5.2331geomean3.9076
-
Zef Change #2: Direct RMWs
nbody2.0130splay11.9987richards1.6367deltablue5.0994geomean3.7677
-
Zef Change #3: Avoid IntObject
nbody1.9922splay11.8824richards1.6220deltablue5.0646geomean3.7339
-
Zef Change #4: Symbols
nbody1.5782splay9.9577richards1.4116deltablue4.4593geomean3.1533
-
Zef Change #5: Value Inline
nbody1.4982splay9.7723richards1.3890deltablue4.3536geomean3.0671
-
Zef Change #6: Object Model and Inline Caches
nbody0.3884splay3.3609richards0.2321deltablue0.6805geomean0.6736
-
Zef Change #7: Arguments
nbody0.3160splay2.6890richards0.1653deltablue0.4738geomean0.5077
-
Zef Change #8: Getters
nbody0.2988splay2.6919richards0.1564deltablue0.4260geomean0.4809
-
Zef Change #9: Setters
nbody0.2850splay2.6690richards0.1514deltablue0.4072geomean0.4651
-
Zef Change #10:
callMethodinlinenbody0.2533splay2.6711richards0.1513deltablue0.4032geomean0.4506
-
Zef Change #11: Hashtable
nbody0.1796splay2.6528richards0.1379deltablue0.3551geomean0.3906
-
Zef Change #12:
std::optionalसे बचावnbody0.1689splay2.6563richards0.1379deltablue0.3518geomean0.3839
-
Zef Change #13: Specialized Arguments
nbody0.1610splay2.5823richards0.1350deltablue0.3372geomean0.3707
-
Zef Change #14: Improved Value Slow Paths
nbody0.1348splay2.5062richards0.1241deltablue0.3076geomean0.3367
-
Zef Change #15:
DotSetRMW::evaluateका deduplicationnbody0.1342splay2.5047richards0.1256deltablue0.3079geomean0.3375
-
Zef Change #16: Fast
sqrtnbody0.1274splay2.5045richards0.1251deltablue0.3060geomean0.3322
-
Zef Change #17: Fast
toStringnbody0.1282splay2.2664richards0.1275deltablue0.2964geomean0.3235
-
Zef Change #18: Array Literal Specialization
nbody0.1295splay1.6661richards0.1250deltablue0.2979geomean0.2992
-
Zef Change #19: Value
callOperatorOptimizationnbody0.1208splay1.6698richards0.1143deltablue0.2713geomean0.2810
-
Zef Change #20: Better C++ Configuration
nbody0.1186splay1.6521richards0.1127deltablue0.2635geomean0.2760
-
Zef Change #21: No Asserts
nbody0.1194splay1.6504richards0.1127deltablue0.2619geomean0.2759
-
Zef in Yolo-C++
nbody0.0233splay0.3992richards0.0309deltablue0.0784geomean0.0686
अभी कोई टिप्पणी नहीं है.