समग्र कुंजियाँ तब होती हैं जब आपके मानचित्र या कैश लुकअप के लिए "कुंजी" को परिभाषित करने के लिए डेटा के संयोजन की आवश्यकता होती है। इसका एक उदाहरण यह हो सकता है कि आपको ग्राहक के नाम के साथ-साथ उपयोगकर्ता की भूमिका के आधार पर मूल्यों को कैश करने की आवश्यकता है। इस तरह के मामले में, आपके कैश को इन दो (या अधिक) मानदंडों में से प्रत्येक के आधार पर अद्वितीय मान संग्रहीत करने में सक्षम होने की आवश्यकता होगी।
कोड में मिश्रित कुंजियों को कुछ अलग-अलग तरीकों से संभाला जा सकता है।
पहला उत्तर जिस पर सबसे अधिक ध्यान दिया जाता है वह कुंजी के रूप में उपयोग करने के लिए मानदंडों को एक स्ट्रिंग में संयोजित करना है। यह सरल है और इसमें अधिक मेहनत नहीं लगती:
private String getMapKey(Long userId, String userLocale) { return userId + "." userLocale; }
यह समस्या से निपटने का एक बहुत ही बुनियादी तरीका है। स्ट्रिंग कुंजी का उपयोग करने से डिबगिंग और जांच आसान हो सकती है, क्योंकि कैश कुंजी मानव-पठनीय प्रारूप में है। लेकिन इस दृष्टिकोण से अवगत होने के लिए कुछ समस्याएं हैं:
इसके लिए मानचित्र के साथ प्रत्येक इंटरैक्शन पर एक नई स्ट्रिंग बनाने की आवश्यकता होती है। हालाँकि यह स्ट्रिंग आवंटन आम तौर पर छोटा होता है, यदि मानचित्र को बार-बार एक्सेस किया जाता है, तो इससे बड़ी संख्या में आवंटन हो सकते हैं जिनमें समय लगता है और कचरा एकत्र करने की आवश्यकता होती है। आपकी कुंजी के घटक कितने बड़े हैं या आपके पास कितने हैं, इसके आधार पर स्ट्रिंग आवंटन का आकार भी बड़ा हो सकता है।
आपको यह सुनिश्चित करना होगा कि आपके द्वारा बनाई गई समग्र कुंजी किसी अन्य कुंजी मान में धोखा न दे सके:
public String getMapKey(Integer groupId, Integer accessType) { return groupId.toString() + accessType.toString(); }
उपरोक्त में, यदि आपके पास ग्रुपआईडी = 1 और एक्सेसटाइप = 23 है, तो वह ग्रुपआईडी = 12 और एक्सेसटाइप = 3 के समान कैश कुंजी होगी। स्ट्रिंग्स के बीच एक विभाजक वर्ण जोड़कर, आप इस तरह के ओवरलैप को रोक सकते हैं। लेकिन कुंजी के वैकल्पिक भागों के बारे में सावधान रहें:
public String getMapKey(String userProvidedString, String extensionName) { return userProvidedString + (extensionName == null ? "" : ("." + extensionName)); }
उपरोक्त उदाहरण में, एक्सटेंशननाम कुंजी का एक वैकल्पिक हिस्सा है। यदि एक्सटेंशननाम वैकल्पिक है, तो userProvidedString में एक विभाजक और वैध एक्सटेंशननाम शामिल हो सकता है और कैश डेटा तक पहुंच प्राप्त हो सकती है, जिस तक इसकी पहुंच नहीं होनी चाहिए।
स्ट्रिंग्स का उपयोग करते समय, आप यह सोचना चाहेंगे कि कुंजियों में किसी भी टकराव से बचने के लिए आप अपने डेटा को कैसे संयोजित कर रहे हैं। विशेष रूप से कुंजी के लिए किसी भी उपयोगकर्ता-जनित इनपुट के आसपास।
एक अन्य विकल्प यह है कि कुंजियों को बिल्कुल भी संयोजित न करें, और इसके बजाय, अपनी डेटा संरचनाओं को नेस्ट करें (मानचित्र के मानचित्र के मानचित्र):
Map<Integer, Map<String, String>> groupAndLocaleMap = new HashMap<>(); groupAndLocaleMap.computeIfAbsent(userId, k -> new HashMap()).put(userLocale, mapValue);
इसका फायदा यह है कि मानचित्रों के साथ इंटरैक्ट करते समय किसी भी नई मेमोरी को आवंटित करने की आवश्यकता नहीं होती है क्योंकि कुंजियों के लिए पारित मान पहले से ही आवंटित होते हैं। और जबकि आपको अंतिम मूल्य तक पहुंचने के लिए कई लुकअप करने की आवश्यकता होगी, मानचित्र छोटे होंगे।
लेकिन इस दृष्टिकोण का नकारात्मक पक्ष यह है कि घोंसला बनाने की प्रक्रिया जितनी गहराई तक जाती है, यह और अधिक जटिल होती जाती है। यहां तक कि केवल दो स्तरों के साथ, मानचित्र आरंभीकरण भ्रमित करने वाला लग सकता है। जब आप डेटा के 3 या अधिक टुकड़ों के साथ काम करना शुरू करते हैं, तो इससे आपका कोड बहुत अधिक जटिल हो सकता है। इसके अलावा, प्रत्येक स्तर पर शून्य संकेतकों से बचने के लिए शून्य जाँच की आवश्यकता होती है।
कुछ "मुख्य भाग" भी मानचित्र कुंजी के रूप में अच्छी तरह से काम नहीं कर सकते हैं। सारणियों या संग्रहों में डिफ़ॉल्ट बराबर विधियाँ नहीं होती हैं जो उनकी सामग्री की तुलना करती हैं। इसलिए, आपको या तो उन्हें लागू करना होगा या किसी अन्य विकल्प का उपयोग करना होगा।
आपकी कुंजियों का प्रत्येक स्तर कितना अद्वितीय है, इसके आधार पर नेस्टेड मानचित्रों का उपयोग कम स्थान कुशल हो सकता है।
अंतिम विकल्प कुंजी मानों को एक स्ट्रिंग में संयोजित करने के बजाय, कुंजी के लिए एक कस्टम ऑब्जेक्ट बनाना है:
private class MapKey { private final int userId; private final String userLocale; public MapKey(int userId, String userLocale) { this.userId = userId; this.userLocale = userLocale; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MapKey mapKey = (MapKey) o; return userId == mapKey.userId && Objects.equals(userLocale, mapKey.userLocale); } @Override public int hashCode() { return Objects.hash(userId, userLocale); } }
जबकि प्रत्येक इंटरैक्शन के लिए अभी भी एक नई वस्तु के लिए एक नई मेमोरी आवंटन की आवश्यकता होती है। ऑब्जेक्ट कुंजी आवंटन समग्र स्ट्रिंग के लिए आवश्यक आवंटन से काफी छोटा है। इसका कारण यह है कि कुंजी बनाने वाले भागों को स्ट्रिंग के रूप में पुनः आवंटित करने की आवश्यकता नहीं है। इसके बजाय, केवल रैपिंग ऑब्जेक्ट कुंजी को नई मेमोरी की आवश्यकता होती है।
एक समग्र कुंजी ऑब्जेक्ट कुंजी समानता और हैशकोड कार्यान्वयन में अनुकूलन की भी अनुमति दे सकता है। जैसे किसी स्ट्रिंग में बड़े अक्षरों को अनदेखा करना, या किसी कुंजी के भाग के रूप में किसी सरणी या संग्रह का उपयोग करना।
हालाँकि, यहाँ नकारात्मक पक्ष यह है कि, इसके लिए एक मिश्रित स्ट्रिंग की तुलना में बहुत अधिक कोड की आवश्यकता होती है। और इसके लिए यह सुनिश्चित करना आवश्यक है कि आपके मानचित्र के लिए कुंजी वर्ग में वैध समान और हैशकोड अनुबंध हों।
तो मुझे किसे चुनना चाहिए?
सामान्यतया, मैं एक समग्र स्ट्रिंग कुंजी का उपयोग करने का सुझाव दूंगा। यह सरल और समझने में आसान है, इसके लिए कम से कम कोड की आवश्यकता होती है, और बाद में डीबग करना सबसे आसान है। हालाँकि यह संभवतः सबसे धीमा प्रदर्शन करने वाला है, सरल, पठनीय कोड लिखना आम तौर पर अन्य दो विकल्पों में से किसी एक का उपयोग करने से मिलने वाले लाभों से अधिक महत्वपूर्ण है। याद करना:
"समय से पहले अनुकूलन सभी बुराइयों की जड़ है" डोनाल्ड नुथ
यदि आपके पास इस बात पर विश्वास करने का कोई सबूत या कारण नहीं है कि आपका मैप/कैश लुकअप एक प्रदर्शन बाधा बनने जा रहा है, तो पठनीयता के साथ आगे बढ़ें।
लेकिन यदि आप ऐसे परिदृश्य में हैं जहां आपके मानचित्र या कैश का थ्रूपुट बहुत अधिक है, तो अन्य दो विकल्पों में से किसी एक पर स्विच करना अच्छा हो सकता है। आइए देखें कि ये तीनों प्रदर्शन के साथ-साथ अपने मेमोरी आवंटन आकार के मामले में एक-दूसरे से कैसे तुलना करते हैं।
उपरोक्त 3 परिदृश्यों का परीक्षण करने के लिए, मैंने कोड लिखा जो एक समग्र कुंजी के लिए सभी 3 परिदृश्यों के समान कार्यान्वयन को दोहराएगा। कुंजी में स्वयं एक पूर्णांक मान, एक स्ट्रिंग मान और एक लंबा मान होता है। सभी तीन कार्यान्वयनों ने कुंजियाँ बनाने के लिए प्रत्येक रन पर समान परीक्षण डेटा का उपयोग किया।
सभी रन मानचित्र में 1 मिलियन रिकॉर्ड के साथ निष्पादित किए गए (जावा के हैशमैप का उपयोग किया गया था)। कुंजी आकारों के विभिन्न संयोजनों के साथ कुंजी बनाने में 3 रन लगे:
100 इनट्स, 100 स्ट्रिंग्स, 100 लॉन्ग - 1 मिलियन अद्वितीय कुंजियाँ
1 इंट, 1 स्ट्रिंग, 1,000,000 लॉन्ग- 1 मिलियन अद्वितीय कुंजियाँ
1,000,000 इनट्स, 1 स्ट्रिंग, 1 लंबी - 1 मिलियन अद्वितीय कुंजियाँ
सबसे पहले, आइए देखें कि प्रत्येक मानचित्र ढेर में कितनी जगह घेरता है। यह महत्वपूर्ण है क्योंकि यह प्रभावित करता है कि आपके एप्लिकेशन को चलाने के लिए कितनी मेमोरी की आवश्यकता है।
यहां एक दिलचस्प बात स्पष्ट है: अंतिम परिदृश्य (1,000,000 इंच) में, नेस्टेड मानचित्रों का आकार अन्य की तुलना में काफी बड़ा है। ऐसा इसलिए है, क्योंकि इस परिदृश्य में, नेस्टेड मानचित्र 1 मिलियन प्रविष्टियों के साथ 1 प्रथम-स्तरीय मानचित्र बनाते हैं। फिर, दूसरे और तीसरे स्तर के लिए, यह केवल एक प्रविष्टि के साथ 1 मिलियन मानचित्र बनाता है।
वे सभी नेस्टेड मानचित्र अतिरिक्त ओवरहेड संग्रहीत करते हैं और अधिकतर खाली होते हैं। यह स्पष्ट रूप से एक किनारे का मामला है, लेकिन मैं इसे एक मुद्दा बनाने के लिए दिखाना चाहता था। नेस्ट मानचित्र कार्यान्वयन का उपयोग करते समय, विशिष्टता (और उस विशिष्टता का क्रम) बहुत मायने रखती है।
यदि आप ऑर्डर को 1, 1, 1 मिलियन तक उलट देते हैं, तो आपको वास्तव में सबसे कम भंडारण आवश्यकता मिलती है।
अन्य दो परिदृश्यों में, नेस्टेड मैपिंग सबसे कुशल है, जिसमें कस्टम कुंजी ऑब्जेक्ट दूसरे स्थान पर है, और स्ट्रिंग कुंजियाँ अंतिम स्थान पर हैं।
इसके बाद, आइए देखें कि इनमें से प्रत्येक मानचित्र को आरंभ से बनाने में कितना समय लगता है:
फिर से, हम देखते हैं कि नेस्टेड मानचित्र मेमोरी आवंटन के लिए 1 मिलियन-1-1 परिदृश्य में सबसे खराब प्रदर्शन करते हैं, लेकिन फिर भी, यह सीपीयू समय में दूसरों से बेहतर प्रदर्शन करता है। उपरोक्त में, हम यह भी देख सकते हैं कि स्ट्रिंग कुंजी सभी मामलों में सबसे खराब प्रदर्शन करती है जबकि कस्टम कुंजी ऑब्जेक्ट का उपयोग थोड़ा धीमा होता है और नेस्टेड कुंजी की तुलना में अधिक मेमोरी आवंटन की आवश्यकता होती है।
अंत में, आइए उच्चतम थ्रूपुट परिदृश्य को देखें और यह पढ़ने में कितना प्रभावी है। हमने 1 मिलियन रीड ऑपरेशन चलाए (प्रत्येक बनाई गई कुंजी के लिए 1); हमने कोई भी अस्तित्वहीन कुंजी शामिल नहीं की।
यह वह जगह है जहां हम वास्तव में देखते हैं कि स्ट्रिंग-आधारित कुंजी लुकअप कितना धीमा है। यह अब तक का सबसे धीमा है और अब तक के 3 विकल्पों में से किसी एक में सबसे अधिक मेमोरी आवंटित करता है। कस्टम कुंजी ऑब्जेक्ट नेस्टेड मानचित्र कार्यान्वयन के "करीब" कार्य करता है लेकिन अभी भी एक छोटे अंतर से लगातार धीमा है।
हालाँकि, लुकअप मेमोरी आवंटन में, ध्यान दें कि नेस्टेड मानचित्र कितनी अच्छी तरह चमकते हैं। नहीं, यह ग्राफ़ में कोई गड़बड़ी नहीं है; नेस्टेड मानचित्रों में किसी मान को खोजने के लिए लुकअप करने के लिए किसी अतिरिक्त मेमोरी आवंटन की आवश्यकता नहीं होती है। वह कैसे संभव है?
खैर, मिश्रित ऑब्जेक्ट को एक स्ट्रिंग कुंजी में संयोजित करते समय, आपको हर बार एक नई स्ट्रिंग ऑब्जेक्ट के लिए मेमोरी आवंटित करने की आवश्यकता होती है:
private String lookup(int key1, String key2, long key3) { return map.get(key1 + "." + key2 + "." + key3); }
मिश्रित कुंजी का उपयोग करते समय, आपको अभी भी एक नई कुंजी ऑब्जेक्ट के लिए मेमोरी आवंटित करने की आवश्यकता होती है। लेकिन क्योंकि उस ऑब्जेक्ट के सदस्य पहले ही बनाए और संदर्भित किए जा चुके हैं, यह अभी भी एक नई स्ट्रिंग से बहुत कम आवंटित करता है:
private String lookup(int key1, String key2, long key3) { return map.get(new MapKey(key1, key2, key3)); }
लेकिन नेस्टेड मानचित्र कार्यान्वयन के लिए लुकअप पर किसी नए मेमोरी आवंटन की आवश्यकता नहीं है। आप प्रत्येक नेस्टेड मानचित्र की कुंजी के रूप में दिए गए भागों का पुन: उपयोग कर रहे हैं:
private String lookup(int key1, String key2, long key3) { return map.get(key1).get(key2).get(key3); }
तो, उपरोक्त के आधार पर, सबसे अधिक प्रदर्शन करने वाला कौन सा है?
यह देखना आसान है कि नेस्टेड मानचित्र लगभग सभी परिदृश्यों में शीर्ष पर आते हैं। यदि आप अधिकांश उपयोग के मामलों में कच्चे प्रदर्शन की तलाश में हैं, तो यह संभवतः सबसे अच्छा विकल्प है। हालाँकि, आपको अपने उपयोग के मामलों की पुष्टि के लिए अपना स्वयं का परीक्षण करना चाहिए।
जब नेस्टेड मानचित्र आपके कार्यान्वयन के लिए उपयोग करना अव्यावहारिक या असंभव हो जाते हैं तो मुख्य वस्तु एक बहुत अच्छा सामान्य-उद्देश्य विकल्प बन जाती है। और समग्र स्ट्रिंग कुंजी, हालांकि कार्यान्वयन में सबसे आसान है, लगभग हमेशा सबसे धीमी होने वाली है।
समग्र कुंजियाँ लागू करते समय विचार करने योग्य अंतिम बिंदु यह है कि आप उपरोक्त को जोड़ सकते हैं। उदाहरण के लिए, आप पहले या दो स्तरों के लिए नेस्टेड मानचित्रों का उपयोग कर सकते हैं, और फिर गहरे स्तरों को सरल बनाने के लिए एक समग्र कुंजी ऑब्जेक्ट का उपयोग कर सकते हैं।
यह स्टोरेज और लुकअप प्रदर्शन को अनुकूलित करते हुए आपके डेटा को तेज़ लुकअप के लिए विभाजित रख सकता है। और अपने कोड को पढ़ने योग्य भी रखें।
अधिकांश समय, इसे सरल रखें। यदि यह सबसे आसान विकल्प है और प्रदर्शन कोई बड़ी चिंता का विषय नहीं है, तो मानचित्र या कैश में भंडारण के लिए अपनी समग्र कुंजियों को एक स्ट्रिंग कुंजी में संयोजित करें।
ऐसे परिदृश्यों में जहां प्रदर्शन महत्वपूर्ण है, अपना परीक्षण स्वयं करना सुनिश्चित करें। लेकिन अधिकांश मामलों में नेस्टेड मानचित्रों का उपयोग सबसे अधिक लाभदायक होगा। इसमें संभवतः सबसे छोटी भंडारण आवश्यकताएं भी होंगी। और जब नेस्टिंग मैपिंग अव्यावहारिक हो जाती है तो समग्र कुंजियाँ अभी भी एक प्रभावी विकल्प हैं।
यहाँ भी प्रकाशित किया गया