ऑब्जेक्ट हायड्रेशन (Object hydration) ही समस्या सुटली आहे असे वाटते, जोपर्यंत तुम्ही त्याचे मोजमाप करत नाही. तुम्ही डेटाबेसमधून एक रो (row) मिळवता, ॲरेचे ऑब्जेक्टमध्ये रूपांतर करता आणि पुढे जाता. आपल्यापैकी बहुतेक लोक याकडे 'प्लंबिंग' प्रमाणे पाहतात—जे अदृश्य आहे, कंटाळवाणे आहे आणि पुरेसे जलद आहे असे मानले जाते. मग एके दिवशी तुम्ही एखादे बॅच जॉब किंवा क्यू वर्कर (queue worker) प्रोफाईल करता आणि लक्षात येते की CPU वेळेचा एक मोठा हिस्सा मॅपरमध्ये (mapper) वाया जात आहे. याच जाणिवेमुळे HydraType ची निर्मिती झाली, जो एका ठाम नियमावर आधारित हायड्रेटर आहे: एखाद्या क्लासने अशा वैशिष्ट्यांसाठी (features) किंमत मोजू नये ज्याचा त्याच्या प्रॉपर्टीजमध्ये वापर होत नाही.

जर एखादे फील्ड साधे स्ट्रिंग असेल ज्याला कोणत्याही ट्रान्सफॉर्मेशनची गरज नाही, तर जनरेट झालेला कोड असा असावा जणू तो एखाद्या माणसाने स्वतः हाताने लिहिला आहे. थेट असाइनमेंट (Direct assignment). कोणतीही लूप्स, पाइपलाइन्स किंवा रिफ्लेक्शन नाही.

हायड्रेशनसाठी प्रत्यक्षात किती खर्च येतो

पहिली नजर टाकली तर, ['name' => 'Alice', 'age' => 30] चे User ऑब्जेक्टमध्ये रूपांतर करणे अगदी साधे वाटते. अडचण तेव्हा सुरू होते जेव्हा तुम्हाला लवचिकता (flexibility) हवी असते. बहुतेक सामान्य-उद्देशीय हायड्रेटर्स रनटाइममध्ये क्लासेस तपासण्यासाठी रिफ्लेक्शनवर (reflection) अवलंबून असतात. ते मेटाडेटा मॅप्स तयार करतात, टाईप कोअरशन (type coercion) हाताळतात आणि नेस्टेड ऑब्जेक्ट ग्राफ्स सोडवतात. त्यांना पाइपलाइन्स देखील खूप आवडतात. प्रत्येक व्हॅल्यू म्यूटेटर्स, अ‍ॅसर्शन्स आणि ट्रान्सफॉर्मर्सच्या एका क्रमाने जाते, जरी तो क्रम रिकामा असला तरीही. लूप स्ट्रक्चरमुळे स्वतःचा एक अतिरिक्त भार (overhead) वाढतो.

तुम्ही काही डझन रेकॉर्ड्स मिळवले तर तुम्हाला कधीच लक्षात येणार नाही. परंतु आधुनिक PHP ॲप्लिकेशन्स नियमितपणे हजारो क्यू मेसेजेस प्रोसेस करतात, मोठ्या CSV संचांची आयात करतात किंवा Elasticsearch मधून डीप रिझल्ट सेट्स हायड्रेट करतात. अशा परिस्थितीत, प्रत्येक फील्डसाठी काही मायक्रोसेकंद्स खर्च करणारा मॅपर खरोखरच अडथळा (bottleneck) बनतो. आठ फील्ड्स असलेले हजार ऑब्जेक्ट्स म्हणजे असा एक अतिरिक्त भार (tax) आहे जो तुम्हाला प्रत्यक्ष जाणवतो.

झिरो-ओव्हरहेड तत्त्वज्ञान (The Zero-Overhead Philosophy)

HydraType च्यामागील मुख्य कल्पना म्हणजे 'फीचर आयसोलेशन' (feature isolation) आहे. टाईप कन्व्हर्जन, नेस्टेड ऑब्जेक्ट हायड्रेशन आणि अ‍ॅसर्शन्स या सर्वांना सपोर्ट आहे, तरीही त्यापैकी कोणतेही अनिवार्य नाही. जर एखाद्या प्रॉपर्टीला ॲरेमधून ऑब्जेक्टमध्ये थेट कॉपी करण्याशिवाय कशाचीही गरज नसेल, तर जनरेट केलेला रायटर नेमके तेच करतो. तिथे कोणताही सामायिक पाइपलाइन नाही जो एखाद्या साध्या स्केलर फील्डला अशा व्हॅलिडेटर्सच्या रांगेत उभे राहण्यास भाग पाडेल ज्याची त्याला गरज नाही.

