PostgreSQL के लिए नया JIT कंपाइलर विकसित
(pinaraf.info)- प्रयोगात्मक JIT इंजन pg-copyjit का फोकस LLVM की तुलना में कम generation cost के साथ छोटे PostgreSQL queries के लिए भी “काफ़ी तेज़” कोड बनाना है
- PostgreSQL के cost estimates सीधे वास्तविक execution time से मेल नहीं खाते, इसलिए optimization cost ज़्यादा होने पर LLVM JIT छोटे queries में उल्टा नुकसानदेह हो सकता है
- copy-and-patch तरीका C में बने stencil को पहले से compile करके रखता है, फिर runtime पर ज़रूरी टुकड़ों को copy·patch करके उन्हें नई function की तरह चलाता है
- pg-copyjit PostgreSQL के JIT provider interface के ज़रिए जुड़ता है, और अगर कोई unimplemented opcode मिलता है तो PostgreSQL interpreter पर अपने-आप fallback कर जाता है
- अभी यह PostgreSQL 16 और AMD64 पर काम करने वाले proof of concept चरण में है; code generation सैकड़ों microseconds के स्तर पर है, लेकिन build·documentation·support अभी तैयार नहीं हैं
pg-copyjit किस समस्या को लक्ष्य बनाता है
- pg-copyjit PostgreSQL server को तेज़ बनाने के लिए एक प्रयोगात्मक JIT इंजन है
- मौजूदा कोड production server की तुलना में विशेषज्ञ hackers के प्रयोग के लिए ज़्यादा उपयुक्त है
- अपेक्षित feedback में रोचक प्रयोगात्मक नतीजे, performance improvements के उदाहरण, और implementation ideas शामिल हैं; यह अभी business-critical applications के downtime का जोखिम उठाने वाला चरण नहीं है
PostgreSQL में LLVM JIT बोझिल क्यों हो सकता है
- PostgreSQL में Andres Freund द्वारा जोड़ा गया LLVM-आधारित JIT compiler पहले से मौजूद है
- LLVM efficient code बना सकता है, लेकिन PostgreSQL में इसके उपयोग के तरीके में JIT compilation और optimization की लागत काफ़ी अधिक है
- optimizer का उपयोग न करें तो यह compile न करने से भी बदतर हो सकता है, और optimizer का उपयोग करें तो लागत और बढ़ सकती है
- JIT लागू करना है या नहीं, इसका निर्णय सामान्य query cost estimation से किया जाता है
- PostgreSQL का cost queries के बीच तुलना के लिए एक मान है, वास्तविक execution time नहीं
- cost 100 वाला query 1 सेकंड ले सकता है, और cost 1000 वाला query 100ms में पूरा हो सकता है
- अगर query 10ms तेज़ हो जाए लेकिन optimization में 50ms लग जाएँ, तो कुल execution time के हिसाब से घाटा ही है
- LLVM JIT को अधिक उपयोगी बनाने का एक तरीका compiled queries को cache और reuse करना है, लेकिन इसका implementation कोई छोटा काम नहीं है
copy-and-patch तरीके की संरचना
- copy-and-patch 2021 में एक पेपर के रूप में पेश किया गया JIT compiler निर्माण तरीका है, और इसका उपयोग Python 3.13 JIT इंजन में भी होता है
- इसकी मुख्य इकाई C में लिखा हुआ stencil है
- stencil एक छेदों वाला function है
- इसे clang से पहले ही compile किया जाता है
- gcc support अभी लंबित है
- compilation के समय ज़रूरी stencils को जोड़ा जाता है, छेद भरे जाते हैं, और फिर नए बने “compiled” function पर jump किया जाता है
- मूल flow सरल है
- नए memory region में stencil को copy किया जाता है
- ज़रूरी values patch की जाती हैं
- फिर बने हुए code को execute किया जाता है
- अतिरिक्त optimization की गुंजाइश भी है
- compilation के समय निकाले जा सकने वाले values को पहले से compute करना
- loop को कई stencils में बाँटकर unroll करना
- कई stencils को मिलाकर एक साथ optimize करने वाला meta-stencil बनाना
PostgreSQL से इसे जोड़ने का तरीका
- PostgreSQL का JIT एक extensible provider structure रखता है
.soएक_PG_jit_provider_initfunction देता है, और इसी function में तीन callbacks initialize किए जाते हैंcompile_exprrelease_contextreset_after_error
- मुख्य callback
compile_exprहै- input opcode से बने expression pointer
ExprState*के रूप में आता है - opcodes को इच्छित तरीके से compile किया जाता है
- बने हुए code को executable के रूप में mark किया जाता है
evalfuncको PostgreSQL interpreter की जगह generated code पर बदल दिया जाता है
- input opcode से बने expression pointer
- अगर कोई unimplemented opcode मिलता है, तो PostgreSQL interpreter पर automatic fallback किया जा सकता है
pg-copyjit की compilation प्रक्रिया
- pg-copyjit का copy-and-patch algorithm अभी कुछ छोटे optimizations के साथ एक सरल रूप में है
- हर opcode के लिए compiler stencil collection को देखता है
- अगर उस opcode के लिए stencil है, तो उसे बन रहे code में जोड़ा जाता है
- stencil न हो तो compilation रोक दी जाती है और PostgreSQL interpreter execution संभाल लेता है
- stencil जोड़ने के बाद उसके हर छेद को ज़रूरी value से patch किया जाता है
CONSTopcode stencilopको बाहरीExprEvalStepके रूप में declare करता है, और compiled.ofile मेंopaddress के लिए एक छेद छोड़ा जाता है- stencil collection इस relocation information को सुरक्षित रखता है, और JIT compiler मौजूदा opcode structure का address डालकर executable code बनाता है
- build flow में पहले stencils को एक
.ofile में बनाया जाता है, फिर उससे assembly code और relocation निकालकर उन्हें C में उपयोग होने वाली structures में बदला जाता है
implementation की स्थिति और performance
- शुरुआत में assembly code को हाथ से निकालकर
SELECT 42;के लिए ज़रूरी 3 opcodes को चलाया गया - इसके बाद assembly code extraction को automate करने के लिए DirtyPython script लिखी गई, और कुछ घंटों में ये सुविधाएँ जोड़ी गईं
- function calls
- single-table queries
- अधिक जटिल data types
- कुछ optimizations
- अभी पुष्टि की गई स्थिति इस प्रकार है
- यह PostgreSQL 16 पर काम करता है
- अनुमान है कि पुराने releases पर भी ठीक रहेगा, लेकिन पुष्टि किया गया environment PostgreSQL 16 ही है
- supported architecture केवल AMD64 है
- ARM64 support जोड़ने की योजना है
- POWER64 और S390x जैसे targets में भी रुचि है, लेकिन इसके लिए compiler patches और उन machines तक access की ज़रूरत पड़ सकती है
- performance numbers अभी लगभग बिना optimization की स्थिति के हैं
- code generation सैकड़ों microseconds में पूरा हो जाता है
- यह छोटे queries में भी उपयोग योग्य स्तर है
SELECT 42;के आधार पर no JIT में 0.3ms, copyjit में 0.6ms, बिना optimization वाले LLVM में 1.6ms, और optimized LLVM में 6.6ms लगते हैं
- LLVM बहुत तेज़ code बना सकता है, लेकिन pg-copyjit का लक्ष्य जल्दी काफ़ी तेज़ code बनाना है, इसलिए दोनों tools की सीधी तुलना आसान नहीं है
- एक साधारण non-indexed 90k rows table पर दो queries का benchmark किया गया, और CPU काम वाले
whereclause में interpreter की तुलना में performance बेहतर हुई - benchmark laptop पर किया गया था, इसलिए उसकी reliability सीमित है; आगे desktop पर अधिक उपयुक्त benchmark करने की योजना है
- implemented opcodes अभी कम होने पर भी किसी भी query को चलाया जा सकता है
- JIT engine unimplemented हिस्सों के लिए message देता है
- वास्तविक execution interpreter संभालता है
code release और बाकी काम
- code GitHub के pg-copyjit पर सार्वजनिक है
- अभी focus git history को साफ़ करने या documentation पर नहीं, बल्कि code पर ही है
- build करने के लिए
build-stencils.shfile को पहले manually चलाना पड़ता है - मौजूदा स्थिति में support देना संभव नहीं है, इसलिए documentation भी अभी नहीं की गई है
- बचे हुए काम काफ़ी स्पष्ट हैं
- और अधिक opcodes implement करना
- optimizations की खोज
- build convenience में सुधार
- packaging योग्य स्थिति तक व्यवस्थित करना
- build script अभी Debian और PostgreSQL 16 के लिए विशेष रूप से तैयार है
संभावित उपयोग और architecture विस्तार
- लक्ष्य इसे सुरक्षित रूप से package करके अपनी production server पर deploy करने लायक स्तर तक पहुँचाना है
- server के हिसाब से अलग-अलग JIT उपयोग करने का विचार भी है
- GIS server जैसे स्थान, जहाँ queries optimization cost को जायज़ ठहरा सकती हैं, वहाँ LLVM JIT का उपयोग
- web application database, जहाँ छोटे query time महत्वपूर्ण हैं, वहाँ pg-copyjit का उपयोग
- दूसरी architectures पर port करना भी एक गंभीर लक्ष्य है
- Alpha, Itanium, Sparc, M68k जैसी विविध architectures के दौर को याद करते हुए, एकल architecture-केंद्रित monoculture समस्या का हिस्सा न बनने की सोच रखी गई है
1 टिप्पणियां
Hacker News की रायें
JIT compiler को सीधे memory addresses inject करने पड़ते हैं, इसलिए generated code उस query और process से बंध जाता है
execution plans भी अलग-अलग processes में share करने के लिए design की गई memory data structure नहीं हैं, इसलिए वही समस्या आती है
MSSQL जैसे DB single-process architecture में threads इस्तेमाल करते हैं, इसलिए यह समस्या नहीं होती; यही वजह भी है कि वे external pooler के बिना ज़्यादा concurrent connections handle कर पाते हैं
MSSQL execution plans को process से न बंधे हुए representation में serialize करके DB में store भी कर सकता है, जिससे execution plan pinning जैसी features में उनका उपयोग हो सकता है
पारंपरिक compilation भी C के बजाय किसी दूसरी intermediate language का इस्तेमाल करने वाले “copy-and-patch” जैसा दिखता है
जैसे कौन-से passes, कौन-सी backend optimizations इस्तेमाल हो रही हैं वगैरह
हमारे अनुभव [1] के अनुसार, optimizations बंद करके और
-O0backend pipeline के साथ compile time के हिसाब से tune करने पर LLVM भी काफ़ी तेज़ हो सकता है, लेकिन दूसरे approaches से अभी भी 10–20 गुना धीमा रहता हैसाथ ही हमारा अनुभव है कि copy-and-patch से generated code की execution speed काफ़ी धीमी होती है और उसे optimize करना मुश्किल होता है। हमने कुछ चीज़ें आज़माईं, लेकिन [2; Sec. 5] के बाद भी अंतर अभी बड़ा है। database evaluation results के लिए Fig. 3 देखें
LLVM की तुलना में execution time degradation के आँकड़े हैं या नहीं, और जल्दी compile किए गए code से LLVM optimized code पर dynamically switch करने वाले multi-tier JIT को implement करने की योजना है या नहीं, यह भी जानना चाहता हूँ
[1]: https://home.in.tum.de/~engelke/pubs/2403-cgo.pdf
[2]: https://home.in.tum.de/~engelke/pubs/2403-cc.pdf
2010 के आसपास programming, खासकर interpreters सीखते समय, मुझे लगा था कि यह बात अच्छी तरह जानी जाती है कि सावधानी से इस्तेमाल किया जाए तो compiler द्वारा बनाए गए executable code fragments को
memcpyकिया जा सकता हैउस समय बड़ा trap यह था कि NX bit बस popular होना शुरू हुआ था। Linux में भी ज़्यादातर लोग अभी भी 32-bit distributions को default मानते थे, और कुछ लोग इस बात से हैरान होते थे कि CPU 64-bit support करता है
बाद में मैंने एक netbook भी इस्तेमाल किया था जो 64-bit code को बिल्कुल support नहीं करता था
अफ़सोस, बाकी code पर बहुत ज़्यादा समय लग गया, इसलिए मैं इसे पर्याप्त गहराई से explore करके कुछ usable नहीं बना पाया
अफ़सोस, talk proposals की deadline खत्म हो चुकी है, लेकिन “unconference” वाला हिस्सा है। हालांकि topics venue पर तय होते हैं, इसलिए guarantee नहीं है
cache आने पर अक्सर चलने वाली queries cache हो सकती हैं और ज़्यादा aggressively optimize की जा सकती हैं, ताकि compilation cost की भरपाई हो; इस तरह दोनों approaches अच्छी तरह जुड़ते दिखते हैं
बेशक, यह अपने-आप में पूरी तरह नई complexity और headaches जोड़ देगा
https://www.postgresql.org/docs/current/sql-prepare.html के “notes” देखें
बहुत सरल करके कहें तो execution engine के कई हिस्सों में query के parts की ओर point करने वाले pointers leak हो रहे हैं
इन्हें हटाने के लिए execution engine, planner और शायद बाकी चीज़ों तक में काफ़ी बड़े स्तर का overhaul चाहिए
एक ही session के अंदर भी इसी वजह से दो compiled queries के compiled code अलग-अलग होंगे। LLVM और मेरा copyjit, दोनों को कई structs के addresses assembly code में inject करने पड़ते हैं
आम तौर पर manually जाकर hints जोड़ने या execution plan force करने पड़ते थे
SELECT * FROM t WHERE x = TRUE;जैसी simple query भी table मेंxvalues के distribution के आधार पर nightmare बन सकती थीPostgres में मुझे ऐसी समस्याएँ लगभग नहीं आईं, लेकिन यह मानता हूँ कि मैंने prepared statements के साथ Postgres इस्तेमाल नहीं किया है
मैंने ऐसी queries देखी हैं जिनका planning time धीमा था (100ms से ज़्यादा), इसलिए cache उपयोगी हो सकता था, लेकिन याद नहीं कि वाकई optimization ज़रूरी पड़ी हो
यह यहाँ पेश किए गए stencils जैसा है, लेकिन code आमतौर पर single instance होता था, इसलिए copies बनाना rare था
उदाहरण के लिए DOS पर Doom ने भी ऐसी optimization technique इस्तेमाल की थी। पुराने CPUs पर tight rendering loop से पर्याप्त performance निकालने के लिए यह ज़रूरी था