विषय सूची (TABLE OF CONTENTS)
The RareSkills Book of Gas Optimization
- गैस ऑप्टिमाइज़ेशन ट्रिक्स हमेशा काम नहीं करती हैं
- जटिलता (complexity) और पठनीयता (readability) से सावधान रहें
- यहाँ प्रत्येक विषय पर विस्तार से चर्चा संभव नहीं है
- हम एप्लिकेशन-विशिष्ट ट्रिक्स पर चर्चा नहीं करते हैं
- 1. सबसे महत्वपूर्ण: जहाँ संभव हो शून्य से एक (zero to one) स्टोरेज राइट्स से बचें
- 2. स्टोरेज वेरिएबल्स को कैश (Cache) करें: स्टोरेज वेरिएबल्स को ठीक एक बार लिखें और पढ़ें
- 3. संबंधित वेरिएबल्स को पैक करें
- 4. Structs को पैक करें
- 5. Strings को 32 बाइट्स से छोटा रखें
- 6. जो वेरिएबल्स कभी अपडेट नहीं होते हैं, उन्हें immutable या constant होना चाहिए
- 7. लेंथ चेक्स (length checks) से बचने के लिए arrays के बजाय mappings का उपयोग करना
- 8. अनावश्यक लेंथ चेक्स से बचने के लिए arrays पर unsafeAccess का उपयोग करना
- 9. जब बड़ी संख्या में booleans का उपयोग किया जाता है तो bools के बजाय bitmaps का उपयोग करें
- 10. बहुत सारा डेटा स्टोर करने के लिए SSTORE2 या SSTORE3 का उपयोग करें
- 11. जहाँ उपयुक्त हो memory के बजाय storage pointers का उपयोग करें
- 12. ERC20 टोकन बैलेंस को शून्य होने से बचाएं, हमेशा एक छोटी राशि रखें
- 13. शून्य से n तक गिनने के बजाय n से शून्य तक गिनें
- 14. Timestamps और block numbers को uint256 होने की आवश्यकता नहीं है
डिप्लॉयमेंट पर गैस बचाना (Saving Gas On Deployment)
- 1. अन्योन्याश्रित (interdependent) स्मार्ट कॉन्ट्रैक्ट्स के एड्रेस की भविष्यवाणी करने के लिए अकाउंट nonce का उपयोग करें, जिससे स्टोरेज वेरिएबल्स और एड्रेस सेटर फ़ंक्शन्स से बचा जा सके
- 2. Constructors को payable बनाएं
- 3. अधिक शून्य (zeros) रखने के लिए IPFS हैश को अनुकूलित करके (या --no-cbor-metadata कंपाइलर विकल्प का उपयोग करके) डिप्लॉयमेंट साइज़ को कम किया जा सकता है
- 4. यदि कॉन्ट्रैक्ट का केवल एक बार उपयोग होना है तो constructor में selfdestruct का उपयोग करें
- 5. Internal functions और modifiers के बीच चयन करते समय ट्रेड-ऑफ़ को समझें
- 6. बहुत समान स्मार्ट कॉन्ट्रैक्ट्स जो अक्सर कॉल नहीं किए जाते हैं, उन्हें डिप्लॉय करते समय clones या metaproxies का उपयोग करें
- 7. एडमिन फ़ंक्शन्स payable हो सकते हैं
- 8. Custom errors (आमतौर पर) require स्टेटमेंट्स से छोटे होते हैं
- 9. अपना स्वयं का डिप्लॉय करने के बजाय मौजूदा create2 फ़ैक्टरीज़ का उपयोग करें
क्रॉस कॉन्ट्रैक्ट कॉल्स (Cross contract calls)
- 1. डेस्टिनेशन स्मार्ट कॉन्ट्रैक्ट से ट्रांसफर शुरू करने के बजाय टोकन के लिए transfer hooks का उपयोग करें
- 2. Ether ट्रांसफर करते समय deposit() के बजाय fallback या receive का उपयोग करें
- 3. स्टोरेज स्लॉट्स को प्री-वार्म (pre-warm) करने के लिए क्रॉस-कॉन्ट्रैक्ट कॉल करते समय ERC2930 एक्सेस लिस्ट ट्रांज़ैक्शन का उपयोग करें
- 4. बाहरी कॉन्ट्रैक्ट्स की कॉल्स को कैश (Cache) करें जहाँ इसका अर्थ बनता हो (जैसे chainlink oracle से रिटर्न डेटा को कैश करना)
- 5. राउटर जैसे कॉन्ट्रैक्ट्स में multicall लागू करें
- 6. आर्किटेक्चर को मोनोलिथिक (monolithic) बनाकर कॉन्ट्रैक्ट कॉल्स से बचें
डिज़ाइन पैटर्न (Design Patterns)
- 1. ट्रांज़ैक्शन्स को बैच करने के लिए multidelegatecall का उपयोग करें
- 2. Allowlists और airdrops के लिए merkle trees के बजाय ECDSA signatures का उपयोग करें
- 3. अप्रूवल और ट्रांसफर स्टेप को एक ही ट्रांज़ैक्शन में बैच करने के लिए ERC20Permit का उपयोग करें
- 4. गेम्स या अन्य उच्च-थ्रूपुट, कम ट्रांज़ैक्शन वैल्यू वाले एप्लिकेशन्स के लिए L2 मैसेज पासिंग का उपयोग करें
- 5. यदि लागू हो तो state-channels का उपयोग करें
- 6. गैस बचाने के उपाय के रूप में वोटिंग डेलिगेशन (voting delegation) का उपयोग करें
- 7. ERC1155, ERC721 की तुलना में एक सस्ता नॉन-फंजिबल टोकन है
- 8. कई ERC20 टोकन्स के बजाय एक ERC1155 या ERC6909 टोकन का उपयोग करें
- 9. UUPS अपग्रेड पैटर्न यूज़र्स के लिए Transparent Upgradeable Proxy की तुलना में अधिक गैस कुशल है
- 10. OpenZeppelin के विकल्पों का उपयोग करने पर विचार करें
- 1. वैनिटी एड्रेस (vanity addresses) का उपयोग करें (सुरक्षित रूप से!)
- 2. यदि संभव हो तो calldata में signed integers से बचें
- 3. Calldata (आमतौर पर) memory से सस्ता होता है
- 4. Calldata को पैक करने पर विचार करें, विशेषकर L2 पर
- 1. एरर मैसेज के साथ revert करने के लिए assembly का उपयोग करना
- 2. इंटरफ़ेस के माध्यम से फ़ंक्शन्स को कॉल करने पर memory एक्सपेंशन लागत आती है, इसलिए memory में पहले से मौजूद डेटा का पुन: उपयोग करने के लिए assembly का उपयोग करें
- 3. सामान्य गणित ऑपरेशन्स, जैसे min और max के गैस कुशल विकल्प मौजूद हैं
- 4. असमानता की जांच करने के लिए ISZERO(EQ()) के बजाय SUB या XOR का उपयोग करें (कुछ परिदृश्यों में अधिक कुशल)
- 5. address(0) की जांच के लिए inline assembly का उपयोग करें
- 6. selfbalance, address(this).balance से सस्ता है (कुछ परिदृश्यों में अधिक कुशल)
- 7. 96 बाइट्स या उससे कम आकार के डेटा पर ऑपरेशन्स करने के लिए assembly का उपयोग करें: इवेंट्स में हैशिंग और अनइंडेक्स्ड डेटा
- 8. एक से अधिक बाहरी कॉल करते समय memory स्पेस का पुन: उपयोग करने के लिए assembly का उपयोग करें।
- 9. एक से अधिक कॉन्ट्रैक्ट बनाते समय memory स्पेस का पुन: उपयोग करने के लिए assembly का उपयोग करें।
- 10. Modulo ऑपरेटर का उपयोग करने के बजाय अंतिम बिट की जांच करके परीक्षण करें कि कोई संख्या सम (even) है या विषम (odd)
- 1. नॉन-स्ट्रिक्ट असमानताओं (non-strict inequalities) की तुलना में स्ट्रिक्ट असमानताओं (strict inequalities) को प्राथमिकता दें, लेकिन दोनों विकल्पों का परीक्षण करें
- 2. उन require स्टेटमेंट्स को विभाजित करें जिनमें boolean एक्सप्रेशन्स हों
- 3. Revert स्टेटमेंट्स को विभाजित करें
- 4. हमेशा Named Returns का उपयोग करें
- 5. उन if-else स्टेटमेंट्स को उलट दें जिनमें negation हो
- 6. बढ़ाने (increment) के लिए i++ के बजाय ++i का उपयोग करें
- 7. जहाँ उपयुक्त हो unchecked मैथ का उपयोग करें
- 8. गैस-ऑप्टिमल for-loops लिखें
- 9. Do-While लूप्स for लूप्स से सस्ते होते हैं
- 10. अनावश्यक Variable Casting से बचें, uint256 से छोटे वेरिएबल्स (boolean और address सहित) तब तक कम कुशल होते हैं जब तक कि उन्हें पैक न किया गया हो
- 11. Short-circuit booleans
- 12. वेरिएबल्स को तब तक public न बनाएं जब तक कि ऐसा करना आवश्यक न हो
- 13. ऑप्टिमाइज़र के लिए बहुत बड़े मूल्यों को प्राथमिकता दें
- 14. अत्यधिक उपयोग किए जाने वाले फ़ंक्शन्स के नाम ऑप्टिमल होने चाहिए
- 15. Bitshifting दो की शक्ति से गुणा या भाग करने की तुलना में सस्ता है
- 16. Calldata को कैश करना कभी-कभी सस्ता होता है
- 17. Conditionals और loops के प्रतिस्थापन के रूप में branchless एल्गोरिदम का उपयोग करें
- 18. केवल एक बार उपयोग किए जाने वाले Internal functions को गैस बचाने के लिए inline किया जा सकता है
- 19. Array समानता और string समानता की तुलना उन्हें हैश करके करें यदि वे 32 बाइट्स से लंबे हैं
- 20. शक्तियों (powers) और लघुगणक (logarithms) की गणना करते समय lookup tables का उपयोग करें
- 21. Precompiled contracts कुछ गुणा या memory ऑपरेशन्स के लिए उपयोगी हो सकते हैं
- 22. n * n * n, n ** 3 से सस्ता हो सकता है
खतरनाक तकनीकें (Dangerous techniques)
- 1. जानकारी पास करने के लिए gasprice() या msg.value का उपयोग करें
- 2. यदि परीक्षण इसकी अनुमति देते हैं तो coinbase() या block.number जैसे पर्यावरण वेरिएबल्स में हेरफेर करें
- 3. प्रमुख बिंदुओं पर निर्णयों को ब्रांच करने के लिए gasleft() का उपयोग करें
- 4. Ether को स्थानांतरित करने के लिए send() का उपयोग करें, लेकिन सफलता की जांच न करें
- 5. सभी फ़ंक्शन्स को payable बनाएं
- 6. External library jumping
- 7. एक अत्यधिक अनुकूलित सबरूटीन (subroutine) बनाने के लिए कॉन्ट्रैक्ट के अंत में बाइटकोड जोड़ें
पुरानी ट्रिक्स (Outdated tricks)
Solidity Gas Optimization
Ethereum में गैस ऑप्टिमाइज़ेशन का मतलब है समान बिज़नेस लॉजिक को पूरा करने के लिए Solidity कोड को फिर से लिखना, ताकि Ethereum Virtual Machine (EVM) में कम गैस यूनिट्स खर्च हों।
सोर्स कोड को छोड़कर 11,000 से अधिक शब्दों वाला यह लेख, गैस ऑप्टिमाइज़ेशन पर उपलब्ध सबसे व्यापक सामग्री है।
इस ट्यूटोरियल में दी गई ट्रिक्स को पूरी तरह से समझने के लिए, आपको यह समझना होगा कि EVM कैसे काम करता है, जिसे आप हमारे Gas Optimization Course, Yul Course को लेकर और Huff Puzzles का अभ्यास करके सीख सकते हैं।
हालाँकि, यदि आप केवल यह जानना चाहते हैं कि संभावित गैस ऑप्टिमाइज़ेशन के लिए कोड के किन क्षेत्रों को लक्षित किया जाए, तो यह लेख आपको देखने के लिए कई क्षेत्र प्रदान करता है।
लेखक (Authorship)
RareSkills के शोधकर्ताओं Michael Amadi (LinkedIn, Twitter) और Jesse Raymond (LinkedIn, Twitter) ने इस काम में महत्वपूर्ण योगदान दिया।
गैस ऑप्टिमाइज़ेशन ट्रिक्स हमेशा काम नहीं करती हैं
कुछ गैस ऑप्टिमाइज़ेशन ट्रिक्स केवल एक निश्चित संदर्भ में काम करती हैं। उदाहरण के लिए, सहज रूप से, ऐसा लगेगा कि
if (!cond) {
// branch False
}
else {
// branch True
}
इसकी तुलना में कम कुशल है
if (cond) {
// branch True
}
else {
// branch False
}
क्योंकि शर्त (condition) को उलटने में अतिरिक्त opcodes खर्च होते हैं। इसके विपरीत, ऐसे कई मामले हैं जहाँ यह ऑप्टिमाइज़ेशन वास्तव में ट्रांज़ैक्शन की लागत बढ़ा देता है। Solidity कंपाइलर कभी-कभी अप्रत्याशित हो सकता है। इसलिए, किसी निश्चित एल्गोरिथम को तय करने से पहले आपको वास्तव में विकल्पों के प्रभाव को मापना चाहिए। इनमें से कुछ ट्रिक्स उन क्षेत्रों के प्रति जागरूकता लाने के बारे में हैं जहाँ कंपाइलर आश्चर्यजनक हो सकता है।
कुछ ट्रिक्स जो सार्वभौमिक (universal) नहीं हैं, उन्हें इस दस्तावेज़ में उसी रूप में चिह्नित किया गया है। गैस ऑप्टिमाइज़ेशन ट्रिक्स कभी-कभी इस बात पर निर्भर करती हैं कि कंपाइलर स्थानीय रूप से क्या कर रहा है। आपको आमतौर पर यह देखने के लिए कोड के ऑप्टिमल वर्ज़न और नॉन-ऑप्टिमल वर्ज़न दोनों का परीक्षण करना चाहिए कि आपको वास्तव में सुधार मिलता है या नहीं। हम कुछ ऐसे आश्चर्यजनक मामलों का दस्तावेजीकरण करेंगे जहाँ जो चीज़ ऑप्टिमाइज़ेशन की ओर ले जानी चाहिए, वह वास्तव में उच्च लागत की ओर ले जाती है।
दूसरा, Solidity कंपाइलर पर --via-ir विकल्प का उपयोग करते समय इनमें से कुछ ऑप्टिमाइज़ेशन व्यवहार बदल सकते हैं।
जटिलता (complexity) और पठनीयता (readability) से सावधान रहें
गैस ऑप्टिमाइज़ेशन आमतौर पर कोड को कम पठनीय और अधिक जटिल बनाते हैं। एक अच्छे इंजीनियर को इस बात का व्यक्तिपरक ट्रेड-ऑफ़ करना चाहिए कि कौन से ऑप्टिमाइज़ेशन्स इसके लायक हैं, और कौन से नहीं।
यहाँ प्रत्येक विषय पर विस्तार से चर्चा संभव नहीं है
हम प्रत्येक ऑप्टिमाइज़ेशन को विस्तार से नहीं समझा सकते हैं, और ऐसा करना वास्तव में आवश्यक भी नहीं है क्योंकि अन्य ऑनलाइन संसाधन उपलब्ध हैं। उदाहरण के लिए, layer 2s और state channels का पूर्ण या यहां तक कि पर्याप्त विवरण देना दायरे से बाहर होगा, और उन विषयों को विस्तार से सीखने के लिए ऑनलाइन अन्य संसाधन मौजूद हैं।
इस लेख का उद्देश्य उपलब्ध ट्रिक्स की सबसे व्यापक सूची बनना है। यदि कोई ट्रिक अपरिचित लगती है, तो यह आगे के स्व-अध्ययन (self-study) के लिए एक संकेत हो सकती है। यदि हेडर ऐसा लगता है कि यह कोई ऐसी ट्रिक है जिसे आप पहले से जानते हैं, तो बस उस अनुभाग को सरसरी तौर पर देख लें।
हम एप्लिकेशन-विशिष्ट ट्रिक्स पर चर्चा नहीं करते हैं
उदाहरण के लिए, यह निर्धारित करने के गैस-कुशल तरीके हैं कि कोई संख्या अभाज्य (prime) है या नहीं, लेकिन इसकी इतनी कम आवश्यकता होती है कि इसके लिए स्थान समर्पित करने से इस लेख का मूल्य कम हो जाएगा। इसी तरह, हमारे Tornado Cash tutorial में, हम सुझाव देते हैं कि कोडबेस को और अधिक कुशल कैसे बनाया जा सकता है, लेकिन उस विवरण को यहाँ शामिल करने से पाठकों को कोई लाभ नहीं होगा क्योंकि यह बहुत अधिक एप्लिकेशन विशिष्ट है।
1. सबसे महत्वपूर्ण: जहाँ संभव हो शून्य से एक (zero to one) स्टोरेज राइट्स से बचें
स्टोरेज वेरिएबल को इनिशियलाइज़ करना कॉन्ट्रैक्ट द्वारा किए जा सकने वाले सबसे महंगे ऑपरेशन्स में से एक है।
जब कोई स्टोरेज वेरिएबल शून्य से गैर-शून्य (non-zero) में जाता है, तो यूज़र को कुल 22,100 गैस (शून्य से गैर-शून्य राइट के लिए 20,000 गैस और कोल्ड स्टोरेज एक्सेस के लिए 2,100 गैस) का भुगतान करना होगा।
यही कारण है कि OpenZeppelin reentrancy guard फ़ंक्शन्स को 0 और 1 के बजाय 1 और 2 के साथ सक्रिय या निष्क्रिय के रूप में पंजीकृत करता है। स्टोरेज वेरिएबल को गैर-शून्य से गैर-शून्य में बदलने में केवल 5,000 गैस खर्च होती है।
2. स्टोरेज वेरिएबल्स को कैश (Cache) करें: स्टोरेज वेरिएबल्स को ठीक एक बार लिखें और पढ़ें
आप कुशल solidity कोड में अक्सर निम्नलिखित पैटर्न देखेंगे। स्टोरेज वेरिएबल से पढ़ने में कम से कम 100 गैस खर्च होती है क्योंकि Solidity स्टोरेज रीड को कैश नहीं करता है। राइट्स (Writes) काफी अधिक महंगे हैं। इसलिए, आपको ठीक एक स्टोरेज रीड और ठीक एक स्टोरेज राइट करने के लिए वेरिएबल को मैन्युअली कैश करना चाहिए।
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
contract Counter1 {
uint256 public number;
function increment() public {
require(number < 10);
number = number + 1;
}
}
contract Counter2 {
uint256 public number;
function increment() public {
uint256 _number = number;
require(_number < 10);
number = _number + 1;
}
}
पहला फ़ंक्शन counter को दो बार पढ़ता है, दूसरा कोड इसे एक बार पढ़ता है।
3. संबंधित वेरिएबल्स को पैक करें
संबंधित वेरिएबल्स को एक ही स्लॉट में पैक करने से महंगे स्टोरेज संबंधी ऑपरेशन्स कम होकर गैस लागत कम हो जाती है।
मैन्युअल पैकिंग सबसे अधिक कुशल है
हम बिट शिफ्टिंग का उपयोग करके एक वेरिएबल (uint160) में दो uint80 मान संग्रहीत और प्राप्त करते हैं। यह केवल एक स्टोरेज स्लॉट का उपयोग करेगा और एक ही ट्रांज़ैक्शन में व्यक्तिगत मूल्यों को संग्रहीत या पढ़ते समय सस्ता होता है।
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
contract GasSavingExample {
uint160 public packedVariables;
function packVariables(uint80 x, uint80 y) external {
packedVariables = uint160(x) << 80 | uint160(y);
}
function unpackVariables() external view returns (uint80, uint80) {
uint80 x = uint80(packedVariables >> 80);
uint80 y = uint80(packedVariables);
return (x, y);
}
}
EVM पैकिंग थोड़ी कम कुशल है
यह भी उपरोक्त उदाहरण की तरह एक स्लॉट का उपयोग करता है, लेकिन एक ही ट्रांज़ैक्शन में मूल्यों को संग्रहीत या पढ़ते समय थोड़ा महंगा हो सकता है। इसका कारण यह है कि EVM बिट-शिफ्टिंग स्वयं करेगा।
contract GasSavingExample2 {
uint80 public var1;
uint80 public var2;
function updateVars(uint80 x, uint80 y) external {
var1 = x;
var2 = y;
}
function loadVars() external view returns (uint80, uint80) {
return (var1, var2);
}
}
कोई पैकिंग सबसे कम कुशल नहीं है
यह किसी भी ऑप्टिमाइज़ेशन का उपयोग नहीं करता है, और मूल्यों को संग्रहीत या पढ़ते समय अधिक महंगा होता है।
अन्य उदाहरणों के विपरीत, यह वेरिएबल्स को स्टोर करने के लिए दो स्टोरेज स्लॉट्स का उपयोग करता है।
contract NonGasSavingExample {
uint256 public var1;
uint256 public var2;
function updateVars(uint256 x, uint256 y) external {
var1 = x;
var2 = y;
}
function loadVars() external view returns (uint256, uint256) {
return (var1, var2);
}
}
4. Structs को पैक करें
संबंधित स्टेट वेरिएबल्स की पैकिंग की तरह, struct आइटम्स को पैक करने से गैस बचाने में मदद मिल सकती है।
(यह ध्यान रखना महत्वपूर्ण है कि Solidity में, struct मेम्बर्स को कॉन्ट्रैक्ट के स्टोरेज में क्रमिक रूप से संग्रहीत किया जाता है, उस स्लॉट स्थिति से शुरू होकर जहाँ उन्हें इनिशियलाइज़ किया गया है)।
निम्नलिखित उदाहरणों पर विचार करें:
Unpacked Struct
unpackedStruct में तीन आइटम हैं जिन्हें तीन अलग-अलग स्लॉट में संग्रहीत किया जाएगा। हालाँकि, यदि इन आइटम्स को पैक किया जाता, तो केवल दो स्लॉट्स का उपयोग किया जाता और इससे struct के आइटम्स को पढ़ना और लिखना सस्ता हो जाता।
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
contract Unpacked_Struct {
struct unpackedStruct {
uint64 time; // Takes one slot - although it only uses 64 bits (8 bytes) out of 256 bits (32 bytes).
uint256 money; // This will take a new slot because it is a complete 256 bits (32 bytes) value and thus cannot be packed with the previous value.
address person; // An address occupies only 160 bits (20 bytes).
}
// Starts at slot 0
unpackedStruct details = unpackedStruct(53_000, 21_000, address(0xdeadbeef));
function unpack() external view returns (unpackedStruct memory) {
return details;
}
}
Packed Struct
हम इस तरह struct आइटम्स को पैक करके ऊपर दिए गए उदाहरण में कम गैस का उपयोग कर सकते हैं।
contract Packed_Struct {
struct packedStruct {
uint64 time; // In this case, both `time` (64 bits) and `person` (160 bits) are packed in the same slot since they can both fit into 256 bits (32 bytes)
address person; // Same slot as `time`. Together they occupy 224 bits (28 bytes) out of 256 bits (32 bytes).
uint256 money; // This will take a new slot because it is a complete 256 bits (32 bytes) value and thus cannot be packed with the previous value.
}
// Starts at slot 0
packedStruct details = packedStruct(53_000, address(0xdeadbeef), 21_000);
function unpack() external view returns (packedStruct memory) {
return details;
}
}
5. Strings को 32 बाइट्स से छोटा रखें
Solidity में, strings परिवर्तनीय लंबाई (variable length) वाले डायनामिक डेटा प्रकार हैं, जिसका अर्थ है कि उनकी लंबाई आवश्यकतानुसार बदल और बढ़ सकती है।
यदि लंबाई 32 बाइट्स या उससे अधिक है, तो जिस स्लॉट में उन्हें परिभाषित किया गया है वह string * 2 + 1 की लंबाई को संग्रहीत करता है, जबकि उनका वास्तविक डेटा कहीं और संग्रहीत होता है (उस स्लॉट का keccak हैश)।
हालाँकि, यदि कोई string 32 बाइट्स से कम है, तो length * 2 इसके स्टोरेज स्लॉट के सबसे कम महत्वपूर्ण बाइट (least significant byte) पर संग्रहीत किया जाता है और string का वास्तविक डेटा उस स्लॉट में सबसे महत्वपूर्ण बाइट (most significant byte) से शुरू होकर संग्रहीत किया जाता है जिसमें इसे परिभाषित किया गया है।
String उदाहरण (32 बाइट्स से कम)
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
contract StringStorage1 {
// Uses only one slot
// slot 0: 0x(len * 2)00...hex of (len * 2)(hex"hello")
// Has smaller gas cost due to size.
string public exampleString = "hello";
function getString() public view returns (string memory) {
return exampleString;
}
}
String उदाहरण (32 बाइट्स से अधिक)
contract StringStorage2 {
// Length is more than 32 bytes.
// Slot 0: 0x00...(length*2+1).
// keccak256(0x00): stores hex representation of "hello"
// Has increased gas cost due to size.
string public exampleString = "This is a string that is slightly over 32 bytes!";
function getStringLongerThan32bytes() public view returns (string memory) {
return exampleString;
}
}
हम निम्नलिखित foundry टेस्ट स्क्रिप्ट के साथ इसका परीक्षण कर सकते हैं:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "forge-std/Test.sol";
import "../src/StringLessThan32Bytes.sol";
contract StringStorageTest is Test {
StringStorage1 public store1;
StringStorage2 public store2;
function setUp() public {
store1 = new StringStorage1();
store2 = new StringStorage2();
}
function testStringStorage1() public {
// test for string less than 32 bytes
store1.getString();
bytes32 data = vm.load(address(store1), 0); // slot 0
emit log_named_bytes32("Full string plus length", data); // the full string and its length*2 is stored at slot 0, because it is less than 32 bytes
}
function testStringStorage2() public {
// test for string longer than 32 bytes
store2.getStringLongerThan32bytes();
bytes32 length = vm.load(address(store2), 0); // slot 0 stores the length*2+1
emit log_named_bytes32("Length of string", length);
// uncomment to get original length as number
// emit log_named_uint("Real length of string (no. of bytes)", uint256(length) / 2);
// divide by 2 to get the original length
bytes32 data1 = vm.load(address(store2), keccak256(abi.encode(0))); // slot keccak256(0)
emit log_named_bytes32("First string chunk", data1);
bytes32 data2 = vm.load(address(store2), bytes32(uint256(keccak256(abi.encode(0))) + 1));
emit log_named_bytes32("Second string chunk", data2);
}
}
टेस्ट चलाने के बाद यह परिणाम है।
यदि हम string (32 बाइट्स से लंबी) के हेक्स मान को बिना लंबाई के जोड़ते हैं, तो हम इसे वापस मूल string में बदल देते हैं (Python के साथ)।
यदि किसी string की लंबाई 32 बाइट्स से कम है, तो इसे bytes32 वेरिएबल में स्टोर करना और आवश्यकता पड़ने पर असेंबली (assembly) का उपयोग करना भी कुशल है।
उदाहरण:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;
contract EfficientString {
bytes32 shortString;
function getShortString() external view returns(string memory) {
string memory value;
assembly {
// get slot 0
let slot0Value := sload(shortString.slot)
// to get the byte that holds the length info, we mask it to rmove the string and divide it by 2 to get the length
let len := div(and(slot0Value, 0xff), 2)
// to get string, we mask the slot value to remove the length// we are sure that it can't take more than a byte because of the length check in the `storeShortString` function
let str := and(slot0Value, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00)
// store length in memory
mstore(0x80, len)
// store string in memory
mstore(0xa0, str)
// make `value` reference 0x80 so that solidity does the returning for us
value := 0x80// update the free memory pointer
mstore(0x40, 0xc0)
}
return value;
}
function storeShortString(string calldata value) external {
assembly {
// require that the length is less than 32
if gt(value.length, 31) {
revert(0, 0)
}
// multiply the length, so we can store length*2 following solidity's convention
let length := mul(value.length, 2)
// get the string itself
let str := calldataload(value.offset)
// or the length and str to get what we need to store in storage
let toBeStored := or(str, length)
// store it in storage
sstore(shortString.slot, toBeStored)
}
}
}
उपरोक्त कोड को और अधिक अनुकूलित किया जा सकता है लेकिन समझने में आसान बनाने के लिए इसे इस तरह रखा गया है।
6. जो वेरिएबल्स कभी अपडेट नहीं होते हैं, उन्हें immutable या constant होना चाहिए
Solidity में, जिन वेरिएबल्स को अपडेट करने का इरादा नहीं है उन्हें constant या immutable होना चाहिए।
इसका कारण यह है कि constants और immutable मान सीधे उस कॉन्ट्रैक्ट के बाइटकोड में एम्बेड किए जाते हैं जिसे वे परिभाषित किए गए हैं और इस वजह से स्टोरेज का उपयोग नहीं करते हैं।
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
contract Constants {
uint256 constant MAX_UINT256 = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
function get_max_value() external pure returns (uint256) {
return MAX_UINT256;
}
}
// This uses more gas than the above contract
contract NoConstants {
uint256 MAX_UINT256 = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
function get_max_value() external view returns (uint256) {
return MAX_UINT256;
}
}
इससे बहुत अधिक गैस की बचत होती है क्योंकि हम कोई स्टोरेज रीड्स नहीं करते हैं जो महंगे होते हैं।
7. लेंथ चेक्स (length checks) से बचने के लिए arrays के बजाय mappings का उपयोग करना
आइटम्स की एक सूची या समूह को स्टोर करते समय जिसे आप एक विशिष्ट क्रम में व्यवस्थित करना चाहते हैं और एक निश्चित कुंजी/इंडेक्स (key/index) के साथ प्राप्त करना चाहते हैं, एक array डेटा स्ट्रक्चर का उपयोग करना आम बात है। यह अच्छी तरह से काम करता है, लेकिन क्या आप जानते हैं कि एक mapping का उपयोग करके प्रत्येक रीड पर 2,000+ गैस बचाने के लिए एक ट्रिक लागू की जा सकती है?
नीचे दिया गया उदाहरण देखें
/// get(0) gas cost: 4860
contract Array {
uint256[] a;
constructor() {
a.push() = 1;
a.push() = 2;
a.push() = 3;
}
function get(uint256 index) external view returns(uint256) {
return a[index];
}
}
/// get(0) gas cost: 2758
contract Mapping {
mapping(uint256 => uint256) a;
constructor() {
a[0] = 1;
a[1] = 2;
a[2] = 3;
}
function get(uint256 index) external view returns(uint256) {
return a[index];
}
}
केवल एक mapping का उपयोग करने से, हमें 2102 गैस की बचत होती है। क्यों? आंतरिक रूप से जब आप array के इंडेक्स का मान पढ़ते हैं, तो solidity बाइटकोड जोड़ता है जो यह जांचता है कि आप एक वैध इंडेक्स (यानी array की लंबाई से सख्ती से कम इंडेक्स) से पढ़ रहे हैं, अन्यथा यह एक पैनिक एरर (Panic(0x32) अधिक सटीक होने के लिए) के साथ revert कर देता है। यह इसे अवांछित (unallocated) या इससे भी बदतर, आवंटित (allocated) स्टोरेज/मेमोरी स्थानों को पढ़ने से रोकता है।
जिस तरह से mappings होते हैं (बस एक key => value जोड़ी), उस तरह की कोई जांच मौजूद नहीं है और हम सीधे स्टोरेज स्लॉट से पढ़ने में सक्षम हैं। यह ध्यान रखना महत्वपूर्ण है कि इस तरीके से mappings का उपयोग करते समय, आपके कोड को यह सुनिश्चित करना चाहिए कि आप अपने कैनोनिकल (canonical) array के आउट-ऑफ़-बाउंड इंडेक्स को नहीं पढ़ रहे हैं।
8. अनावश्यक लेंथ चेक्स से बचने के लिए arrays पर unsafeAccess का उपयोग करना
Arrays से पढ़ते समय solidity द्वारा किए जाने वाले लेंथ चेक्स से बचने के लिए (arrays का उपयोग करते हुए भी) mappings का उपयोग करने का एक विकल्प, OpenZeppelin की Arrays.sol लाइब्रेरी में unsafeAccess फ़ंक्शन का उपयोग करना है। यह डेवलपर्स को लेंथ ओवरफ़्लो जांच को छोड़ते हुए array के किसी भी दिए गए इंडेक्स के मूल्यों तक सीधे पहुंचने की अनुमति देता है। इसका उपयोग केवल तभी करना महत्वपूर्ण है जब आप सुनिश्चित हों कि फ़ंक्शन में पार्स किए गए इंडेक्स पार्स किए गए array की लंबाई से अधिक नहीं हो सकते।
9. जब बड़ी संख्या में booleans का उपयोग किया जाता है तो bools के बजाय bitmaps का उपयोग करें
एक सामान्य पैटर्न, विशेषकर एयरड्रॉप्स में, किसी पते को “पहले से उपयोग किया गया” (already used) के रूप में चिह्नित करना है जब एयरड्रॉप या NFT मिंट का दावा किया जाता है।
हालाँकि, चूंकि इस जानकारी को संग्रहीत करने में केवल एक बिट लगता है, और प्रत्येक स्लॉट 256 बिट्स का होता है, इसका मतलब है कि कोई एक स्टोरेज स्लॉट के साथ 256 झंडे/बूलियन (flags/booleans) संग्रहीत कर सकता है।
आप इन संसाधनों से इस तकनीक के बारे में अधिक जान सकते हैं:
RareSkills के एक छात्र द्वारा वीडियो ट्यूटोरियल
Bitmap प्रेस्ले ट्यूटोरियल
10. बहुत सारा डेटा स्टोर करने के लिए SSTORE2 या SSTORE3 का उपयोग करें
SSTORE
SSTORE एक EVM opcode है जो हमें key-value के आधार पर लगातार डेटा संग्रहीत करने की अनुमति देता है। EVM में हर चीज़ की तरह, एक key और value दोनों 32 बाइट्स के मान हैं।
लिखने (SSTORE) और पढ़ने (SLOAD) की लागत खर्च की गई गैस के मामले में बहुत महंगी है। 32 बाइट्स लिखने पर 22,100 गैस खर्च होती है, जो लगभग 690 गैस प्रति बाइट्स में बदल जाती है। दूसरी ओर, स्मार्ट कॉन्ट्रैक्ट का बाइटकोड लिखने पर प्रति बाइट 200 गैस खर्च होती है।
SSTORE2
SSTORE2 इस मायने में एक अनूठी अवधारणा है कि यह डेटा लिखने और संग्रहीत करने के लिए कॉन्ट्रैक्ट के बाइटकोड का उपयोग करता है। इसे प्राप्त करने के लिए हम बाइटकोड की अपरिवर्तनीयता (immutability) के अंतर्निहित गुण का उपयोग करते हैं।
SSTORE2 के कुछ गुण:
- हम केवल एक बार लिख सकते हैं। प्रभावी रूप से
SSTOREके बजायCREATEका उपयोग करना। - पढ़ने के लिए,
SLOADका उपयोग करने के बजाय, अब हम डिप्लॉय किए गए एड्रेस परEXTCODECOPYको कॉल करते हैं जहाँ विशेष डेटा को बाइटकोड के रूप में संग्रहीत किया जाता है। - जब अधिक से अधिक डेटा संग्रहीत करने की आवश्यकता होती है तो डेटा लिखना काफी सस्ता हो जाता है।
उदाहरण:
डेटा लिखना (Writing data)
हमारा लक्ष्य एक विशिष्ट डेटा (बाइट्स प्रारूप में) को कॉन्ट्रैक्ट के बाइटकोड के रूप में संग्रहीत करना है। इसे प्राप्त करने के लिए, हमें 2 चीजें करने की आवश्यकता है:
- अपने डेटा को पहले memory में कॉपी करें, क्योंकि EVM तब इस डेटा को memory से लेता है और इसे रनटाइम कोड के रूप में स्टोर करता है। आप कॉन्ट्रैक्ट क्रिएशन कोड के बारे में हमारे लेख में अधिक जान सकते हैं।
- भविष्य के उपयोग के लिए नए डिप्लॉय किए गए कॉन्ट्रैक्ट एड्रेस को वापस करें और स्टोर करें।
- हम नीचे दिए गए कोड में 61 और 80 के बीच चार शून्यों (0000) के स्थान पर कॉन्ट्रैक्ट कोड साइज़ जोड़ते हैं
0x61000080600a3d393df300। इसलिए यदि कोड का आकार 65 है, तो यह0x61004180600a3d393df300हो जाएगा (0x0041 = 65) - यह बाइटकोड चरण 1 के लिए ज़िम्मेदार है जिसका हमने उल्लेख किया था।
- अब हम चरण 2 के लिए नए डिप्लॉय किए गए एड्रेस को वापस करते हैं।
- हम नीचे दिए गए कोड में 61 और 80 के बीच चार शून्यों (0000) के स्थान पर कॉन्ट्रैक्ट कोड साइज़ जोड़ते हैं
अंतिम कॉन्ट्रैक्ट बाइटकोड = 00 + डेटा (00 = STOP को पहले जोड़ा जाता है ताकि यह सुनिश्चित हो सके कि गलती से एड्रेस को कॉल करके बाइटकोड को निष्पादित नहीं किया जा सकता है)।
डेटा पढ़ना (Reading data)
- प्रासंगिक डेटा प्राप्त करने के लिए, आपको उस पते की आवश्यकता है जहाँ आपने डेटा संग्रहीत किया है।
- स्पष्ट कारणों से कोड का आकार = 0 होने पर हम revert करते हैं।
- अब हम बस कॉन्ट्रैक्ट के बाइटकोड को प्रासंगिक शुरुआती स्थिति से वापस करते हैं जो 1 बाइट्स के बाद है (याद रखें पहला बाइट
STOP OPCODE(0x00)है)।
जिज्ञासुओं के लिए अतिरिक्त जानकारी:
- हम पॉइंटर एड्रेस को स्टोर करने पर निर्भर हुए बिना पॉइंटर एड्रेस की ऑन-चेन या ऑफ-चेन गणना करने के लिए
CREATE2का उपयोग करके प्री-डिटरमिनिस्टिक एड्रेस का भी उपयोग कर सकते हैं।
संदर्भ: solady
SSTORE3
SSTORE3 को समझने के लिए, पहले आइए SSTORE2 की एक महत्वपूर्ण संपत्ति को दोहराएं।
- नया डिप्लॉय किया गया एड्रेस उस डेटा पर निर्भर करता है जिसे हम स्टोर करने का इरादा रखते हैं।
डेटा लिखें
SSTORE3 एक ऐसा डिज़ाइन लागू करता है कि नया डिप्लॉय किया गया एड्रेस हमारे द्वारा प्रदान किए गए डेटा से स्वतंत्र होता है। प्रदान किए गए डेटा को पहले SSTORE का उपयोग करके स्टोरेज में संग्रहीत किया जाता है। फिर हम CREATE2 में डेटा के रूप में एक निरंतर (constant) INIT_CODE पास करते हैं जो आंतरिक रूप से स्टोरेज में संग्रहीत प्रदान किए गए डेटा को कोड के रूप में डिप्लॉय करने के लिए पढ़ता है।
यह डिज़ाइन विकल्प हमें केवल साल्ट (salt) (जो 20 बाइट्स से कम हो सकता है) प्रदान करके अपने डेटा के पॉइंटर एड्रेस की कुशलतापूर्वक गणना करने में सक्षम बनाता है। इस प्रकार हमें अपने पॉइंटर को अन्य वेरिएबल्स के साथ पैक करने में सक्षम बनाता है, जिससे स्टोरेज लागत कम होती है।
डेटा पढ़ें
कल्पना करने का प्रयास करें कि हम डेटा कैसे पढ़ सकते हैं।
- उत्तर है हम केवल साल्ट (salt) प्रदान करके डिप्लॉय किए गए एड्रेस की आसानी से गणना कर सकते हैं।
- फिर पॉइंटर एड्रेस प्राप्त करने के बाद, आवश्यक डेटा प्राप्त करने के लिए उसी
EXTCODECOPYऑपकोड का उपयोग करें।
संक्षेप में (To summarize):
SSTORE2उन मामलों में सहायक है जहाँ राइट ऑपरेशन्स दुर्लभ हैं, और बड़े रीड ऑपरेशन्स अक्सर होते हैं (और पॉइंटर > 14 बाइट्स)SSTORE3बेहतर है जब आप बहुत कम लिखते हैं, लेकिन बहुत बार पढ़ते हैं। (और पॉइंटर < 14 बाइट्स)
SSTORE3 के लिए Philogy को श्रेय।
11. जहाँ उपयुक्त हो memory के बजाय storage pointers का उपयोग करें
Solidity में, स्टोरेज पॉइंटर्स ऐसे वेरिएबल्स होते हैं जो कॉन्ट्रैक्ट के स्टोरेज में किसी स्थान को संदर्भित (reference) करते हैं। वे C/C++ जैसी भाषाओं में पॉइंटर्स के बिल्कुल समान नहीं हैं।
यह जानना मददगार है कि अनावश्यक स्टोरेज रीड्स से बचने और गैस-कुशल स्टोरेज अपडेट करने के लिए स्टोरेज पॉइंटर्स का कुशलतापूर्वक उपयोग कैसे किया जाए।
यहाँ एक उदाहरण दिया गया है जो दर्शाता है कि स्टोरेज पॉइंटर्स कहाँ सहायक हो सकते हैं।
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
contract StoragePointerUnOptimized {
struct User {
uint256 id;
string name;
uint256 lastSeen;
}
constructor() {
users[0] = User(0, "John Doe", block.timestamp);
}
mapping(uint256 => User) public users;
function returnLastSeenSecondsAgo(uint256 _id) public view returns (uint256) {
User memory _user = users[_id];
uint256 lastSeen = block.timestamp - _user.lastSeen;
return lastSeen;
}
}
ऊपर, हमारे पास एक फ़ंक्शन है जो दिए गए इंडेक्स पर यूज़र का अंतिम बार देखा गया (last seen) रिटर्न करता है। यह lastSeen मान प्राप्त करता है और उसे वर्तमान block.timestamp से घटा देता है। फिर हम पूरे struct को memory में कॉपी करते हैं और lastSeen प्राप्त करते हैं जिसका उपयोग हम कुछ सेकंड पहले अंतिम बार देखे गए समय की गणना करने में करते हैं। यह तरीका अच्छी तरह काम करता है लेकिन इतना कुशल नहीं है, इसका कारण यह है कि हम स्टोरेज से memory में struct की सभी चीजों की प्रतिलिपि बना रहे हैं, जिसमें वे वेरिएबल्स भी शामिल हैं जिनकी हमें आवश्यकता नहीं है। काश केवल lastSeen स्टोरेज स्लॉट से पढ़ने का कोई तरीका होता (बिना असेंबली के)। यहीं पर स्टोरेज पॉइंटर्स आते हैं।
// This results in approximately 5,000 gas savings compared to the previous version.
contract StoragePointerOptimized {
struct User {
uint256 id;
string name;
uint256 lastSeen;
}
constructor() {
users[0] = User(0, "John Doe", block.timestamp);
}
mapping(uint256 => User) public users;
function returnLastSeenSecondsAgoOptimized(uint256 _id) public view returns (uint256) {
User storage _user = users[_id];
uint256 lastSeen = block.timestamp - _user.lastSeen;
return lastSeen;
}
}
“उपरोक्त कार्यान्वयन के परिणामस्वरूप पहले वर्ज़न की तुलना में लगभग 5,000 गैस की बचत होती है”। ऐसा क्यों, यहाँ एकमात्र बदलाव memory को storage में बदलना था और हमें बताया गया था कि स्टोरेज की कोई भी चीज़ महंगी है और इससे बचा जाना चाहिए?
यहाँ हम स्टैक पर एक निश्चित आकार के वेरिएबल में users[_id] के लिए स्टोरेज पॉइंटर स्टोर करते हैं (एक struct का पॉइंटर मूल रूप से struct की शुरुआत का स्टोरेज स्लॉट होता है, इस मामले में, यह user[_id].id का स्टोरेज स्लॉट होगा)। चूँकि स्टोरेज पॉइंटर्स आलसी (lazy) होते हैं (जिसका अर्थ है कि वे केवल कॉल किए जाने या संदर्भित होने पर ही कार्य (पढ़ें या लिखें) करते हैं)। इसके बाद हम केवल struct की lastSeen कुंजी तक पहुँचते हैं। इस तरह हम 3 या संभवतः अधिक स्टोरेज लोड और स्टैक पर memory से एक छोटा हिस्सा लेने से पहले एक memory स्टोर के बजाय, एक ही स्टोरेज लोड बनाते हैं और फिर इसे स्टैक पर स्टोर करते हैं।
ध्यान दें: स्टोरेज पॉइंटर्स का उपयोग करते समय, यह महत्वपूर्ण है कि सावधानी बरती जाए और डैंगलिंग पॉइंटर्स (dangling pointers) को संदर्भित न किया जाए। (यहाँ RareSkills के एक प्रशिक्षक द्वारा डैंगलिंग पॉइंटर्स पर एक वीडियो ट्यूटोरियल दिया गया है)।
12. ERC20 टोकन बैलेंस को शून्य होने से बचाएं, हमेशा एक छोटी राशि रखें
यह ऊपर दिए गए ज़ीरो राइट्स से बचने वाले अनुभाग से संबंधित है, लेकिन इसे अलग से बताना उचित है क्योंकि इसका कार्यान्वयन थोड़ा सूक्ष्म (subtle) है।
यदि कोई एड्रेस बार-बार अपना अकाउंट बैलेंस खाली कर रहा है (और रीलोड कर रहा है), तो इससे बहुत सारे शून्य से एक (zero to one) राइट्स होंगे।
13. शून्य से n तक गिनने के बजाय n से शून्य तक गिनें
जब किसी स्टोरेज वेरिएबल को शून्य पर सेट किया जाता है, तो रिफंड दिया जाता है, इसलिए गिनती पर खर्च की गई शुद्ध गैस कम होगी यदि स्टोरेज वेरिएबल की अंतिम स्थिति शून्य हो।
14. Timestamps और block numbers को uint256 होने की आवश्यकता नहीं है
uint48 आकार का टाइमस्टैम्प भविष्य में लाखों वर्षों तक काम करेगा। एक ब्लॉक नंबर हर 12 सेकंड में एक बार बढ़ता है। इससे आपको संख्याओं के उस आकार का बोध होना चाहिए जो समझदार (sensible) है।
डिप्लॉयमेंट पर गैस बचाना (Saving On Gas Deployment)
1. अन्योन्याश्रित (interdependent) स्मार्ट कॉन्ट्रैक्ट्स के एड्रेस की भविष्यवाणी करने के लिए अकाउंट nonce का उपयोग करें, जिससे स्टोरेज वेरिएबल्स और एड्रेस सेटर फ़ंक्शन्स से बचा जा सके
पारंपरिक कॉन्ट्रैक्ट डिप्लॉयमेंट का उपयोग करते समय, स्मार्ट कॉन्ट्रैक्ट के पते की गणना डिप्लॉयर (deployer) के पते और उनके nonce के आधार पर नियतात्मक रूप से (deterministically) की जा सकती है। Solady से LibRLP लाइब्रेरी हमें ठीक यही करने में मदद कर सकती है।
निम्नलिखित उदाहरण परिदृश्य लें;
StorageContract केवल Writer को स्टोरेज वेरिएबल x सेट करने की अनुमति देता है, जिसका अर्थ है कि उसे Writer के address को जानने की आवश्यकता है। लेकिन Writer को StorageContract में लिखने के लिए, उसे StorageContract के address को भी जानना होगा।
नीचे दिया गया कार्यान्वयन इस समस्या के लिए एक भोला (naive) दृष्टिकोण है। यह एक सेटर फ़ंक्शन रखकर इसे संभालता है जो डिप्लॉयमेंट के बाद स्टोरेज वेरिएबल सेट करता है। लेकिन स्टोरेज वेरिएबल्स महंगे हैं और हम उनसे बचना पसंद करेंगे।
contract StorageContract {
address immutable public writer;
uint256 public x;
constructor(address _writer) {
writer = _writer;
}
function setX(uint256 x_) external {
require(msg.sender == address(writer), "only writer can set");
x = x_;
}
}
contract Writer {
StorageContract public storageContract;
// cost: 49291
function set(uint256 x_) external {
storageContract.setX(x_);
}
function setStorageContract(address _storageContract) external {
storageContract = StorageContract(_storageContract);
}
}
यह डिप्लॉयमेंट और रनटाइम दोनों पर अधिक खर्च करता है। इसमें Writer को डिप्लॉय करना, फिर डिप्लॉय किए गए Writer address को writer के रूप में सेट करके StorageContract को डिप्लॉय करना शामिल है। फिर नए बनाए गए StorageContract के साथ Writer के StorageContract वेरिएबल को सेट करना। इसमें बहुत सारे कदम शामिल हैं और यह महंगा हो सकता है क्योंकि हम StorageContract को स्टोरेज में स्टोर करते हैं। Writer.setX() कॉल करने पर 49k गैस खर्च होती है।
इसे करने का एक अधिक कुशल तरीका पहले से उस पते की गणना करना होगा जहां StorageContract और Writer डिप्लॉय किए जाएंगे और उन्हें उनके दोनों constructors में सेट करना होगा।
यहाँ इस बात का उदाहरण दिया गया है कि यह कैसा दिखेगा:
import {LibRLP} from "https://github.com/vectorized/solady/blob/main/src/utils/LibRLP.sol";
contract StorageContract {
address immutable public writer;
uint256 public x;
constructor(address _writer) {
writer = _writer;
}
// cost: 47158
function setX(uint256 x_) external {
require(msg.sender == address(writer), "only writer can set");
x = x_;
}
}
contract Writer {
StorageContract immutable public storageContract;
constructor(StorageContract _storageContract) {
storageContract = _storageContract;
}
function set(uint256 x_) external {
storageContract.setX(x_);
}
}
// one time deployer.
contract BurnerDeployer {
using LibRLP for address;
function deploy() public returns(StorageContract storageContract, address writer) {
StorageContract storageContractComputed = StorageContract(address(this).computeAddress(2)); // contracts nonce start at 1 and only increment when it creates a contract
writer = address(new Writer(storageContractComputed)); // first creation happens here using nonce = 1
storageContract = new StorageContract(writer); // second create happens here using nonce = 2
require(storageContract == storageContractComputed, "false compute of create1 address"); // sanity check
}
}
यहाँ, Writer.setX() कॉल करने पर 47k गैस खर्च होती है। हमने StorageContract को डिप्लॉय करने से पहले उस पते की गणना करके 2k+ गैस बचाई जहां यह डिप्लॉय होगा ताकि हम इसे Writer को डिप्लॉय करते समय उपयोग कर सकें, इसलिए किसी सेटर फ़ंक्शन की आवश्यकता नहीं है।
इस तकनीक को नियोजित (employ) करने के लिए एक अलग कॉन्ट्रैक्ट का उपयोग करना आवश्यक नहीं है, आप इसके बजाय इसे डिप्लॉयमेंट स्क्रिप्ट के अंदर कर सकते हैं।
यदि आप इसका और अन्वेषण करना चाहते हैं तो हम Philogy द्वारा किए गए एड्रेस भविष्यवाणी का एक वीडियो ट्यूटोरियल प्रदान करते हैं।
2. Constructors को payable बनाएं
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.20;
contract A {}
contract B {
constructor() payable {}
}
Constructor को payable बनाने से डिप्लॉयमेंट पर 200 गैस की बचत हुई। ऐसा इसलिए है क्योंकि non-payable फ़ंक्शन्स में एक निहित (implicit) require(msg.value == 0) डाला जाता है। इसके अतिरिक्त, डिप्लॉय समय पर कम बाइटकोड का मतलब है छोटे calldata के कारण कम गैस लागत।
नियमित फ़ंक्शन्स को non-payable बनाने के अच्छे कारण हैं, लेकिन आमतौर पर एक कॉन्ट्रैक्ट एक विशेषाधिकार प्राप्त पते द्वारा डिप्लॉय किया जाता है जो आप यथोचित (reasonably) रूप से मान सकते हैं कि ether नहीं भेजेगा। यह तब लागू नहीं हो सकता है यदि अनुभवहीन यूज़र्स कॉन्ट्रैक्ट डिप्लॉय कर रहे हैं।
3. अधिक शून्य (zeros) रखने के लिए IPFS हैश को अनुकूलित करके (या --no-cbor-metadata कंपाइलर विकल्प का उपयोग करके) डिप्लॉयमेंट साइज़ को कम किया जा सकता है
हमने स्मार्ट कॉन्ट्रैक्ट मेटाडेटा के बारे में अपने ट्यूटोरियल में पहले ही यह स्पष्ट कर दिया है, लेकिन संक्षेप में, Solidity कंपाइलर वास्तविक स्मार्ट कॉन्ट्रैक्ट कोड में 51 बाइट्स मेटाडेटा जोड़ता है। चूँकि प्रत्येक डिप्लॉयमेंट बाइट पर 200 गैस खर्च होती है, इसलिए उन्हें हटाने से डिप्लॉयमेंट की लागत 10,000 गैस से अधिक कम हो सकती है।
हालांकि यह हमेशा आदर्श नहीं होता है क्योंकि यह स्मार्ट कॉन्ट्रैक्ट सत्यापन को प्रभावित कर सकता है। इसके बजाय, डेवलपर्स कोड टिप्पणियों के लिए खोज कर सकते हैं जो संलग्न (appended) होने वाले IPFS हैश में अधिक शून्य (zeros) बनाते हैं।
4. यदि कॉन्ट्रैक्ट का केवल एक बार उपयोग होना है तो constructor में selfdestruct का उपयोग करें
कभी-कभी, कॉन्ट्रैक्ट्स का उपयोग एक ही ट्रांज़ैक्शन में कई कॉन्ट्रैक्ट्स डिप्लॉय करने के लिए किया जाता है, जिसके लिए इसे constructor में करना आवश्यक होता है।
यदि कॉन्ट्रैक्ट का एकमात्र उपयोग constructor में मौजूद कोड है, तो ऑपरेशन के अंत में selfdestructing गैस बचाएगा।
हालाँकि आगामी हार्डफ़ॉर्क में selfdestruct को हटाने के लिए सेट किया गया है, फिर भी इसे EIP 6780 के अनुसार constructor में समर्थित किया जाएगा।
5. Internal functions और modifiers के बीच चयन करते समय ट्रेड-ऑफ़ को समझें
Modifiers अपने कार्यान्वयन बाइटकोड को वहीं इंजेक्ट करते हैं जहां इसका उपयोग किया जाता है जबकि internal functions रनटाइम कोड में उस स्थान पर कूदते (jump) हैं जहां इसका कार्यान्वयन है। यह दोनों विकल्पों में कुछ ट्रेड-ऑफ़ लाता है।
- Modifiers का एक से अधिक बार उपयोग करने का मतलब है दोहराव (repetitiveness) और रनटाइम कोड के आकार में वृद्धि, लेकिन internal function निष्पादन ऑफसेट पर कूदने (jumping) और जारी रखने के लिए वापस कूदने की अनुपस्थिति के कारण गैस लागत को कम करता है। इसका अर्थ है कि यदि आपके लिए रनटाइम गैस लागत सबसे ज्यादा मायने रखती है, तो modifiers आपकी पसंद होनी चाहिए, लेकिन यदि डिप्लॉयमेंट गैस लागत और/या निर्माण कोड (creation code) के आकार को कम करना आपके लिए सबसे महत्वपूर्ण है तो internal functions का उपयोग करना सबसे अच्छा होगा।
- हालाँकि, modifiers में ट्रेडऑफ़ होता है कि उन्हें केवल फ़ंक्शन के आरंभ या अंत में निष्पादित किया जा सकता है। इसका मतलब है कि इसे फ़ंक्शन के मध्य में निष्पादित करना सीधे तौर पर संभव नहीं होगा, कम से कम internal functions के बिना नहीं जो मूल उद्देश्य को खत्म कर देता है। यह इसके लचीलेपन को प्रभावित करता है। Internal functions को हालांकि किसी फ़ंक्शन में किसी भी बिंदु पर कॉल किया जा सकता है।
Modifiers और internal function का उपयोग करने पर गैस लागत में अंतर दिखाने वाला उदाहरण
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
/** deployment gas cost: 195435
gas per call:
restrictedAction1: 28367
restrictedAction2: 28377
restrictedAction3: 28411
*/
contract Modifier {
address owner;
uint256 val;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
function restrictedAction1() external onlyOwner {
val = 1;
}
function restrictedAction2() external onlyOwner {
val = 2;
}
function restrictedAction3() external onlyOwner {
val = 3;
}
}
/** deployment gas cost: 159309
gas per call:
restrictedAction1: 28391
restrictedAction2: 28401
restrictedAction3: 28435
*/
contract InternalFunction {
address owner;
uint256 val;
constructor() {
owner = msg.sender;
}
function onlyOwner() internal view {
require(msg.sender == owner);
}
function restrictedAction1() external {
onlyOwner();
val = 1;
}
function restrictedAction2() external {
onlyOwner();
val = 2;
}
function restrictedAction3() external {
onlyOwner();
val = 3;
}
}
| Operation | Deployment | restrictedAction1 | restrictedAction2 | restrictedAction3 |
|---|---|---|---|---|
| Modifiers | 195435 | 28367 | 28377 | 28411 |
| Internal Functions | 159309 | 28391 | 28401 | 28435 |
उपरोक्त तालिका से, हम देख सकते हैं कि modifiers का उपयोग करने वाले कॉन्ट्रैक्ट को 3 फ़ंक्शन्स में onlyOwner कार्यक्षमता के दोहराव के कारण internal functions का उपयोग करने वाले कॉन्ट्रैक्ट की तुलना में डिप्लॉय करते समय 35k से अधिक गैस खर्च होती है।
रनटाइम के दौरान, हम देख सकते हैं कि modifiers का उपयोग करने वाले प्रत्येक फ़ंक्शन पर internal functions का उपयोग करने वाले फ़ंक्शन्स की तुलना में निश्चित रूप से 24 कम गैस खर्च होती है।
6. बहुत समान स्मार्ट कॉन्ट्रैक्ट्स जो अक्सर कॉल नहीं किए जाते हैं, उन्हें डिप्लॉय करते समय clones या metaproxies का उपयोग करें
कई समान स्मार्ट कॉन्ट्रैक्ट्स को डिप्लॉय करते समय, गैस लागत अधिक हो सकती है। इन लागतों को कम करने के लिए, आप न्यूनतम clones या metaproxies का उपयोग कर सकते हैं जो कार्यान्वयन (implementation) कॉन्ट्रैक्ट के पते को अपने बाइटकोड में संग्रहीत करते हैं और इसके साथ एक प्रॉक्सी (proxy) के रूप में बातचीत करते हैं।
हालाँकि, रनटाइम लागत और clones की डिप्लॉयमेंट लागत के बीच एक ट्रेड-ऑफ़ है। Clones उनके द्वारा उपयोग किए जाने वाले delegatecall के कारण सामान्य कॉन्ट्रैक्ट्स की तुलना में बातचीत करने के लिए अधिक महंगे होते हैं, इसलिए उनका उपयोग केवल तभी किया जाना चाहिए जब आपको उनसे अक्सर बातचीत करने की आवश्यकता न हो। उदाहरण के लिए, Gnosis Safe कॉन्ट्रैक्ट डिप्लॉयमेंट लागत को कम करने के लिए clones का उपयोग करता है।
स्मार्ट कॉन्ट्रैक्ट डिप्लॉय करने की गैस लागत को कम करने के लिए clones और metaproxies का उपयोग कैसे करें, इसके बारे में हमारे ब्लॉग पोस्ट से अधिक जानें:
7. एडमिन फ़ंक्शन्स payable हो सकते हैं
हम एडमिन विशिष्ट फ़ंक्शन्स को गैस बचाने के लिए payable बना सकते हैं, क्योंकि कंपाइलर फ़ंक्शन के callvalue की जांच नहीं करेगा।
यह कॉन्ट्रैक्ट को छोटा और डिप्लॉय करने में सस्ता भी बना देगा क्योंकि क्रिएशन और रनटाइम कोड में कम opcodes होंगे।
8. Custom errors (आमतौर पर) require स्टेटमेंट्स से छोटे होते हैं
कस्टम एरर्स (Custom errors) को कैसे संभाला जाता है, इसके कारण स्ट्रिंग्स के साथ require स्टेटमेंट्स की तुलना में Custom errors सस्ते होते हैं। Solidity एरर सिग्नेचर के हैश के केवल पहले 4 बाइट्स को स्टोर करता है और केवल उसे वापस करता है। इसका मतलब है कि reverting के दौरान, memory में केवल 4 बाइट्स को स्टोर करने की आवश्यकता होती है। Require स्टेटमेंट्स में स्ट्रिंग संदेशों के मामले में, Solidity को कम से कम 64 बाइट्स के साथ स्टोर (memory में) और revert करना पड़ता है।
यहाँ नीचे एक उदाहरण दिया गया है।
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
contract CustomError {
error InvalidAmount();
function withdraw(uint256 _amount) external pure {
if (_amount > 10 ether) revert InvalidAmount();
}
}
// This uses more gas than the above contract
contract NoCustomError {
function withdraw(uint256 _amount) external pure {
require(_amount <= 10 ether, "Error: Pass in a valid amount");
}
}
9. अपना स्वयं का डिप्लॉय करने के बजाय मौजूदा create2 फ़ैक्टरीज़ का उपयोग करें
शीर्षक स्व-व्याख्यात्मक है। यदि आपको एक नियतात्मक (deterministic) एड्रेस की आवश्यकता है, तो आप आमतौर पर पहले से डिप्लॉय किए गए का पुन: उपयोग कर सकते हैं।
क्रॉस कॉन्ट्रैक्ट कॉल्स (Cross contract calls)
1. डेस्टिनेशन स्मार्ट कॉन्ट्रैक्ट से ट्रांसफर शुरू करने के बजाय टोकन के लिए transfer hooks का उपयोग करें
मान लीजिए कि आपके पास contract A है जो token B (एक NFT या एक ERC1363 टोकन) स्वीकार करता है। भोला (naive) वर्कफ़्लो इस प्रकार है:
msg.sendertoken Bको स्वीकार करने के लिएcontract Aको मंजूरी (approves) देता हैmsg.senderसेAमें टोकन ट्रांसफर करने के लिएmsg.sendercontract Aको कॉल करता हैContract Aफिर ट्रांसफर करने के लिएtoken Bको कॉल करता हैToken Bट्रांसफर करता है, औरcontract AमेंonTokenReceived()को कॉल करता हैContract AonTokenReceived()सेtoken Bको एक मान वापस करता हैToken Bcontract Aमें निष्पादन (execution) वापस करता है
यह बहुत अकुशल है। ट्रांसफर करने के लिए msg.sender का contract B को कॉल करना बेहतर है जो contract A में tokenReceived हुक (hook) को कॉल करता है।
ध्यान दें कि:
- सभी ERC1155 टोकन में एक transfer hook शामिल है
- ERC721 में
safeTransferऔरsafeMintमें एक transfer hook है - ERC1363 में
transferAndCallहै - ERC777 में एक transfer hook है लेकिन इसे हटा दिया गया है (deprecated)। यदि आपको फंजिबल (fungible) टोकन की आवश्यकता है तो इसके बजाय ERC1363 या ERC1155 का उपयोग करें
यदि आपको कॉन्ट्रैक्ट A में तर्क (arguments) पास करने की आवश्यकता है, तो बस डेटा फ़ील्ड का उपयोग करें और उसे कॉन्ट्रैक्ट A में पार्स करें।
2. Ether ट्रांसफर करते समय deposit() के बजाय fallback या receive का उपयोग करें
ऊपर के समान, आप कॉन्ट्रैक्ट में “बस ether ट्रांसफर” कर सकते हैं और payable फ़ंक्शन का उपयोग करने के बजाय उसे ट्रांसफर पर प्रतिक्रिया दे सकते हैं। बेशक, यह कॉन्ट्रैक्ट के बाकी आर्किटेक्चर पर निर्भर करता है।
AAVE में Deposit का उदाहरण
contract AddLiquidity{
receive() external payable {
IWETH(weth).deposit{msg.value}();
AAVE.deposit(weth, msg.value, msg.sender, REFERRAL_CODE)
}
}
Fallback फ़ंक्शन बाइट्स डेटा प्राप्त करने में सक्षम है जिसे abi.decode के साथ पार्स किया जा सकता है। यह किसी deposit फ़ंक्शन को तर्क (arguments) देने के विकल्प के रूप में कार्य करता है।
3. स्टोरेज स्लॉट्स और कॉन्ट्रैक्ट एड्रेस को प्री-वार्म (pre-warm) करने के लिए क्रॉस-कॉन्ट्रैक्ट कॉल करते समय ERC2930 एक्सेस लिस्ट ट्रांज़ैक्शन का उपयोग करें
एक्सेस लिस्ट ट्रांज़ैक्शन (Access list transactions) आपको 200 गैस छूट के साथ कुछ स्टोरेज और कॉल ऑपरेशन्स के लिए गैस लागत का पूर्व भुगतान (prepay) करने की अनुमति देते हैं। यह आगे के स्टेट या स्टोरेज एक्सेस पर गैस बचा सकता है, जिसका भुगतान वार्म एक्सेस (warm access) के रूप में किया जाता है।
यदि आपका ट्रांज़ैक्शन एक क्रॉस-कॉन्ट्रैक्ट कॉल करेगा, तो आपको लगभग निश्चित रूप से एक्सेस लिस्ट ट्रांज़ैक्शन का उपयोग करना चाहिए।
Clones या proxies को कॉल करते समय जिसमें हमेशा delegatecall के माध्यम से क्रॉस-कॉन्ट्रैक्ट कॉल शामिल होता है, आपको ट्रांज़ैक्शन को एक एक्सेस लिस्ट ट्रांज़ैक्शन बनाना चाहिए।
इस पर हमारा एक समर्पित ब्लॉग पोस्ट है, अधिक जानने के लिए https://www.rareskills.io/post/eip-2930-optional-access-list-ethereum पर जाएं।
4. बाहरी कॉन्ट्रैक्ट्स की कॉल्स को कैश (Cache) करें जहाँ इसका अर्थ बनता हो (जैसे chainlink oracle से रिटर्न डेटा को कैश करना)
डेटा को कैश करने की आमतौर पर तब अनुशंसा की जाती है जब आप एक ही निष्पादन प्रक्रिया के दौरान एक ही डेटा का > 1 बार उपयोग करना चाहते हैं, ताकि memory में दोहराव से बचा जा सके।
स्पष्ट उदाहरण यह है कि यदि आपको कई ऑपरेशन्स करने की आवश्यकता है, मान लीजिए, chainlink से प्राप्त ETH मूल्य का उपयोग करके। आप महंगे बाहरी कॉल को फिर से करने के बजाय, मूल्य को memory में स्टोर करते हैं।
5. राउटर जैसे कॉन्ट्रैक्ट्स में multicall लागू करें
यह एक सामान्य विशेषता है, जैसे Uniswap Router और Compound Bulker। यदि आप अपने यूज़र्स से कॉल्स के एक क्रम (sequence) की अपेक्षा करते हैं, तो multicall का उपयोग करके एक कॉन्ट्रैक्ट द्वारा उन्हें एक साथ बैच करें।
6. आर्किटेक्चर को मोनोलिथिक (monolithic) बनाकर कॉन्ट्रैक्ट कॉल्स से बचें
कॉन्ट्रैक्ट कॉल्स महंगी होती हैं, और उन पर गैस बचाने का सबसे अच्छा तरीका यह है कि उनका बिल्कुल भी उपयोग न किया जाए। इसके साथ एक स्वाभाविक ट्रेडऑफ़ है, लेकिन कई कॉन्ट्रैक्ट्स होना जो एक-दूसरे से बात करते हैं, कभी-कभी गैस और जटिलता को प्रबंधित करने के बजाय बढ़ा सकते हैं।
डिज़ाइन पैटर्न (Design Patterns)
1. ट्रांज़ैक्शन्स को बैच करने के लिए multidelegatecall का उपयोग करें
Multi-delegatecall msg.sender और msg.value जैसे पर्यावरण वेरिएबल्स को संरक्षित करते हुए msg.sender को एक कॉन्ट्रैक्ट के भीतर कई फ़ंक्शन्स को कॉल करने में मदद करता है।
ध्यान दें: ध्यान रखें कि चूंकि msg.value लगातार बना रहता है, इसलिए यह ऐसे मुद्दों को जन्म दे सकता है जिन्हें डेवलपर को अपने कॉन्ट्रैक्ट में multi delegatecall इनहेरिट (inherit) करते समय संबोधित करने की आवश्यकता होती है।
Multi delegatecall का उदाहरण नीचे Uniswap का कार्यान्वयन है:
function multicall(bytes[] calldata data) public payable override returns (bytes[] memory results) {
results = new bytes[](data.length);
for (uint256 i = 0; i < data.length; i++) {
(bool success, bytes memory result) = address(this).delegatecall(data[i]);
if (!success) {
// Next 5 lines from https://ethereum.stackexchange.com/a/83577
if (result.length < 68) revert();
assembly {
result := add(result, 0x04)
}
revert(abi.decode(result, (string)));
}
results[i] = result;
}
}
2. Allowlists और airdrops के लिए merkle trees के बजाय ECDSA signatures का उपयोग करें
Merkle trees पर्याप्त मात्रा में calldata का उपयोग करते हैं और merkle proof के आकार के साथ लागत में वृद्धि करते हैं। आम तौर पर, merkle proofs की तुलना में डिजिटल हस्ताक्षरों (digital signatures) का उपयोग गैस-वार (gas-wise) सस्ता होता है।
3. अप्रूवल और ट्रांसफर स्टेप को एक ही ट्रांज़ैक्शन में बैच करने के लिए ERC20Permit का उपयोग करें
ERC20 Permit में एक अतिरिक्त फ़ंक्शन है जो किसी अन्य पते के लिए अप्रूवल (approval) बढ़ाने के लिए टोकन धारक से डिजिटल हस्ताक्षर स्वीकार करता है। इस तरह, अप्रूवल प्राप्तकर्ता एक ही ट्रांज़ैक्शन में परमिट (permit) ट्रांज़ैक्शन और ट्रांसफर सबमिट कर सकता है। परमिट देने वाले यूज़र को कोई गैस नहीं देनी होती है, और परमिट प्राप्तकर्ता एक ही ट्रांज़ैक्शन में परमिट और transferFrom ट्रांज़ैक्शन को बैच कर सकता है।
4. गेम्स या अन्य उच्च-थ्रूपुट, कम ट्रांज़ैक्शन वैल्यू वाले एप्लिकेशन्स के लिए L2 मैसेज पासिंग का उपयोग करें
Etherorcs इस पैटर्न के शुरुआती अग्रदूतों में से एक था, इसलिए आप प्रेरणा के लिए उनके Github (ऊपर लिंक किया गया) को देख सकते हैं। विचार यह है कि Ethereum पर एसेट्स को Polygon, Optimism, या Arbitrum जैसी दूसरी चेन पर “ब्रिज” (मैसेज पासिंग के माध्यम से) किया जा सकता है और गेम वहां आयोजित किया जा सकता है जहां ट्रांज़ैक्शन्स सस्ते हैं।
5. यदि लागू हो तो state-channels का उपयोग करें
State channels संभवतः सबसे पुराने, लेकिन अभी भी उपयोग योग्य Ethereum के लिए स्केलेबिलिटी (scalability) समाधान हैं। L2s के विपरीत, वे एप्लिकेशन विशिष्ट हैं। यूज़र्स अपने ट्रांज़ैक्शन्स को चेन पर कमिट (commit) करने के बजाय, एसेट्स को स्मार्ट कॉन्ट्रैक्ट में कमिट करते हैं और फिर स्टेट ट्रांज़िशन्स के रूप में एक-दूसरे के साथ बाध्यकारी हस्ताक्षर (binding signatures) साझा करते हैं। जब ऑपरेशन समाप्त हो जाता है, तो वे अंतिम परिणाम चेन पर कमिट कर देते हैं।
यदि प्रतिभागियों में से कोई एक बेईमान है, तो एक ईमानदार प्रतिभागी दूसरे पक्ष के हस्ताक्षर का उपयोग स्मार्ट कॉन्ट्रैक्ट को उनकी एसेट्स जारी करने के लिए मजबूर करने के लिए कर सकता है।
6. गैस बचाने के उपाय के रूप में वोटिंग डेलिगेशन (voting delegation) का उपयोग करें
ERC20 Votes पर हमारा ट्यूटोरियल इस पैटर्न का अधिक विस्तार से वर्णन करता है। प्रत्येक टोकन मालिक के मतदान के बजाय, केवल प्रतिनिधि (delegates) मतदान करते हैं, जो शुद्ध वोटों की संख्या को कम कर देता है।
7. ERC1155, ERC721 की तुलना में एक सस्ता नॉन-फंजिबल टोकन है
ERC721 balanceOf फ़ंक्शन का व्यवहार में शायद ही कभी उपयोग किया जाता है लेकिन जब भी कोई मिंट (mint) और ट्रांसफर होता है तो यह एक स्टोरेज ओवरहेड जोड़ता है। ERC1155 प्रति आईडी (id) बैलेंस को ट्रैक करता है, और आईडी के स्वामित्व को ट्रैक करने के लिए उसी बैलेंस का उपयोग भी करता है। यदि प्रत्येक आईडी के लिए अधिकतम आपूर्ति (maximum supply) एक है, तो टोकन प्रत्येक आईडी के लिए नॉन-फंजिबल (non-fungible) हो जाता है।
8. कई ERC20 टोकन्स के बजाय एक ERC1155 या ERC6909 टोकन का उपयोग करें
यह ERC1155 टोकन का मूल उद्देश्य था। प्रत्येक व्यक्तिगत टोकन ERC20 की तरह व्यवहार करता है, लेकिन केवल एक कॉन्ट्रैक्ट डिप्लॉय करने की आवश्यकता होती है।
इस दृष्टिकोण का दोष यह है कि टोकन अधिकांश DeFi स्वैपिंग प्रिमिटिव (swapping primitives) के अनुकूल नहीं होंगे।
ERC1155 सभी ट्रांसफर विधियों (methods) पर कॉलबैक (callbacks) का उपयोग करता है। यदि यह वांछित नहीं है, तो इसके बजाय ERC6909 का उपयोग किया जा सकता है।
9. UUPS अपग्रेड पैटर्न यूज़र्स के लिए Transparent Upgradeable Proxy की तुलना में अधिक गैस कुशल है
Transparent upgradeable proxy पैटर्न के लिए हर बार ट्रांज़ैक्शन होने पर msg.sender की तुलना एडमिन से करने की आवश्यकता होती है। UUPS केवल अपग्रेड फ़ंक्शन के लिए ऐसा करता है।
10. OpenZeppelin के विकल्पों का उपयोग करने पर विचार करें
OpenZeppelin एक बेहतरीन और लोकप्रिय स्मार्ट कॉन्ट्रैक्ट लाइब्रेरी है, लेकिन अन्य विकल्प भी हैं जिन पर विचार करने योग्य है। ये विकल्प बेहतर गैस दक्षता प्रदान करते हैं और डेवलपर्स द्वारा परीक्षण और अनुशंसित किए गए हैं।
ऐसे विकल्पों के दो उदाहरण Solmate और Solady हैं।
Solmate एक लाइब्रेरी है जो सामान्य स्मार्ट कॉन्ट्रैक्ट पैटर्न्स के कई गैस-कुशल कार्यान्वयन प्रदान करती है। Solady एक अन्य गैस-कुशल लाइब्रेरी है जो assembly के उपयोग पर ज़ोर देती है।
Calldata ऑप्टिमाइज़ेशन्स
1. वैनिटी एड्रेस (vanity addresses) का उपयोग करें (सुरक्षित रूप से!)
प्रमुख शून्यों (leading zeros) वाले वैनिटी पतों का उपयोग करना सस्ता है, इससे calldata गैस लागत बचती है।
इस पते के साथ OpenSea Seaport contract एक अच्छा उदाहरण है:
0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC।
सीधे पते पर कॉल करने पर इससे गैस की बचत नहीं होगी। हालाँकि, यदि उस कॉन्ट्रैक्ट के पते का उपयोग किसी फ़ंक्शन के तर्क के रूप में किया जाता है, तो calldata में अधिक शून्य होने के कारण उस फ़ंक्शन कॉल में कम गैस खर्च होगी।
यह बहुत सारे शून्य वाले EOAs को एक फ़ंक्शन तर्क के रूप में पारित (passing) करने के बारे में भी सच है - यह उसी कारण से गैस बचाता है।
बस ध्यान रखें कि अपर्याप्त यादृच्छिक निजी कुंजियों (insufficiently random private keys) वाले वॉलेट के लिए वैनिटी पते बनाने से हैक हुए हैं। यह create2 के लिए साल्ट खोजने के साथ बनाए गए स्मार्ट कॉन्ट्रैक्ट्स वैनिटी पतों के लिए चिंता का विषय नहीं है, क्योंकि स्मार्ट कॉन्ट्रैक्ट्स में निजी कुंजियां (private keys) नहीं होती हैं।
2. यदि संभव हो तो calldata में signed integers से बचें
चूँकि solidity two’s complement का उपयोग करता है signed integers को दर्शाने के लिए, छोटी नकारात्मक संख्याओं (negative numbers) के लिए calldata काफी हद तक गैर-शून्य (non-zero) होगा। उदाहरण के लिए, two’s complement रूप में -1 0xff…ff है और इसलिए अधिक महंगा है।
3. Calldata (आमतौर पर) memory से सस्ता होता है
फ़ंक्शन इनपुट या डेटा को सीधे calldata से लोड करना memory से लोड करने की तुलना में सस्ता है। इसका कारण यह है कि calldata से डेटा तक पहुंचने में कम ऑपरेशन्स और गैस लागत शामिल होती है। इसलिए, memory का उपयोग केवल तभी करने की सलाह दी जाती है जब फ़ंक्शन में डेटा को संशोधित करने की आवश्यकता हो (calldata को संशोधित नहीं किया जा सकता है)।
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
contract CalldataContract {
function getDataFromCalldata(bytes calldata data) public pure returns (bytes memory) {
return data;
}
}
contract MemoryContract {
function getDataFromMemory(bytes memory data) public pure returns (bytes memory) {
return data;
}
}
4. Calldata को पैक करने पर विचार करें, विशेषकर L2 पर
Solidity स्टोरेज वेरिएबल्स को स्वचालित रूप से पैक करता है, लेकिन उन वेरिएबल्स के लिए abi encoding जो स्टोरेज में पैक किए जाएंगे, वे calldata में पैक नहीं होते हैं।
यह एक चरम ऑप्टिमाइज़ेशन है जो उच्च कोड जटिलता की ओर ले जाता है, लेकिन यदि कोई फ़ंक्शन बहुत सारा calldata लेता है तो इस पर विचार करना चाहिए।
ABI एन्कोडिंग हर डेटा प्रतिनिधित्व (data representation) के लिए कुशल नहीं है, कुछ डेटा अभ्यावेदन को एप्लिकेशन विशिष्ट तरीके से अधिक कुशलता से एन्कोड किया जा सकता है।
इस तकनीक के बारे में अधिक चर्चा हमारे L2 Calldata Optimization पर लेख में की गई है।
अपडेट (Update) Dencun अपग्रेड के अनुसार, अधिकांश L2s अब L1 पर calldata पोस्ट नहीं करते हैं, बल्कि blobs का उपयोग करते हैं, इसलिए हालांकि calldata को छोटा करने से अभी भी लागत बचेगी, लेकिन बचत उतनी महत्वपूर्ण नहीं है।
Assembly ट्रिक्स
आपको यह नहीं मान लेना चाहिए कि assembly लिखने से स्वतः ही अधिक कुशल कोड प्राप्त होगा। हमने उन क्षेत्रों को सूचीबद्ध किया है जहाँ assembly लिखना आमतौर पर बेहतर काम करता है, लेकिन आपको हमेशा गैर-असेंबली वर्ज़न (non-assembly version) का परीक्षण करना चाहिए।
1. एरर मैसेज के साथ revert करने के लिए assembly का उपयोग करना
Solidity कोड में revert करते समय, एरर मैसेज के साथ निष्पादन को revert करने के लिए require या revert स्टेटमेंट का उपयोग करना आम बात है। इसे ज्यादातर मामलों में एरर मैसेज के साथ revert करने के लिए assembly का उपयोग करके और अधिक अनुकूलित किया जा सकता है।
यहाँ एक उदाहरण है:
/// calling restrictedAction(2) with a non-owner address: 24042
contract SolidityRevert {
address owner;
uint256 specialNumber = 1;
constructor() {
owner = msg.sender;
}
function restrictedAction(uint256 num) external {
require(owner == msg.sender, "caller is not owner");
specialNumber = num;
}
}
/// calling restrictedAction(2) with a non-owner address: 23734
contract AssemblyRevert {
address owner;
uint256 specialNumber = 1;
constructor() {
owner = msg.sender;
}
function restrictedAction(uint256 num) external {
assembly {
if sub(caller(), sload(owner.slot)) {
mstore(0x00, 0x20) // store offset to where length of revert message is stored
mstore(0x20, 0x13) // store length (19)
mstore(0x40, 0x63616c6c6572206973206e6f74206f776e657200000000000000000000000000) // store hex representation of message
revert(0x00, 0x60) // revert with data
}
}
specialNumber = num;
}
}
उपरोक्त उदाहरण से हम देख सकते हैं कि solidity में ऐसा करने के विरुद्ध assembly के साथ उसी एरर मैसेज के साथ revert करने पर हमें 300 से अधिक गैस की बचत होती है। यह गैस बचत memory एक्सपेंशन लागत और अतिरिक्त टाइप चेक से आती है जो solidity कंपाइलर आंतरिक रूप से करता है।
2. इंटरफ़ेस के माध्यम से फ़ंक्शन्स को कॉल करने पर memory एक्सपेंशन लागत आती है, इसलिए memory में पहले से मौजूद डेटा का पुन: उपयोग करने के लिए assembly का उपयोग करें
किसी अन्य contract A से contract B पर किसी फ़ंक्शन को कॉल करते समय, इंटरफ़ेस का उपयोग करना, एक पते के साथ B का एक उदाहरण बनाना और उस फ़ंक्शन को कॉल करना सबसे सुविधाजनक होता है जिसे हम कॉल करना चाहते हैं। यह बहुत अच्छी तरह से काम करता है, लेकिन जिस तरह से solidity हमारे कोड को संकलित (compiles) करता है, उसके कारण यह contract B को भेजने के लिए डेटा को एक नए memory स्थान में संग्रहीत करता है जिससे memory का विस्तार होता है, कभी-कभी अनावश्यक रूप से।
Inline assembly के साथ, हम अपने कोड को बेहतर ढंग से अनुकूलित कर सकते हैं और पहले से उपयोग किए गए memory स्थानों का उपयोग करके कुछ गैस बचा सकते हैं जिनकी हमें दोबारा आवश्यकता नहीं है या (यदि calldata जिसकी contract B को उम्मीद है 64 बाइट्स से कम है) हमारे calldata को संग्रहीत करने के लिए स्क्रैच स्पेस (scratch space) में।
यहाँ दोनों की तुलना करने वाला एक उदाहरण दिया गया है:
/// 30570
contract Sol {
function set(address addr, uint256 num) external {
Callme(addr).setNum(num);
}
}
/// 30350
contract Assembly {
function set(address addr, uint256 num) external {
assembly {
mstore(0x00, hex"cd16ecbf")
mstore(0x04, num)
if iszero(extcodesize(addr)) {
revert(0x00, 0x00) // revert if address has no code deployed to it
}
let success := call(gas(), addr, 0x00, 0x00, 0x24, 0x00, 0x00)
if iszero(success) {
revert(0x00, 0x00)
}
}
}
}
contract Callme {
uint256 num = 1;
function setNum(uint256 a) external {
num = a;
}
}
हम देख सकते हैं कि Assembly पर set(uint256) कॉल करने पर उस गैस से 220 कम गैस खर्च होती है जो हम solidity का उपयोग करते तो खर्च होती।
ध्यान दें कि बाहरी कॉल (external calls) करने के लिए inline assembly का उपयोग करते समय, यह जांचना महत्वपूर्ण है कि जिस पते पर हम कॉल कर रहे हैं उसमें extcodesize(addr) का उपयोग करके कोड डिप्लॉय किया गया है या नहीं और यदि यह 0 रिटर्न करता है तो revert करें। यह महत्वपूर्ण है क्योंकि किसी ऐसे पते को कॉल करना जिसमें कोई कोड डिप्लॉय नहीं किया गया है हमेशा सत्य (true) रिटर्न करता है जो अधिकांश परिदृश्यों में हमारे कॉन्ट्रैक्ट लॉजिक के लिए विनाशकारी हो सकता है।
3. सामान्य गणित ऑपरेशन्स, जैसे min और max के गैस कुशल विकल्प मौजूद हैं
Unoptimized
function max(uint256 x, uint256 y) public pure returns (uint256 z) {
z = x > y ? x : y;
}
Optimized
function max(uint256 x, uint256 y) public pure returns (uint256 z) {
/// @solidity memory-safe-assembly
assembly {
z := xor(x, mul(xor(x, y), gt(y, x)))
}
}
उपरोक्त कोड Solady Library के गणित अनुभाग से लिया गया है, वहां और अधिक गणित ऑपरेशन्स मिल सकते हैं। यह देखने के लिए लाइब्रेरी की खोज करने योग्य है कि आपके लिए कौन से गैस कुशल ऑपरेशन्स उपलब्ध हैं।
उपरोक्त उदाहरण अधिक गैस कुशल होने का कारण यह है कि टर्नरी ऑपरेटर (ternary operator) (और सामान्य तौर पर, इसमें conditionals वाला कोड) में opcodes में सशर्त जंप (conditional jumps) होते हैं, जो अधिक महंगे होते हैं।
यहाँ branchless max पर हमारा वीडियो ट्यूटोरियल है जो उपरोक्त कोड की व्याख्या करता है।
4. असमानता की जांच करने के लिए ISZERO(EQ()) के बजाय SUB या XOR का उपयोग करें (कुछ परिदृश्यों में अधिक कुशल)
दो मानों की समानता (equality) की तुलना करने के लिए inline assembly का उपयोग करते समय (जैसे कि क्या owner caller() के समान है), कभी-कभी यह करना अधिक कुशल होता है
if sub(caller, sload(owner.slot)) {
revert(0x00, 0x00)
}
ऐसा करने के बजाय
if eq(caller, sload(owner.slot)) {
revert(0x00, 0x00)
}
xor भी वही काम कर सकता है, लेकिन ध्यान रखें कि xor सभी बिट्स फ़्लिप किए गए मान को भी समान मानेगा, इसलिए सुनिश्चित करें कि यह एक हमले का वेक्टर (attack vector) नहीं बन सकता है।
यह ट्रिक उपयोग किए गए कंपाइलर संस्करण (version) और कोड के संदर्भ (context) पर निर्भर करेगी।
5. address(0) की जांच के लिए inline assembly का उपयोग करें
Inline assembly में कॉन्ट्रैक्ट लिखना आमतौर पर गैस अनुकूलित माना जाता है। हम सीधे memory में हेरफेर कर सकते हैं और इसे Solidity कंपाइलर पर छोड़ने के बजाय कम opcodes का उपयोग कर सकते हैं।
प्रमाणीकरण तंत्र (Authentication mechanism) एक ऐसा उदाहरण है जहाँ inline assembly का उपयोग करना अच्छा है, जैसे address zero जांच लागू करना।
यहाँ नीचे एक उदाहरण दिया गया है:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
contract NormalAddressZeroCheck {
function check(address _caller) public pure returns (bool) {
require(_caller != address(0x00), "Zero address");
return true;
}
}
contract AddressZeroCheckAssembly {
// Saves about 90 gas
function checkOptimized(address _caller) public pure returns (bool) {
assembly {
if iszero(_caller) {
mstore(0x00, 0x20)
mstore(0x20, 0x0c)
mstore(0x40, 0x5a65726f20416464726573730000000000000000000000000000000000000000) // load hex of "Zero Address" to memory
revert(0x00, 0x60)
}
}
return true;
}
}
6. selfbalance, address(this).balance से सस्ता है (कुछ परिदृश्यों में अधिक कुशल)
solidity कोड address(this).balance को कभी-कभी yul से selfbalance() फ़ंक्शन के साथ अधिक कुशलता से किया जा सकता है, लेकिन ध्यान रखें कि कंपाइलर कभी-कभी आंतरिक रूप से इस ट्रिक का उपयोग करने के लिए पर्याप्त स्मार्ट होता है, इसलिए दोनों तरीकों का परीक्षण करें।
7. 96 बाइट्स या उससे कम आकार के डेटा पर ऑपरेशन्स करने के लिए assembly का उपयोग करें: इवेंट्स में हैशिंग और अनइंडेक्स्ड डेटा
Solidity हमेशा memory का विस्तार (expanding) करके इसमें लिखता है जो कभी-कभी कुशल नहीं होता है। हम inline-assembly का उपयोग करके डेटा पर memory ऑपरेशन्स को अनुकूलित कर सकते हैं जो आकार में 96 बाइट्स या उससे कम हैं।
Solidity memory के अपने पहले 64 बाइट्स (mem[0x00:0x40]) को स्क्रैच स्पेस (scratch space) के रूप में आरक्षित (reserves) करता है जिसका उपयोग डेवलपर्स गारंटी के साथ कोई भी ऑपरेशन करने के लिए कर सकते हैं कि इसे ओवरराइट नहीं किया जाएगा या अप्रत्याशित रूप से पढ़ा नहीं जाएगा। memory के अगले 32 बाइट्स (mem[0x40:0x60]) वह स्थान है जहाँ solidity फ्री memory पॉइंटर को संग्रहीत, पढ़ता और अपडेट करता है। इस तरह solidity नया डेटा लिखने के लिए अगले memory ऑफ़सेट का ट्रैक रखता है। memory के अगले 32 बाइट्स (mem[0x60:0x80]) को ज़ीरो स्लॉट (zero slot) कहा जाता है। यह वह जगह है जहाँ अनइनिशियलाइज़्ड डायनेमिक memory डेटा (bytes memory, string memory, T[] memory(जहाँ T कोई भी वैध प्रकार है)) इशारा (points) करता है। चूँकि ये मान अनइनिशियलाइज़्ड होते हैं, solidity आगे बढ़ाता है कि जिस स्लॉट पर वे इशारा करते हैं (0x60) वह 0x00 बना रहता है।
ध्यान दें: memory में संग्रहीत Structs तब भी जब वे गतिशील (dynamic) होते हैं (यानी स्वयं के भीतर एक गतिशील मान होता है), अनइनिशियलाइज़्ड होने पर ज़ीरो स्लॉट की ओर इशारा नहीं करते हैं।
ध्यान दें: अनइनिशियलाइज़्ड डायनेमिक memory डेटा अभी भी ज़ीरो स्लॉट की ओर इशारा करता है, भले ही वे एक struct के भीतर नेस्टेड (nested) हों।
यदि हम memory में ऑपरेशन्स करने में स्क्रैच स्पेस का उपयोग कर सकते हैं जिसे कंपाइलर आमतौर पर memory का विस्तार करके करता यदि वह इसे स्वयं करता, तो हम अपने कोड को अनुकूलित कर सकते हैं। तो अब हमारे पास काम करने के लिए 64 बाइट्स की सस्ती memory है। फ्री memory पॉइंटर स्पेस का उपयोग तब तक किया जा सकता है जब तक कि हम असेंबली ब्लॉक से बाहर निकलने से पहले इसे अपडेट कर दें। हम इसके लिए इसे अस्थायी रूप से स्टैक पर स्टोर कर सकते हैं।
आइए कुछ उदाहरण देखें।
- 96 बाइट्स तक अनइंडेक्स्ड डेटा लॉग करने के लिए assembly का उपयोग करना
contract ExpensiveLogger {
event BlockData(uint256 blockTimestamp, uint256 blockNumber, uint256 blockGasLimit);
// cost: 26145
function returnBlockData() external {
emit BlockData(block.timestamp, block.number, block.gaslimit);
}
}
contract CheapLogger {
event BlockData(uint256 blockTimestamp, uint256 blockNumber, uint256 blockGasLimit);
// cost: 22790
function returnBlockData() external {
assembly {
mstore(0x00, timestamp())
mstore(0x20, number())
mstore(0x40, gaslimit())
log1(0x00,
0x60,
0x9ae98f1999f57fc58c1850d34a78f15d31bee81788521909bea49d7f53ed270b // event hash of BlockData
)
}
}
}
उपरोक्त उदाहरण दिखाता है कि हम BlockData इवेंट में उत्सर्जित (emit) होने वाले डेटा को स्टोर करने के लिए memory का उपयोग करके लगभग 2,000 गैस कैसे बचा सकते हैं।
यहाँ हमारे फ्री memory पॉइंटर को अपडेट करने की कोई आवश्यकता नहीं है क्योंकि हमारे इवेंट को उत्सर्जित करने के ठीक बाद निष्पादन समाप्त हो जाता है और हम कभी भी solidity कोड में वापस कदम नहीं रखते हैं।
आइए एक और उदाहरण लें जहां हमें फ्री memory पॉइंटर को अपडेट करने की आवश्यकता होगी
- 96 बाइट्स तक डेटा को हैश करने के लिए assembly का उपयोग करना
contract ExpensiveHasher {
bytes32 public hash;
struct Values {
uint256 a;
uint256 b;
uint256 c;
}
Values values;
// cost: 113155function setOnchainHash(Values calldata _values) external {
hash = keccak256(abi.encode(_values));
values = _values;
}
}
contract CheapHasher {
bytes32 public hash;
struct Values {
uint256 a;
uint256 b;
uint256 c;
}
Values values;
// cost: 112107
function setOnchainHash(Values calldata _values) external {
assembly {
// cache the free memory pointer because we are about to override it
let fmp := mload(0x40)
// use 0x00 to 0x60
calldatacopy(0x00, 0x04, 0x60)
sstore(hash.slot, keccak256(0x00, 0x60))
// restore the cache value of free memory pointer
mstore(0x40, fmp)
}
values = _values;
}
}
उपरोक्त उदाहरण में, पहले के समान, हम मानों को memory के पहले 96 बाइट्स में संग्रहीत करने के लिए assembly का उपयोग करते हैं जो हमें 1,000+ गैस बचाता है। यह भी ध्यान दें कि इस उदाहरण में, क्योंकि हम अभी भी solidity कोड में वापस आते हैं, हमने अपने असेंबली ब्लॉक की शुरुआत और अंत में अपने फ्री memory पॉइंटर को कैश किया और अपडेट किया। यह सुनिश्चित करने के लिए है कि solidity कंपाइलर की धारणाएं कि memory में क्या संग्रहीत है, संगत (compatible) बनी रहें।
8. एक से अधिक बाहरी कॉल (external calls) करते समय memory स्पेस का पुन: उपयोग करने के लिए assembly का उपयोग करें।
एक ऑपरेशन जिसके कारण solidity कंपाइलर memory का विस्तार करता है वह है बाहरी कॉल (external calls) करना। बाहरी कॉल करते समय कंपाइलर को उस फ़ंक्शन के फ़ंक्शन सिग्नेचर को एनकोड करना होता है जिसे वह अपने तर्कों (arguments) के साथ बाहरी कॉन्ट्रैक्ट पर कॉल करना चाहता है। जैसा कि हम जानते हैं, solidity memory को साफ (clear) या पुन: उपयोग नहीं करता है इसलिए उसे इस डेटा को अगले फ्री memory पॉइंटर में स्टोर करना होगा जो memory का और विस्तार करता है।
Inline assembly के साथ, हम या तो स्क्रैच स्पेस और फ्री memory पॉइंटर ऑफ़सेट का उपयोग इस डेटा को स्टोर करने के लिए कर सकते हैं (जैसा कि ऊपर दिया गया है) यदि फ़ंक्शन तर्क memory में 96 बाइट्स से अधिक जगह नहीं लेते हैं। इससे भी बेहतर, यदि हम एक से अधिक बाहरी कॉल कर रहे हैं तो हम memory का अनावश्यक रूप से विस्तार किए बिना memory में नए तर्कों को स्टोर करने के लिए पहली कॉल के समान memory स्पेस का पुन: उपयोग कर सकते हैं। इस परिदृश्य में Solidity memory का उतना ही विस्तार करेगा जितनी लौटाए गए डेटा (returned data) की लंबाई है। ऐसा इसलिए है क्योंकि लौटाया गया डेटा memory में संग्रहीत किया जाता है (ज्यादातर मामलों में)। यदि रिटर्न डेटा 96 बाइट्स से कम है, तो हम memory का विस्तार करने से रोकने के लिए इसे स्टोर करने के लिए स्क्रैच स्पेस का उपयोग कर सकते हैं।
नीचे दिया गया उदाहरण देखें;
contract Called {
function add(uint256 a, uint256 b) external pure returns(uint256) {
return a + b;
}
}
contract Solidity {
// cost: 7262
function call(address calledAddress) external pure returns(uint256) {
Called called = Called(calledAddress);
uint256 res1 = called.add(1, 2);
uint256 res2 = called.add(3, 4);
uint256 res = res1 + res2;
return res;
}
}
contract Assembly {
// cost: 5281
function call(address calledAddress) external view returns(uint256) {
assembly {
// check that calledAddress has code deployed to it
if iszero(extcodesize(calledAddress)) {
revert(0x00, 0x00)
}
// first call
mstore(0x00, hex"771602f7")
mstore(0x04, 0x01)
mstore(0x24, 0x02)
let success := staticcall(gas(), calledAddress, 0x00, 0x44, 0x60, 0x20)
if iszero(success) {
revert(0x00, 0x00)
}
let res1 := mload(0x60)
// second call
mstore(0x04, 0x03)
mstore(0x24, 0x4)
success := staticcall(gas(), calledAddress, 0x00, 0x44, 0x60, 0x20)
if iszero(success) {
revert(0x00, 0x00)
}
let res2 := mload(0x60)
// add results
let res := add(res1, res2)
// return data
mstore(0x60, res)
return(0x60, 0x20)
}
}
}
हम फ़ंक्शन चयनकर्ता (selector) और उसके तर्कों को स्टोर करने के लिए स्क्रैच स्पेस का उपयोग करके लगभग 2,000 गैस बचाते हैं और दूसरी कॉल के लिए उसी memory स्पेस का पुन: उपयोग करते हुए लौटाए गए डेटा को ज़ीरो स्लॉट में संग्रहीत करते हैं जिससे memory का विस्तार नहीं होता है।
यदि बाहरी फ़ंक्शन के तर्क जिसे आप कॉल करना चाहते हैं 64 बाइट्स से ऊपर है और यदि आप एक बाहरी कॉल कर रहे हैं, तो इसे assembly में लिखने से कोई महत्वपूर्ण गैस नहीं बचेगी। हालाँकि, यदि एक से अधिक कॉल कर रहे हैं। आप अभी भी inline assembly का उपयोग करके 2 कॉल्स के लिए समान memory स्लॉट का पुन: उपयोग करके गैस बचा सकते हैं।
ध्यान दें: हमेशा फ्री memory पॉइंटर को अपडेट करना याद रखें यदि जिस ऑफ़सेट की ओर यह इशारा करता है वह पहले से ही उपयोग किया गया है, ताकि solidity को वहां संग्रहीत डेटा को ओवरराइड करने या वहां संग्रहीत मान का अप्रत्याशित तरीके से उपयोग करने से रोका जा सके।
यह भी ध्यान दें कि यदि आपके पास उस कॉल स्टैक के भीतर अपरिभाषित डायनेमिक memory मान हैं, तो ज़ीरो स्लॉट (0x60 memory ऑफ़सेट) को अधिलेखित (overwriting) करने से बचें। एक विकल्प स्पष्ट रूप से डायनेमिक memory मानों को परिभाषित करना है या यदि उपयोग किया जाता है, तो असेंबली ब्लॉक से बाहर निकलने से पहले स्लॉट को वापस 0x00 पर सेट करना है।
9. एक से अधिक कॉन्ट्रैक्ट बनाते समय memory स्पेस का पुन: उपयोग करने के लिए assembly का उपयोग करें।
Solidity कॉन्ट्रैक्ट निर्माण (creation) को बाहरी कॉल के समान मानता है जो 32 बाइट्स वापस करता है (यानी यह बनाए गए कॉन्ट्रैक्ट का पता या address(0) वापस करता है यदि कॉन्ट्रैक्ट निर्माण विफल हो गया)।
बाहरी कॉल्स के साथ गैस बचाने वाले अनुभाग से, हम तुरंत देख सकते हैं कि इसे ऑप्टिमाइज़ करने का एक तरीका यह है कि लौटाए गए पते को स्क्रैच स्पेस में स्टोर किया जाए और memory के विस्तार से बचा जाए।
नीचे एक समान उदाहरण देखें;
contract Solidity {
// cost: 261032
function call() external returns (Called, Called) {
Called called1 = new Called();
Called called2 = new Called();
return (called1, called2);
}
}
contract Assembly {
// cost: 260210
function call() external returns(Called, Called) {
bytes memory creationCode = type(Called).creationCode;
assembly {
let called1 := create(0x00, add(0x20, creationCode), mload(creationCode))
let called2 := create(0x00, add(0x20, creationCode), mload(creationCode))
// revert if either called1 or called2 returned address(0)
if iszero(and(called1, called2)) {
revert(0x00, 0x00)
}
mstore(0x00, called1)
mstore(0x20, called2)
return(0x00, 0x40)
}
}
}
contract Called {
function add(uint256 a, uint256 b) external pure returns(uint256) {
return a + b;
}
}
हमने inline assembly का उपयोग करके लगभग 1,000 गैस बचाई।
ध्यान दें: उस परिदृश्य में जहाँ डिप्लॉय किए जाने वाले दो कॉन्ट्रैक्ट समान नहीं हैं, memory विस्तार से बचने के लिए दूसरे कॉन्ट्रैक्ट के निर्माण कोड (creation code) को inline assembly का उपयोग करके मैन्युअल रूप से mstored करने की आवश्यकता होगी और solidity में किसी वेरिएबल को असाइन नहीं किया जाएगा।
10. Modulo ऑपरेटर का उपयोग करने के बजाय अंतिम बिट की जांच करके परीक्षण करें कि कोई संख्या सम (even) है या विषम (odd)
किसी संख्या के सम या विषम होने की जांच करने का पारंपरिक तरीका x % 2 == 0 करना है जहाँ x विचाराधीन संख्या है। इसके बजाय आप जांच सकते हैं कि क्या x & uint256(1) == 0। जहाँ x को uint256 माना जाता है। Bitwise and modulo ऑप कोड से सस्ता है। बाइनरी में, सबसे दायां बिट “1” का प्रतिनिधित्व करता है जबकि बाकी सभी बिट्स 2 के गुणक (multiples) होते हैं, जो सम (even) होते हैं। सम संख्या में “1” जोड़ने से यह विषम (odd) हो जाती है।
Solidity कंपाइलर से संबंधित
निम्नलिखित ट्रिक्स को Solidity कंपाइलर में गैस दक्षता में सुधार करने के लिए जाना जाता है। हालाँकि, यह उम्मीद की जाती है कि Solidity कंपाइलर समय के साथ सुधरेगा जिससे ये ट्रिक्स कम उपयोगी या प्रतिकूल (counterproductive) भी हो जाएंगी।
आपको यहाँ सूचीबद्ध ट्रिक्स का आँख बंद करके उपयोग नहीं करना चाहिए, बल्कि दोनों विकल्पों का बेंचमार्क करना चाहिए।
इनमें से कुछ ट्रिक्स पहले से ही --via-ir कंपाइलर ध्वज (flag) का उपयोग करते समय कंपाइलर द्वारा शामिल की जाती हैं, और उस ध्वज का उपयोग करने पर कोड को कम कुशल भी बना सकती हैं।
बेंचमार्क। हमेशा बेंचमार्क करें।
1. नॉन-स्ट्रिक्ट असमानताओं (non-strict inequalities) की तुलना में स्ट्रिक्ट असमानताओं (strict inequalities) को प्राथमिकता दें, लेकिन दोनों विकल्पों का परीक्षण करें
आमतौर पर नॉन-स्ट्रिक्ट असमानताओं (<=, >=) की तुलना में स्ट्रिक्ट असमानताओं (<, >) का उपयोग करने की अनुशंसा की जाती है। ऐसा इसलिए है क्योंकि नॉन-स्ट्रिक्ट असमानता को पूरा करने के लिए कंपाइलर कभी-कभी > b को !(a < b) में बदल देगा। EVM में कम-से-कम या बराबर (less-than-or-equal to) या अधिक-या-बराबर (greater-than-or-equal to) की जांच के लिए कोई opcode नहीं है।
हालाँकि, आपको दोनों तुलनाओं का प्रयास करना चाहिए, क्योंकि यह हमेशा ऐसा नहीं होता है कि स्ट्रिक्ट असमानता का उपयोग करने से गैस की बचत होगी। यह आस-पास के opcodes के संदर्भ पर बहुत निर्भर करता है।
2. उन require स्टेटमेंट्स को विभाजित करें जिनमें boolean एक्सप्रेशन्स हों
जब हम require स्टेटमेंट्स को विभाजित करते हैं, तो हम अनिवार्य रूप से कह रहे हैं कि फ़ंक्शन को निष्पादित करना जारी रखने के लिए प्रत्येक स्टेटमेंट का सत्य होना चाहिए।
यदि पहला स्टेटमेंट गलत (false) का मूल्यांकन करता है, तो फ़ंक्शन तुरंत revert हो जाएगा और निम्नलिखित require स्टेटमेंट्स की जांच नहीं की जाएगी। यह अगले require स्टेटमेंट का मूल्यांकन करने के बजाय गैस लागत को बचाएगा।
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
contract Require {
function dontSplitRequireStatement(uint256 x, uint256 y) external pure returns (uint256) {
require(x > 0 && y > 0); // both conditon would be evaluated, before reverting or notreturn x * y;
}
}
contract RequireTwo {
function splitRequireStatement(uint256 x, uint256 y) external pure returns (uint256) {
require(x > 0); // if x <= 0, the call reverts and "y > 0" is not checked.
require(y > 0);
return x * y;
}
}
3. Revert स्टेटमेंट्स को विभाजित करें
Require स्टेटमेंट्स को विभाजित करने के समान, यदि स्टेटमेंट में boolean ऑपरेटर नहीं होने से आप आमतौर पर कुछ गैस बचाएंगे।
contract CustomErrorBoolLessEfficient {
error BadValue();
function requireGood(uint256 x) external pure {
if (x < 10 || x > 20) {
revert BadValue();
}
}
}
contract CustomErrorBoolEfficient {
error TooLow();
error TooHigh();
function requireGood(uint256 x) external pure {
if (x < 10) {
revert TooLow();
}
if (x > 20) {
revert TooHigh();
}
}
}
4. हमेशा Named Returns का उपयोग करें
Solidity कंपाइलर तब अधिक कुशल कोड आउटपुट करता है जब वेरिएबल को रिटर्न स्टेटमेंट में घोषित (declared) किया जाता है। व्यवहार में इसके बहुत कम अपवाद (exceptions) प्रतीत होते हैं, इसलिए यदि आप एक अनाम (anonymous) रिटर्न देखते हैं, तो आपको यह निर्धारित करने के लिए इसके बजाय नामित (named) रिटर्न के साथ इसका परीक्षण करना चाहिए कि कौन सा मामला सबसे कुशल है।
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
contract NamedReturn {
function myFunc1(uint256 x, uint256 y) external pure returns (uint256) {
require(x > 0);
require(y > 0);
return x * y;
}
}
contract NamedReturn2 {
function myFunc2(uint256 x, uint256 y) external pure returns (uint256 z) {
require(x > 0);
require(y > 0);
z = x * y;
}
}
5. उन if-else स्टेटमेंट्स को उलट दें जिनमें negation हो
यह वही उदाहरण है जो हमने लेख की शुरुआत में दिया था। नीचे दिए गए कोड स्निपेट में, दूसरा फ़ंक्शन अनावश्यक निषेध (negation) से बचता है। सिद्धांत रूप में, अतिरिक्त ! कम्प्यूटेशनल लागत बढ़ाता है। लेकिन जैसा कि हमने लेख के शीर्ष पर ध्यान दिया है, आपको दोनों तरीकों का बेंचमार्क करना चाहिए क्योंकि कंपाइलर कभी-कभी इसे अनुकूलित कर सकता है।
function cond() public {
if (!condition) {
action1();
}
else {
action2();
}
}
function cond() public {
if (condition) {
action2();
}
else {
action1();
}
}
6. बढ़ाने (increment) के लिए i++ के बजाय ++i का उपयोग करें
इसके पीछे कारण यह है कि कंपाइलर द्वारा ++i और i++ का मूल्यांकन कैसे किया जाता है। i++ एक नए मान पर i को बढ़ाने से पहले i (इसका पुराना मान) लौटाता है। इसका मतलब है कि उपयोग के लिए स्टैक पर 2 मान संग्रहीत किए जाते हैं चाहे आप इसका उपयोग करना चाहते हों या नहीं। दूसरी ओर ++i, i पर ++ ऑपरेशन का मूल्यांकन करता है (यानी यह i को बढ़ाता है) फिर i (इसका बढ़ा हुआ मान) लौटाता है जिसका अर्थ है कि स्टैक पर केवल एक आइटम को संग्रहीत करने की आवश्यकता है।
7. जहाँ उपयुक्त हो unchecked मैथ का उपयोग करें
Solidity डिफ़ॉल्ट रूप से चेक किए गए मैथ (checked math) का उपयोग करता है (यानी यदि किसी गणित ऑपरेशन का परिणाम परिणाम वेरिएबल के प्रकार (type) को ओवरफ़्लो करता है तो यह revert हो जाता है), लेकिन कुछ स्थितियां ऐसी होती हैं जहां ओवरफ़्लो होना अव्यवहार्य (infeasible) होता है।
- for लूप जिनमें प्राकृतिक ऊपरी सीमाएँ (natural upper bounds) होती हैं
- ऐसा गणित जहां फ़ंक्शन के इनपुट को पहले से ही उचित सीमाओं में साफ (sanitized) कर दिया गया हो
- वेरिएबल्स जो कम संख्या से शुरू होते हैं और फिर प्रत्येक ट्रांज़ैक्शन इसमें एक या छोटी संख्या जोड़ता है (जैसे एक काउंटर)
जब भी आप कोड में अंकगणित (arithmetic) देखते हैं, तो अपने आप से पूछें कि क्या संदर्भ में ओवरफ़्लो या अंडरफ़्लो के लिए कोई प्राकृतिक गार्ड है (संख्या रखने वाले वेरिएबल के प्रकार को भी ध्यान में रखें)। यदि ऐसा है, तो एक unchecked ब्लॉक जोड़ें।
8. गैस-ऑप्टिमल for-loops लिखें
ध्यान दें: Solidity 0.8.22 के अनुसार, यह ट्रिक कंपाइलर द्वारा स्वचालित रूप से की जाती है और इसे स्पष्ट रूप से करने की आवश्यकता नहीं है।
यदि आप ऊपर दी गई दोनों ट्रिक्स को मिलाते हैं, तो गैस-ऑप्टिमल for loop ऐसा दिखता है:
for (uint256 i; i < limit; ) {
// inside the loop
unchecked {
++i;
}
}
यहाँ पारंपरिक for लूप से दो अंतर यह हैं कि i++ ++i हो जाता है (जैसा कि ऊपर बताया गया है), और यह unchecked है क्योंकि सीमा वेरिएबल (limit variable) यह सुनिश्चित करता है कि यह ओवरफ़्लो नहीं होगा।
9. Do-While लूप्स for लूप्स से सस्ते होते हैं
यदि आप थोड़ा अपरंपरागत (unconventional) कोड बनाने की कीमत पर ऑप्टिमाइज़ेशन को आगे बढ़ाना चाहते हैं, तो Solidity do-while लूप for लूप की तुलना में अधिक गैस कुशल हैं, भले ही आप उस मामले के लिए if-condition चेक जोड़ते हैं जहां लूप बिल्कुल निष्पादित (execute) नहीं होता है।
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
// times == 10 in both tests
contract Loop1 {
function loop(uint256 times) public pure {
for (uint256 i; i < times;) {
unchecked {
++i;
}
}
}
}
contract Loop2 {
function loop(uint256 times) public pure {
if (times == 0) {
return;
}
uint256 i;
do {
unchecked {
++i;
}
} while (i < times);
}
}
10. अनावश्यक Variable Casting से बचें, uint256 से छोटे वेरिएबल्स (boolean और address सहित) तब तक कम कुशल होते हैं जब तक कि उन्हें पैक न किया गया हो
Integers के लिए uint256 का उपयोग करना बेहतर है, सिवाय इसके कि जब छोटे integers आवश्यक हों। ऐसा इसलिए है क्योंकि उपयोग किए जाने पर EVM स्वचालित रूप से छोटे integers को uint256 में परिवर्तित कर देता है। यह रूपांतरण प्रक्रिया अतिरिक्त गैस लागत जोड़ती है, इसलिए शुरू से ही uint256 का उपयोग करना अधिक कुशल है।
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
contract Unnecessary_Typecasting {
uint8 public num;
function incrementNum() public {
num += 1;
}
}
// Uses less gas
contract NoTypecasting {
uint256 public num;
function incrementNumCheap() public {
num += 1;
}
}
11. Short-circuit booleans
Solidity में, जब आप किसी boolean एक्सप्रेशन का मूल्यांकन करते हैं (उदा. || (logical or) या && (logical and) ऑपरेटर्स), || के मामले में दूसरे एक्सप्रेशन का मूल्यांकन केवल तभी किया जाएगा जब पहले एक्सप्रेशन का मूल्यांकन गलत (false) हो और && के मामले में दूसरे एक्सप्रेशन का मूल्यांकन केवल तभी किया जाएगा जब पहले एक्सप्रेशन का मूल्यांकन सही (true) हो। इसे short-circuiting कहा जाता है।
उदाहरण के लिए, एक्सप्रेशन require(msg.sender == owner || msg.sender == manager) पास हो जाएगा यदि पहला एक्सप्रेशन msg.sender == owner का मूल्यांकन सही (true) है। दूसरे एक्सप्रेशन msg.sender == manager का बिल्कुल भी मूल्यांकन नहीं किया जाएगा। हालाँकि, यदि पहले एक्सप्रेशन msg.sender == owner का मूल्यांकन गलत (false) है, तो यह निर्धारित करने के लिए दूसरे एक्सप्रेशन msg.sender == manager का मूल्यांकन किया जाएगा कि समग्र (overall) एक्सप्रेशन सही है या गलत। यहाँ, उस स्थिति की जांच करके जिसके सबसे पहले पास होने की संभावना है, हम दूसरी स्थिति की जांच करने से बच सकते हैं जिससे अधिकांश सफल कॉल्स में गैस की बचत होती है।
यह एक्सप्रेशन require(msg.sender == owner && msg.sender == manager) के लिए भी समान है। यदि पहले एक्सप्रेशन msg.sender == owner का मूल्यांकन गलत (false) है, तो दूसरे एक्सप्रेशन msg.sender == manager का मूल्यांकन नहीं किया जाएगा क्योंकि समग्र (overall) एक्सप्रेशन सही नहीं हो सकता है। समग्र (overall) स्टेटमेंट के सही होने के लिए, एक्सप्रेशन के दोनों पक्षों का मूल्यांकन सही होना चाहिए। यहाँ, उस स्थिति की जांच करके जिसके सबसे पहले विफल होने की संभावना है, हम दूसरी स्थिति की जांच करने से बच सकते हैं जिससे अधिकांश कॉल reverts में गैस की बचत होती है।
Short-circuiting उपयोगी है और कम महंगे एक्सप्रेशन को पहले रखने की अनुशंसा की जाती है, क्योंकि अधिक महंगे वाले को बायपास (bypassed) किया जा सकता है। यदि दूसरा एक्सप्रेशन पहले वाले की तुलना में अधिक महत्वपूर्ण है, तो उनके क्रम को उलटना उचित हो सकता है ताकि सस्ते वाले का पहले मूल्यांकन किया जा सके।
12. वेरिएबल्स को तब तक public न बनाएं जब तक कि ऐसा करना आवश्यक न हो
एक public स्टोरेज वेरिएबल में उसी नाम का एक निहित (implicit) public फ़ंक्शन होता है। एक public फ़ंक्शन जंप टेबल (jump table) के आकार को बढ़ाता है और विचाराधीन वेरिएबल को पढ़ने के लिए बाइटकोड जोड़ता है। यह कॉन्ट्रैक्ट को बड़ा बनाता है। याद रखें, private वेरिएबल्स निजी (private) नहीं होते हैं, web3.js का उपयोग करके वेरिएबल मान निकालना मुश्किल नहीं है। यह विशेष रूप से उन constants के लिए सच है जो स्मार्ट कॉन्ट्रैक्ट्स के बजाय इंसानों द्वारा पढ़े जाने के लिए हैं।
13. ऑप्टिमाइज़र के लिए बहुत बड़े मूल्यों को प्राथमिकता दें
Solidity ऑप्टिमाइज़र दो प्राथमिक पहलुओं (aspects) को अनुकूलित करने पर केंद्रित है:
- स्मार्ट कॉन्ट्रैक्ट की डिप्लॉयमेंट लागत।
- स्मार्ट कॉन्ट्रैक्ट के भीतर फ़ंक्शन्स की निष्पादन (execution) लागत।
ऑप्टिमाइज़र के लिए रन (runs) पैरामीटर का चयन करने में एक ट्रेड-ऑफ़ शामिल है। छोटे रन मान डिप्लॉयमेंट लागत को कम करने को प्राथमिकता देते हैं, जिसके परिणामस्वरूप निर्माण कोड (creation code) छोटा होता है लेकिन संभावित रूप से रनटाइम कोड अनऑप्टिमाइज़्ड होता है। हालाँकि यह डिप्लॉयमेंट के दौरान गैस की लागत को कम करता है, लेकिन यह निष्पादन के दौरान उतना कुशल नहीं हो सकता है।
इसके विपरीत, रन पैरामीटर के बड़े मान निष्पादन लागत को प्राथमिकता देते हैं। यह बड़े निर्माण कोड की ओर जाता है लेकिन एक अनुकूलित (optimized) रनटाइम कोड जो निष्पादित करने के लिए अधिक सस्ता होता है। हालाँकि यह डिप्लॉयमेंट गैस लागत को महत्वपूर्ण रूप से प्रभावित नहीं कर सकता है, लेकिन यह निष्पादन के दौरान गैस लागत को काफी कम कर सकता है।
इस ट्रेड-ऑफ़ पर विचार करते हुए, यदि आपके कॉन्ट्रैक्ट का बार-बार उपयोग किया जाएगा तो ऑप्टिमाइज़र के लिए एक बड़े मान का उपयोग करने की सलाह दी जाती है। क्योंकि यह लंबी अवधि में गैस की लागत बचाएगा।
14. अत्यधिक उपयोग किए जाने वाले फ़ंक्शन्स के नाम ऑप्टिमल होने चाहिए
EVM फ़ंक्शन कॉल के लिए एक जंप टेबल (jump table) का उपयोग करता है, और कम हेक्साडेसिमल क्रम (hexadecimal order) वाले फ़ंक्शन चयनकर्ताओं (selectors) को उच्च हेक्स क्रम वाले चयनकर्ताओं पर पहले क्रमबद्ध किया जाता है। दूसरे शब्दों में, यदि दो फ़ंक्शन चयनकर्ता, उदाहरण के लिए, 0x000071c3 और 0xa0712d68, एक ही कॉन्ट्रैक्ट में मौजूद हैं, तो कॉन्ट्रैक्ट निष्पादन के दौरान 0xa0712d68 वाले फ़ंक्शन से पहले चयनकर्ता 0x000071c3 वाले फ़ंक्शन की जांच की जाएगी।
इसलिए, यदि किसी फ़ंक्शन का अक्सर उपयोग किया जाता है, तो उसका नाम ऑप्टिमल होना आवश्यक है। यह ऑप्टिमाइज़ेशन इसके पहले क्रमबद्ध (sorted) होने की संभावना को बढ़ाता है, इस प्रकार आगे की जांच से गैस की लागत बचाता है (हालाँकि यदि कॉन्ट्रैक्ट में चार से अधिक फ़ंक्शन्स हैं, तो EVM रैखिक खोज (linear search) के बजाय जंप टेबल के लिए द्विआधारी खोज (binary search) करता है)।
यह calldata लागत को भी कम करता है (यदि फ़ंक्शन में अग्रणी शून्य (leading zeros) हैं, क्योंकि शून्य बाइट्स की कीमत 4 गैस होती है, और गैर-शून्य बाइट्स की कीमत 16 गैस होती है)।
यहाँ नीचे एक अच्छा डेमो है।
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
contract FunctionWithLeadingZeros {
uint256 public totalSupply;
// selector = 0xa0712d68
function mint(uint256 amount) public {
totalSupply += amount;
}
// selector = 0x000071c3 (this cheaper than the above function)
function mint_184E17(uint256 amount) public {
totalSupply += amount;
}
}
इसके अलावा, हमारे पास Solidity Zero Finder नामक एक सहायक टूल है जिसे Rust के साथ बनाया गया था जो डेवलपर्स को इसे हासिल करने में सहायता कर सकता है। यह इस GitHub repository पर उपलब्ध है।
15. Bitshifting दो की शक्ति से गुणा या भाग करने की तुलना में सस्ता है
Solidity में, गुणा या भाग ऑपरेटरों का उपयोग करने के बजाय, संख्याओं को जो दो की शक्तियां (powers of two) हैं, उनके बिट्स को शिफ्ट करके गुणा या भाग करना अक्सर अधिक गैस कुशल होता है।
उदाहरण के लिए, निम्नलिखित दो एक्सप्रेशन्स समान हैं
10 * 2
10 << 1 # shift 10 left by 1
और यह भी समान है
8 / 4
8 >> 2 # shift 8 right by 2
EVM में बिट शिफ्टिंग ऑपरेशन्स opcodes, जैसे shr (shift right) और shl (shift left), की लागत 3 गैस होती है जबकि गुणा और भाग ऑपरेशन्स (mul और div) की लागत 5 गैस प्रत्येक होती है।
गैस की अधिकांश बचत इस तथ्य से भी आती है कि solidity shr और shl ऑपरेशन्स के लिए ओवरफ़्लो/अंडरफ़्लो या भाग द्वारा जांच नहीं करता है। इन ऑपरेटरों का उपयोग करते समय इसे ध्यान में रखना महत्वपूर्ण है ताकि ओवरफ़्लो और अंडरफ़्लो बग (bugs) न हों।
16. Calldata को कैश करना कभी-कभी सस्ता होता है
यद्यपि calldataload निर्देश (instruction) एक सस्ता opcode है, यदि आप calldataload को कैश करते हैं तो solidity कंपाइलर कभी-कभी सस्ता कोड आउटपुट करेगा। यह हमेशा ऐसा नहीं होगा, इसलिए आपको दोनों संभावनाओं का परीक्षण करना चाहिए।
contract LoopSum {
function sumArr(uint256[] calldata arr) public pure returns (uint256 sum) {
uint256 len = arr.length;
for (uint256 i = 0; i < len; ) {
sum += arr[i];
unchecked {
++i;
}
}
}
}
17. Conditionals और loops के प्रतिस्थापन के रूप में branchless एल्गोरिदम का उपयोग करें
पहले के अनुभाग से max कोड एक branchless एल्गोरिदम का एक उदाहरण है, यानी यह JUMP opcode को समाप्त कर देता है, जो सामान्य रूप से अंकगणितीय (arithmetic) opcodes की तुलना में अधिक महंगा है।
For लूप्स में जंप्स (jumps) बने होते हैं, इसलिए आप गैस बचाने के लिए loop unrolling पर विचार कर सकते हैं।
लूप्स को पूरी तरह से अनरोल (unroll) करने की आवश्यकता नहीं है। उदाहरण के लिए, आप एक बार में दो आइटम एक लूप को निष्पादित कर सकते हैं और जंप्स की संख्या को आधा कर सकते हैं।
यह एक बहुत ही चरम ऑप्टिमाइज़ेशन है, लेकिन आपको इस बात की जानकारी होनी चाहिए कि सशर्त जंप्स (conditional jumps) और लूप्स थोड़ा अधिक महंगा opcode पेश करते हैं।
18. केवल एक बार उपयोग किए जाने वाले Internal functions को गैस बचाने के लिए inline किया जा सकता है
Internal functions का होना ठीक है, हालाँकि वे बाइटकोड में अतिरिक्त जंप लेबल्स पेश करते हैं।
इसलिए, ऐसे मामले में जहाँ इसका उपयोग केवल एक फ़ंक्शन द्वारा किया जाता है, internal function के तर्क (logic) को उस फ़ंक्शन के अंदर inline करना बेहतर होता है जहाँ इसका उपयोग किया जा रहा है। यह फ़ंक्शन निष्पादन के दौरान जंप्स से बचकर कुछ गैस बचाएगा।
19. Array समानता और string समानता की तुलना उन्हें हैश करके करें यदि वे 32 बाइट्स से लंबे हैं
यह एक ऐसी ट्रिक है जिसका आप शायद ही कभी उपयोग करेंगे, लेकिन arrays या strings पर लूप करना उन्हें हैश करने और हैश की तुलना करने की तुलना में बहुत अधिक महंगा है।
20. शक्तियों (powers) और लघुगणक (logarithms) की गणना करते समय lookup tables का उपयोग करें
यदि आपको लघुगणक या शक्तियां लेने की आवश्यकता है जहाँ आधार (base) या शक्ति एक भिन्न (fraction) है, तो एक तालिका (table) को पूर्व-गणना (precompute) करना बेहतर हो सकता है यदि आधार या शक्ति तय है। उदाहरण के रूप में Bancor Formula और Uniswap V3 Tick Math पर विचार करें।
21. Precompiled contracts कुछ गुणा या memory ऑपरेशन्स के लिए उपयोगी हो सकते हैं।
Ethereum precompiled contracts मुख्य रूप से क्रिप्टोग्राफ़ी के लिए ऑपरेशन्स प्रदान करते हैं, लेकिन यदि आपको एक मापांक (modulus) पर बड़ी संख्याओं को गुणा करने या memory के महत्वपूर्ण हिस्से की प्रतिलिपि (copy) बनाने की आवश्यकता है, तो precompiles का उपयोग करने पर विचार करें। ध्यान रखें कि यह आपके एप्लिकेशन को कुछ layer 2s के साथ असंगत (incompatible) बना सकता है।
22. n * n * n, n ** 3 से सस्ता हो सकता है
दो MUL ऑप कोड की कुल लागत 10 गैस है, लेकिन EXP ऑप कोड की लागत 10 गैस + 50 * (बाइट्स में एक्सपोनेंट आकार) है।
खतरनाक तकनीकें (Dangerous techniques)
यदि आप गैस ऑप्टिमाइज़ेशन प्रतियोगिता में भाग ले रहे हैं, तो ये असामान्य डिज़ाइन पैटर्न मदद कर सकते हैं, लेकिन उत्पादन (production) में उनका उपयोग करना अत्यधिक हतोत्साहित (discouraged) किया जाता है, या कम से कम अत्यधिक सावधानी के साथ किया जाना चाहिए।
1. जानकारी पास करने के लिए gasprice() या msg.value का उपयोग करें
किसी फ़ंक्शन में पैरामीटर्स पास करने पर कम से कम 128 गैस जुड़ेगी, क्योंकि calldata के प्रत्येक शून्य बाइट की कीमत 4 गैस होती है। हालाँकि, आप इस तरह से नंबर पास करने के लिए gasprice या msg.value मुफ्त में सेट कर सकते हैं। यह निश्चित रूप से उत्पादन में काम नहीं करेगा क्योंकि msg.value पर वास्तविक Ethereum खर्च होता है और यदि आपकी गैस कीमत बहुत कम है, तो ट्रांज़ैक्शन पूरा नहीं होगा, या क्रिप्टोकरेंसी बर्बाद हो जाएगी।
2. यदि परीक्षण इसकी अनुमति देते हैं तो coinbase() या block.number जैसे पर्यावरण वेरिएबल्स में हेरफेर करें
यह निश्चित रूप से उत्पादन में काम नहीं करेगा, लेकिन यह स्मार्ट कॉन्ट्रैक्ट के व्यवहार को संशोधित करने के लिए एक साइड चैनल के रूप में काम कर सकता है।
3. प्रमुख बिंदुओं पर निर्णयों को ब्रांच करने के लिए gasleft() का उपयोग करें
जैसे-जैसे निष्पादन आगे बढ़ता है गैस का उपयोग होता जाता है, इसलिए यदि आप किसी निश्चित बिंदु के बाद एक लूप को समाप्त (terminate) करना चाहते हैं या निष्पादन के बाद के भाग में व्यवहार को बदलना चाहते हैं, तो आप निर्णय लेने (decision making) को ब्रांच करने के लिए gasprice() कार्यक्षमता का उपयोग कर सकते हैं। gasleft() “मुफ्त” में कम (decrements) होता है इसलिए इससे गैस बचती है।
4. Ether को स्थानांतरित करने के लिए send() का उपयोग करें, लेकिन सफलता की जांच न करें
Send और transfer के बीच अंतर यह है कि यदि ट्रांसफर विफल हो जाता है तो transfer revert कर देता है, लेकिन send गलत (false) रिटर्न करता है। हालाँकि, आप बस send के रिटर्न मान को अनदेखा (ignore) कर सकते हैं, और इसके परिणामस्वरूप कम ऑप कोड (op codes) होंगे। रिटर्न मानों को अनदेखा करना एक बहुत ही बुरा अभ्यास है, और यह शर्म की बात है कि कंपाइलर आपको ऐसा करने से नहीं रोकता है। उत्पादन प्रणालियों (production systems) में, गैस सीमा के कारण आपको बिल्कुल भी send() का उपयोग नहीं करना चाहिए।
5. सभी फ़ंक्शन्स को payable बनाएं
यह एक विवादास्पद ऑप्टिमाइज़ेशन है क्योंकि यह एक ट्रांज़ैक्शन में अप्रत्याशित अवस्था परिवर्तन (state change) की अनुमति देता है, और बहुत गैस नहीं बचाता है। लेकिन गैस प्रतियोगिता के संदर्भ में, उन अतिरिक्त opcodes से बचने के लिए सभी फ़ंक्शन्स को payable बनाएं जो जांचते हैं कि क्या msg.value शून्य (nonzero) नहीं है।
जैसा कि पहले बताया गया है, constructor या एडमिन फ़ंक्शन्स को payable पर सेट करना गैस बचाने का एक वैध तरीका है, क्योंकि संभवतः डिप्लॉयर और एडमिन जानते हैं कि वे क्या कर रहे हैं और ether भेजने की तुलना में अधिक विनाशकारी चीजें कर सकते हैं।
6. External library jumping
Solidity पारंपरिक रूप से यह निर्धारित करने के लिए कि किस फ़ंक्शन का उपयोग करना है, 4 बाइट्स और एक जंप टेबल (jump table) का उपयोग करता है। हालाँकि, कोई (बहुत असुरक्षित रूप से!) केवल एक calldata तर्क के रूप में जंप डेस्टिनेशन (jump destination) की आपूर्ति कर सकता है, “फ़ंक्शन चयनकर्ता” (function selector) को एक बाइट तक कम कर सकता है और जंप टेबल से पूरी तरह से बच सकता है। अधिक जानकारी इस tweet में देखी जा सकती है।
7. एक अत्यधिक अनुकूलित सबरूटीन (subroutine) बनाने के लिए कॉन्ट्रैक्ट के अंत में बाइटकोड जोड़ें
कुछ कम्प्यूटेशनल रूप से गहन एल्गोरिदम, जैसे हैश फ़ंक्शन्स, Solidity, या यहाँ तक कि Yul के बजाय कच्चे बाइटकोड (raw bytecode) में बेहतर तरीके से लिखे जाते हैं। उदाहरण के लिए, Tornado Cash MiMC हैश फ़ंक्शन को एक अलग स्मार्ट कॉन्ट्रैक्ट के रूप में लिखता है, जिसे सीधे कच्चे बाइटकोड में लिखा जाता है। किसी अन्य स्मार्ट कॉन्ट्रैक्ट की अतिरिक्त 2,600 या 100 गैस लागत (कोल्ड या वार्म एक्सेस) से बचने के लिए, उस बाइटकोड को वास्तविक कॉन्ट्रैक्ट में जोड़ें और उससे आगे-पीछे कूदें (jumping back and forth)। यहाँ Huff का उपयोग करके एक कॉन्सेप्ट का प्रमाण (proof of concept) है।
पुरानी ट्रिक्स (Outdated tricks)
1. external, public से सस्ता है
स्पष्टता (clarity) के लिए आपको अभी भी external modifier को प्राथमिकता देनी चाहिए यदि फ़ंक्शन को कॉन्ट्रैक्ट के अंदर नहीं बुलाया जा सकता है, लेकिन गैस की बचत पर इसका कोई प्रभाव नहीं पड़ता है।
2. != 0, > 0 से सस्ता है
लगभग solidity 0.8.12 या उसके आसपास, यह सच होना बंद हो गया। यदि आपको पुराने वर्ज़न का उपयोग करने के लिए मजबूर किया जाता है, तो आप अभी भी इसे बेंचमार्क कर सकते हैं।
बुरी प्रथाएँ (Bad Practices)
डेवलपर्स कई सामान्य गलतियाँ करते हैं जिससे उच्च गैस लागत आती है। स्थान की कमी के कारण, हमने इस सूची को एक अलग लेख में प्रकाशित किया है।
RareSkills के साथ और जानें
जब आप एक प्रेरित समुदाय (motivated community) से घिरे होते हैं और अनुभवी प्रशिक्षकों द्वारा निर्देशित होते हैं तो सीखना हमेशा अधिक प्रभावी होता है। यह सामग्री हमारे उन्नत (advanced) solidity bootcamp का हिस्सा है। यदि आप उद्योग के नेताओं (industry leaders) के मार्गदर्शन में अन्य solidity पेशेवरों के साथ गैस ऑप्टिमाइज़ेशन का अभ्यास करना चाहते हैं, तो कार्यक्रम देखें!
मूल रूप से प्रकाशित सितंबर 7, 2023 (Originally Published September 7, 2023)