हे ऐकायला जितके सोपे वाटते, तितके ते साध्य करणे कठीण आहे. अनेक लायब्ररी प्रत्येक फील्डसाठी एकच एक्झिक्यूशन पाथ वापरतात कारण यामुळे कोडबेस लहान आणि एकसमान राहतो. HydraType याच्या उलट दृष्टिकोन घेते. ते प्रत्येक टार्गेट DTO साठी एक युनिक PHP क्लास जनरेट करते, ज्यामध्ये केवळ त्या विशिष्ट क्लासला आवश्यक असलेल्या ऑपरेशन्सचा समावेश असतो. जटिलता ही केवळ गरजेनुसार निवडण्यासारखी (opt-in) असते, प्रत्येक प्रॉपर्टीवर लादलेला अनिवार्य कर (blanket tax) नाही.

वेग कसा मिळतो

चार ठोस निवडी HydraType ला सामान्य रिफ्लेक्शन-आधारित टूल्स आणि इतर जनरेटेड पद्धतींपासून वेगळे करतात.

1. कोड एकदाच जनरेट करा, कायमस्वरूपी वापरा

HydraType तुमचा टार्गेट क्लास फक्त एकदा तपासते आणि त्यानंतर त्याच्या अचूक आकारासाठी तयार केलेला एक समर्पित PHP क्लास लिहिते. जर तुमच्या DTO मध्ये पब्लिक स्ट्रिंग $name, इंट $status आणि प्रायव्हेट DateTime $createdAt असेल, तर जनरेट केलेला रायटर हे सर्व आधीच जाणून घेतो. रनटाइम दरम्यान कोणतीही वारंवार रिफ्लेक्शन, स्ट्रिंग पार्सिंग किंवा लेझी मेटाडेटा बिल्डिंग होत नाही. एकदा का OPCache ने जनरेट केलेली फाईल कंपाईल केली की, हा हायड्रेटर हाताने लिहिलेल्या PHP कोडपेक्षा वेगळा ओळखता येत नाही.

2. रिकाम्या पाइपलाइन्स काढून टाका

बहुतेक हायड्रेटर्स त्यांचे काम रनटाइम पाइपलाइनभोवती रचतात. ते कॉलॅबल्सचा (callables)—म्यूटेटर्स, व्हॅलिडेटर्स, ट्रान्सफॉर्मर्स—एक ॲरे ठेवतात आणि प्रत्येक व्हॅल्यूवर लूप फिरवतात. जरी ते ॲरे रिकामे असले तरीही, foreach किंवा array_reduce तरीही एक्झिक्युट होते. HydraType हे पूर्णपणे काढून टाकते. कोड जनरेशन दरम्यान ते तपासते की प्रॉपर्टीला प्रत्यक्षात कशाची गरज आहे. जर ऑपरेशन्सची यादी रिकामी असेल, तर ते थेट असाइनमेंट देते जसे की $object->name = $data['name'];. रनटाइममध्ये काहीही लूप फिरवले जात नाही, कारण जनरेटरने आधीच सर्व विचारपूर्वक प्रक्रिया पूर्ण केलेली असते.

3. रिफ्लेक्शन रायटिंग टाळण्यासाठी क्लोजर स्कोपिंगचा (Closure Scoping) वापर करा

बाहेरच्या मॅपर्ससाठी प्रायव्हेट प्रॉपर्टीज हे एक मोठे डोकेदुखीचे कारण असतात. सामान्य उपाय म्हणून ReflectionProperty::setValue() वापरले जाते, परंतु थेट प्रॉपर्टी ॲक्सेसच्या तुलनेत त्या पद्धतीचा खर्च खूप जास्त असतो. HydraType Closure::bind वापरून यावर मात करते. जनरेट केलेला रायटर टार्गेट क्लासच्या स्कोपमधील क्लोजर्स वापरतो, ज्यामुळे त्यांना रिफ्लेक्शनशिवाय थेट प्रायव्हेट आणि प्रोटेक्टेड प्रॉपर्टीज वापरता येतात. हे बाइंडिंग कन्स्ट्रक्शन दरम्यान एकदाच होते; त्यानंतर, क्लोजर जवळजवळ नेटिव्ह वेगाने चालते.

4. रायटरचा (Writer) पुनर्वापर करा

काही लायब्ररी प्रत्येक हायड्रेट केलेल्या ऑब्जेक्टसाठी एक नवीन स्ट्रॅटेजी किंवा रिफ्लेक्शन ग्राफ तयार करतात. HydraType त्याचा रायटर एकदाच तयार करते आणि त्यानंतर हजारो ऑब्जेक्ट्ससाठी त्याच इन्स्टन्सचा पुनर्वापर करते. प्रति-ऑब्जेक्ट खर्च कमी होऊन केवळ प्रॉपर्टी व्हॅल्यू सेट करणे आणि रिझल्ट परत करणे इतकाच राहतो.

आकडेवारी (The Numbers)

PHP 8.2 वर, हा फरक अत्यंत स्पष्ट आहे:

  • HydraType: 267.9 ns
  • Ocramius GeneratedHydrator: 307.9 ns
  • Symfony PropertyNormalizer: 8,499.0 ns
  • Valinor: 10,755.2 ns

Symfony PropertyNormalizer आणि Valinor ही शक्तिशाली साधने आहेत, परंतु ती सर्वसाधारण (generalists) आहेत. PropertyNormalizer हा एका serializer component चा भाग आहे जो format detection, normalizers आणि deep object graphs हाताळतो. Valinor हे type trees आणि coercion च्या बाबतीत अत्यंत काटेकोर आहे. या शक्तीसोबतच त्याची किंमतही चुकवावी लागते. या चाचणीमध्ये, ते HydraType पेक्षा साधारणतः तीस ते चाळीस पटीने संथ होते. अगदी Ocramius GeneratedHydrator देखील, जे आधीच code generation वापरते, pipelines आणि property access च्या आर्किटेक्चरल फरकांमुळे थोडे मागे पडते.

संदर्भ महत्त्वाचा असतो. एक सिंगल डेटाबेस क्वेरी किंवा एक HTTP roundtrip या सर्व संख्यांना फिकट पाडते. जर तुम्ही एखाद्या क्वेरीनंतर फक्त तीन ऑब्जेक्ट्स hydrate करत असाल, तर नॅनोसेकंदच्या मागे लागणे म्हणजे ऊर्जेचा अपव्यय आहे. जेव्हा तुम्ही स्केल करता, तेव्हा हा फरक अर्थपूर्ण ठरतो. पन्नास हजार रेकॉर्ड्स hydrate करणारी बॅच प्रोसेस, किंवा cache arrays मधून ऑब्जेक्ट्स पुन्हा तयार करणारा queue consumer, त्यांना 267 नॅनोसेकंद आणि १० मायक्रोसेकंदमधील फरक जाणवेल. फील्ड्स आणि रो (rows) मध्ये याचा गुणाकार केल्यास, वेगवान mapper कोणत्याही business logic मध्ये बदल न करता कामाचा वेळ काही सेकंद कमी करू शकतो.

मुख्य निष्कर्ष

येथून शिकण्यासारखी गोष्ट ही नाही की प्रत्येक प्रोजेक्टला कस्टम hydrator ची गरज आहे. तर खरी गोष्ट ही आहे की आर्किटेक्चरचे निर्णय एकत्रितपणे परिणाम करतात. फक्त तुम्ही मागितलेल्या गोष्टींचा समावेश असलेला कोड जनरेट करून, HydraType साध्या (plain) properties ला वेगवान ठेवते आणि तरीही जटिल (complex) गोष्टींसाठी पर्याय उपलब्ध करून देते. Type conversion आणि nested objects तेव्हाच उपलब्ध होतात जेव्हा तुम्हाला त्यांची गरज असते, परंतु ते ऑब्जेक्टवरील प्रत्येक scalar string साठी परफॉर्मन्सवर परिणाम करत नाहीत.

साधने बदलण्यापूर्वी, प्रोफाइलिंग करा. जर तुमच्या रिअल-वर्ल्ड ट्रेसमध्ये hydration चा प्रभाव दिसत नसेल, तर दुरुस्त करण्यासाठी काहीही नाही. परंतु जर तुम्ही मोठ्या डेटासेटला generic mappers द्वारे पाठवत असाल आणि CPU चा वापर वाढत असल्याचे पाहत असाल, तर लक्षात ठेवा की साधे PHP assignment अजूनही अस्तित्वात आहे. कधीकधी सर्वात वेगवान कोड म्हणजे तोच असतो जो तुम्ही स्वतः लिहिला असता, जो एकदा जनरेट केला जातो आणि नंतर विसरला जातो.