Java Language
जावा नुकसान - प्रदर्शन के मुद्दे
खोज…
परिचय
यह विषय कई "नुकसान" का वर्णन करता है (यानी नौसिखिए जावा प्रोग्रामर गलतियाँ करते हैं) जो जावा एप्लिकेशन के प्रदर्शन से संबंधित हैं।
टिप्पणियों
यह विषय कुछ "सूक्ष्म" जावा कोडिंग प्रथाओं का वर्णन करता है जो अक्षम हैं। ज्यादातर मामलों में, अक्षमताएं अपेक्षाकृत कम होती हैं, लेकिन यह अभी भी लायक है कि उनसे बचना संभव है।
नुकसान - लॉग संदेश बनाने का ओवरहेड्स
TRACE
और DEBUG
लॉग स्तर हैं जो रनटाइम पर दिए गए कोड के संचालन के बारे में उच्च विवरण देने में सक्षम हैं। इन के ऊपर लॉग स्तर सेट करने की आमतौर पर सिफारिश की जाती है, हालांकि इन बयानों के लिए कुछ ध्यान रखा जाना चाहिए, भले ही "बंद" होने पर भी प्रदर्शन को प्रभावित न करें।
इस लॉग स्टेटमेंट पर विचार करें:
// Processing a request of some kind, logging the parameters
LOG.debug("Request coming from " + myInetAddress.toString()
+ " parameters: " + Arrays.toString(veryLongParamArray));
यहां तक कि जब लॉग स्तर INFO
सेट किया जाता है, तो लाइन के प्रत्येक निष्पादन पर debug()
लिए दिए गए तर्क का मूल्यांकन किया जाएगा। यह अनावश्यक रूप से कई मायने रखता है:
-
String
संघनन: कईString
इंस्टेंसेस बनाए जाएंगे -
InetAddress
एक DNS लुकअप भी कर सकता है। -
veryLongParamArray
बहुत लंबी हो सकती है - इसमें से एक स्ट्रिंग बनाने से मेमोरी की खपत होती है, समय लगता है
समाधान
अधिकांश लॉगिंग फ़्रेमवर्क फ़िक्स स्ट्रिंग्स और ऑब्जेक्ट संदर्भों का उपयोग करके लॉग संदेश बनाने के लिए साधन प्रदान करते हैं। लॉग संदेश का मूल्यांकन केवल तभी किया जाएगा जब संदेश वास्तव में लॉग किया गया हो। उदाहरण:
// No toString() evaluation, no string concatenation if debug is disabled
LOG.debug("Request coming from {} parameters: {}", myInetAddress, parameters));
यह बहुत अच्छी तरह से काम करता है जब तक कि सभी मापदंडों को स्ट्रिंग के लिए परिवर्तित किया जा सकता है String.valueOf (ऑब्जेक्ट) । यदि लॉग संदेश संकलन अधिक जटिल है, तो लॉग करने से पहले लॉग स्तर की जाँच की जा सकती है:
if (LOG.isDebugEnabled()) {
// Argument expression evaluated only when DEBUG is enabled
LOG.debug("Request coming from {}, parameters: {}", myInetAddress,
Arrays.toString(veryLongParamArray);
}
यहां, LOG.debug()
महंगा Arrays.toString(Obect[])
संगणना तभी संसाधित किया जाता है जब DEBUG
वास्तव में सक्षम हो।
नुकसान - एक लूप में स्ट्रिंग का संघनन पैमाने पर नहीं होता है
एक उदाहरण के रूप में निम्नलिखित कोड पर विचार करें:
public String joinWords(List<String> words) {
String message = "";
for (String word : words) {
message = message + " " + word;
}
return message;
}
यदि words
सूची लंबी है तो दुर्भाग्यपूर्ण यह कोड अक्षम है। समस्या की जड़ यह कथन है:
message = message + " " + word;
प्रत्येक लूप पुनरावृत्ति के लिए, यह कथन एक नया message
स्ट्रिंग बनाता है जिसमें मूल message
स्ट्रिंग में सभी वर्णों की एक प्रतिलिपि होती है जिसमें अतिरिक्त वर्ण जोड़े जाते हैं। यह बहुत सारे अस्थायी तार उत्पन्न करता है, और बहुत सारी नकल करता है।
जब हम joinWords
का विश्लेषण joinWords
, तो यह मानते हुए कि M की औसत लंबाई के साथ N शब्द हैं, हम पाते हैं कि O (N) अस्थायी तार बनाए गए हैं और O (MN 2 ) वर्ण को प्रक्रिया में कॉपी किया जाएगा। एन 2 घटक विशेष रूप से परेशान है।
समस्या 1 इस तरह के लिए सिफारिश की दृष्टिकोण एक प्रयोग है StringBuilder
इस प्रकार स्ट्रिंग संयोजन के बजाय:
public String joinWords2(List<String> words) {
StringBuilder message = new StringBuilder();
for (String word : words) {
message.append(" ").append(word);
}
return message.toString();
}
के विश्लेषण joinWords2
"बढ़ रही है" के ऊपरी खर्चों के कारण लेने की जरूरत StringBuilder
समर्थन सरणी कि बिल्डर के पात्रों रखती है। हालाँकि, यह पता चला है कि बनाई गई नई वस्तुओं की संख्या O (logN) है और कॉपी किए गए वर्णों की संख्या O (MN) वर्ण है। उत्तरार्द्ध में अंतिम toString()
कॉल में कॉपी किए गए वर्ण शामिल हैं।
(इसे शुरू करने के लिए सही क्षमता के साथ StringBuilder
बनाकर इसे आगे ट्यून करना संभव हो सकता है। हालांकि, समग्र जटिलता समान है।)
मूल joinWords
विधि पर लौटते हुए, यह पता चलता है कि महत्वपूर्ण विवरण एक विशिष्ट जावा कंपाइलर द्वारा कुछ इस तरह से अनुकूलित किया जाएगा:
StringBuilder tmp = new StringBuilder();
tmp.append(message).append(" ").append(word);
message = tmp.toString();
हालाँकि, Java कंपाइलर StringBuilder
को लूप से "फहराया" नहीं जाएगा, जैसा कि हमने joinWords2
के कोड में हाथ से किया था।
संदर्भ:
1 - जावा 8 और बाद में, इस विशेष समस्या को हल करने के लिए Joiner
वर्ग का उपयोग किया जा सकता है। हालाँकि, यह वह नहीं है जिसके बारे में यह उदाहरण वास्तव में माना जाता है ।
नुकसान - आदिम आवरण उदाहरण बनाने के लिए 'नए' का उपयोग करना अक्षम है
जावा भाषा आपको Integer
, Boolean
और इतने पर उदाहरण बनाने के लिए new
का उपयोग करने की अनुमति देती है, लेकिन यह आमतौर पर एक बुरा विचार है। ऑटोबॉक्सिंग (जावा 5 और बाद) या valueOf
विधि का उपयोग करना बेहतर है।
Integer i1 = new Integer(1); // BAD
Integer i2 = 2; // BEST (autoboxing)
Integer i3 = Integer.valueOf(3); // OK
स्पष्ट रूप से new Integer(int)
का उपयोग करने का कारण यह है कि यह एक बुरा विचार है (यह JIT बायलर द्वारा अनुकूलित नहीं किया गया है)। इसके विपरीत, जब ऑटोबॉक्सिंग या एक स्पष्ट valueOf
कॉल का उपयोग किया जाता है, तो जावा रनटाइम पहले से मौजूद वस्तुओं के कैश से एक Integer
ऑब्जेक्ट का पुन: उपयोग करने का प्रयास करेगा। हर बार रनटाइम में कैश "हिट" होता है, यह ऑब्जेक्ट बनाने से बचता है। यह ढेर मेमोरी को भी बचाता है और जीसी ओवरहेड्स को वस्तु मंथन के कारण कम करता है।
टिप्पणियाँ:
- हाल ही में जावा कार्यान्वयन में, autoboxing फोन करके कार्यान्वित किया जाता है
valueOf
, और वहाँ के लिए कैश कर रहे हैंBoolean
,Byte
,Short
,Integer
,Long
औरCharacter
। - अभिन्न प्रकारों के लिए कैशिंग व्यवहार जावा भाषा विनिर्देश द्वारा अनिवार्य है।
नुकसान - 'नई स्ट्रिंग (स्ट्रिंग)' को कॉल करना अक्षम है
एक स्ट्रिंग को डुप्लिकेट करने के लिए new String(String)
का उपयोग करना अक्षम है और लगभग हमेशा अनावश्यक है।
- स्ट्रिंग ऑब्जेक्ट अपरिवर्तनीय हैं, इसलिए परिवर्तनों से बचाने के लिए उन्हें कॉपी करने की कोई आवश्यकता नहीं है।
- जावा के कुछ पुराने संस्करणों में,
String
ऑब्जेक्ट अन्यString
ऑब्जेक्ट के साथ बैकिंग सरणियों को साझा कर सकते हैं। उन संस्करणों में, (बड़े) स्ट्रिंग के एक (छोटे) स्ट्रिंग को बनाए रखने और इसे बनाए रखने के द्वारा मेमोरी को लीक करना संभव है। हालाँकि, जावा 7 से,String
बैकिंग सरणियाँ साझा नहीं की जाती हैं।
किसी भी ठोस लाभ के अभाव में, new String(String)
कॉल करना बस बेकार है:
- प्रतिलिपि बनाने में CPU समय लगता है।
- प्रतिलिपि अधिक मेमोरी का उपयोग करती है जो एप्लिकेशन के मेमोरू फ़ुटप्रिंट को बढ़ाती है और / या जीसी ओवरहेड्स को बढ़ाती है।
- स्ट्रिंग
equals(Object)
औरhashCode()
जैसे ऑपरेशन स्ट्रिंग ऑब्जेक्ट्स की प्रतिलिपि बनाने पर धीमी हो सकते हैं।
नुकसान - Calling System.gc () अक्षम है
यह System.gc()
को कॉल करने के लिए एक बुरा विचार है (लगभग हमेशा System.gc()
।
gc()
विधि के लिए javadoc निम्नलिखित निर्दिष्ट करता है:
"
gc
विधि को कॉल करने से पता चलता है कि जावा वर्चुअल मशीन खर्च करने के लिए अप्रयुक्त वस्तुओं को पुनर्चक्रण करने की दिशा में प्रयास करता है ताकि वे मेमोरी का उपयोग कर सकें, जो कि वे वर्तमान में त्वरित पुन: उपयोग के लिए उपलब्ध हैं। जब नियंत्रण विधि कॉल से वापस आती है, तो जावा वर्चुअल मशीन ने पुनः प्राप्त करने का सबसे अच्छा प्रयास किया है। सभी खारिज वस्तुओं से अंतरिक्ष। "
कुछ महत्वपूर्ण बिंदु हैं जो इससे तैयार किए जा सकते हैं:
शब्द "सुझाव" के बजाय (कहता है) "बताता है" का अर्थ है कि जेवीएम सुझाव की अनदेखी करने के लिए स्वतंत्र है। डिफ़ॉल्ट JVM व्यवहार (हालिया रिलीज़) सुझाव का पालन करना है, लेकिन यह JVM को लॉन्च करते समय सेटिंग
-XX:+DisableExplicitGC
द्वारा ओवरराइड किया जा सकता है।वाक्यांश "सभी खारिज वस्तुओं से अंतरिक्ष को पुनः प्राप्त करने का एक सर्वोत्तम प्रयास" से तात्पर्य है कि कॉलिंग
gc
एक "पूर्ण" कचरा संग्रह को ट्रिगर करेगा।
तो System.gc()
एक बुरा विचार क्यों कहा जा रहा है?
सबसे पहले, एक पूर्ण कचरा संग्रह चलाना महंगा है। एक पूर्ण जीसी में प्रत्येक वस्तु का दौरा और "अंकन" शामिल है जो अभी भी उपलब्ध है; यानी हर वह वस्तु जो कचरा नहीं है। यदि आप इसे ट्रिगर करते हैं जब बहुत कचरा इकट्ठा नहीं होता है, तो जीसी अपेक्षाकृत कम लाभ के लिए बहुत काम करता है।
दूसरा, एक पूर्ण कचरा संग्रह उन वस्तुओं के "स्थानीयता" गुणों को परेशान करने के लिए उत्तरदायी है जो संग्रहित नहीं हैं। लगभग एक ही समय में एक ही धागे द्वारा आवंटित की जाने वाली वस्तुएँ स्मृति में एक साथ आबंटित होती हैं। यह अच्छा है। एक ही समय में आवंटित की जाने वाली वस्तुओं के संबंधित होने की संभावना है; यानी एक दूसरे का संदर्भ। यदि आपका एप्लिकेशन उन संदर्भों का उपयोग करता है, तो संभावना यह है कि विभिन्न मेमोरी और पेज कैशिंग प्रभावों के कारण मेमोरी एक्सेस तेज होगी। दुर्भाग्य से, एक पूर्ण कचरा संग्रह वस्तुओं को इधर-उधर ले जाता है ताकि जो वस्तुएं एक बार पास थीं वे अब अलग हो जाएं।
तीसरा, एक पूर्ण कचरा संग्रह चलाने के लिए जब तक संग्रह पूरा नहीं हो जाता, तब तक आप अपने आवेदन को रोक सकते हैं। जबकि यह हो रहा है, आपका आवेदन गैर-उत्तरदायी होगा।
वास्तव में, सबसे अच्छी रणनीति यह है कि जेवीएम को तय करना है कि जीसी को कब चलाना है, और किस तरह का संग्रह चलाना है। यदि आप हस्तक्षेप नहीं करते हैं, तो जेवीएम एक समय और संग्रह प्रकार का चयन करेगा जो कि थ्रूपुट को अनुकूलित करता है या जीसी ठहराव समय को कम करता है।
शुरुआत में हमने कहा "... (लगभग हमेशा) एक बुरा विचार ..."। वास्तव में कुछ परिदृश्य हैं जहां यह एक अच्छा विचार हो सकता है:
यदि आप कुछ कोड के लिए एक यूनिट टेस्ट लागू कर रहे हैं जो कचरा संग्रह संवेदनशील है (जैसे कि कुछ अंतिम रूप से कमजोर या नरम / प्रेत संदर्भों को शामिल करना) तो
System.gc()
को कॉल करना आवश्यक हो सकता है।कुछ इंटरैक्टिव अनुप्रयोगों में, ऐसे समय में विशेष बिंदु हो सकते हैं जहां उपयोगकर्ता को परवाह नहीं होगी यदि कोई कचरा संग्रह ठहराव है। एक उदाहरण एक खेल है जहां "खेल" में प्राकृतिक ठहराव हैं; जैसे जब एक नया स्तर लोड हो रहा है।
नुकसान - आदिम आवरण के प्रकारों का अधिक उपयोग अक्षम है
कोड के इन दो टुकड़ों पर विचार करें:
int a = 1000;
int b = a + 1;
तथा
Integer a = 1000;
Integer b = a + 1;
प्रश्न: कौन सा संस्करण अधिक कुशल है?
उत्तर: दो संस्करण लगभग समान दिखते हैं, लेकिन पहला संस्करण दूसरे की तुलना में बहुत अधिक कुशल है।
दूसरा संस्करण संख्याओं के लिए एक प्रतिनिधित्व का उपयोग कर रहा है जो अधिक स्थान का उपयोग करता है, और पर्दे के पीछे ऑटो-बॉक्सिंग और ऑटो-अनबॉक्सिंग पर निर्भर है। वास्तव में दूसरा संस्करण सीधे निम्नलिखित कोड के बराबर है:
Integer a = Integer.valueOf(1000); // box 1000
Integer b = Integer.valueOf(a.intValue() + 1); // unbox 1000, add 1, box 1001
int
का उपयोग करने वाले अन्य संस्करण की तुलना में, Integer
का उपयोग करने पर स्पष्ट रूप से तीन अतिरिक्त विधि कॉल होते हैं। valueOf
के मामले में, कॉल एक नई Integer
ऑब्जेक्ट बनाने और शुरू करने के लिए जा रहे हैं। इस अतिरिक्त मुक्केबाजी और अनबॉक्सिंग के सभी कार्य पहले संस्करण की तुलना में दूसरे संस्करण को परिमाण को धीमा बनाने की संभावना है।
इसके अलावा, दूसरा संस्करण प्रत्येक valueOf
कॉल में ढेर पर वस्तुओं को आवंटित कर रहा है। जबकि अंतरिक्ष उपयोग प्लेटफ़ॉर्म विशिष्ट है, यह प्रत्येक Integer
ऑब्जेक्ट के लिए 16 बाइट्स के क्षेत्र में होने की संभावना है। इसके विपरीत, int
संस्करण को शून्य अतिरिक्त ढेर स्थान की आवश्यकता है, यह मानते हुए कि a
और b
स्थानीय चर हैं।
एक और बड़ा कारण है कि आदिमताएं तेज़ हैं तो उनका बॉक्सिंग समतुल्य है कि कैसे उनके संबंधित सरणी प्रकार को स्मृति में रखा जाता है।
आप नहीं उठाते, तो int[]
और Integer[]
एक उदाहरण के रूप में, एक के मामले में int[]
int
मान समीपवर्ती बाहर स्मृति में दिए जाते हैं। लेकिन एक Integer[]
के मामले में Integer[]
यह ऐसे मान नहीं हैं जो निर्धारित किए गए हैं, लेकिन Integer
ऑब्जेक्ट्स के संदर्भ (पॉइंटर्स), जिनमें बदले में वास्तविक int
वैल्यू शामिल हैं।
अप्रत्यक्ष स्तर के अतिरिक्त होने के अलावा, यह एक बड़ा टैंक हो सकता है जब मूल्यों पर पुनरावृत्ति होने पर कैश को स्थानीयता की बात आती है। एक int[]
के मामले में, CPU सरणी में सभी मूल्यों को प्राप्त कर सकता है, क्योंकि यह एक बार में कैश है, क्योंकि वे स्मृति में सन्निहित हैं। लेकिन एक Integer[]
के मामले में Integer[]
सीपीयू संभावित रूप से प्रत्येक तत्व के लिए एक अतिरिक्त मेमोरी लाने के लिए है, क्योंकि सरणी में केवल वास्तविक मूल्यों के संदर्भ शामिल हैं।
संक्षेप में, आदिम आवरण के प्रकारों का उपयोग सीपीयू और मेमोरी संसाधनों दोनों में अपेक्षाकृत महंगा है। अनावश्यक रूप से उनका उपयोग करना कुशल में है।
नुकसान - एक नक्शे की चाबियाँ Iterating अक्षम हो सकता है
निम्न उदाहरण कोड की तुलना में यह धीमा है:
Map<String, String> map = new HashMap<>();
for (String key : map.keySet()) {
String value = map.get(key);
// Do something with key and value
}
ऐसा इसलिए है क्योंकि इसके लिए मानचित्र में प्रत्येक कुंजी के लिए मैप लुकअप (गेट get()
विधि की आवश्यकता होती है। यह लुकअप कार्यकुशल नहीं हो सकता है (किसी हैशपॉप में, यह hashCode
को कॉल करने पर जोर देता है, फिर आंतरिक डेटा संरचनाओं में सही बाल्टी को देखता है, और कभी-कभी कॉलिंग equals
भी equals
)। एक बड़े मानचित्र पर, यह एक तुच्छ उपरि नहीं हो सकता है।
इससे बचने का सही तरीका नक्शे की प्रविष्टियों पर चलना है, जो संग्रह विषय में विस्तृत है
यदि कोई संग्रह खाली है, तो परीक्षण के लिए आकार () का उपयोग करना अक्षम है।
जावा कलेक्शन फ्रेमवर्क सभी Collection
वस्तुओं के लिए दो संबंधित तरीके प्रदान करता है:
-
size()
एकCollection
में प्रविष्टियों की संख्या देता है, और -
isEmpty()
विधि सही है अगर (और केवल अगर)Collection
खाली है।
संग्रह खालीपन के लिए परीक्षण करने के लिए दोनों तरीकों का उपयोग किया जा सकता है। उदाहरण के लिए:
Collection<String> strings = new ArrayList<>();
boolean isEmpty_wrong = strings.size() == 0; // Avoid this
boolean isEmpty = strings.isEmpty(); // Best
हालांकि ये दृष्टिकोण समान हैं, कुछ संग्रह कार्यान्वयन आकार को संग्रहीत नहीं करते हैं। इस तरह के संग्रह के लिए, size()
के कार्यान्वयन size()
को प्रत्येक बार आकार के आकार की गणना करने की आवश्यकता होती है। उदाहरण के लिए:
- एक साधारण लिंक्ड लिस्ट क्लास (लेकिन
java.util.LinkedList
नहीं) को तत्वों को गिनने के लिए लिस्ट को ट्रेस करना पड़ सकता है। -
ConcurrentHashMap
वर्ग को मानचित्र के सभी "सेगमेंट" में प्रविष्टियों को योग करने की आवश्यकता होती है। - एक संग्रह के आलसी कार्यान्वयन को तत्वों की गणना करने के लिए पूरे संग्रह को मेमोरी में महसूस करने की आवश्यकता हो सकती है।
इसके विपरीत, एक isEmpty()
विधि को केवल परीक्षण करने की आवश्यकता है यदि संग्रह में कम से कम एक तत्व है। यह तत्वों की गिनती में प्रवेश नहीं करता है।
जबकि size() == 0
हमेशा कम कुशल नहीं होता है जो कि isEmpty()
, यह सही ढंग से लागू होने वाले isEmpty()
लिए size() == 0
से कम कुशल होने के लिए समझ से बाहर है size() == 0
। इसलिए isEmpty()
को प्राथमिकता दी जाती है।
नुकसान - नियमित अभिव्यक्ति के साथ दक्षता की चिंता
नियमित अभिव्यक्ति मिलान एक शक्तिशाली उपकरण है (जावा में, और अन्य संदर्भों में) लेकिन इसमें कुछ कमियां हैं। इनमें से एक है कि नियमित अभिव्यक्तियाँ महंगी हो जाती हैं।
पैटर्न और मिलान इंस्टेंस का पुन: उपयोग किया जाना चाहिए
निम्नलिखित उदाहरण पर विचार करें:
/**
* Test if all strings in a list consist of English letters and numbers.
* @param strings the list to be checked
* @return 'true' if an only if all strings satisfy the criteria
* @throws NullPointerException if 'strings' is 'null' or a 'null' element.
*/
public boolean allAlphanumeric(List<String> strings) {
for (String s : strings) {
if (!s.matches("[A-Za-z0-9]*")) {
return false;
}
}
return true;
}
यह कोड सही है, लेकिन यह अक्षम है। समस्या matches(...)
कॉल में है। हुड के तहत, s.matches("[A-Za-z0-9]*")
इसके बराबर है:
Pattern.matches(s, "[A-Za-z0-9]*")
जो बदले में बराबर है
Pattern.compile("[A-Za-z0-9]*").matcher(s).matches()
Pattern.compile("[A-Za-z0-9]*")
कॉल नियमित अभिव्यक्ति को पार्स करता है, इसका विश्लेषण करता है, और एक Pattern
ऑब्जेक्ट का निर्माण करता है जो regex इंजन द्वारा उपयोग की जाने वाली डेटा संरचना को धारण करता है। यह एक गैर तुच्छ गणना है। फिर s
तर्क को लपेटने के लिए एक Matcher
ऑब्जेक्ट बनाया जाता है। अंत में हम match()
वास्तविक पैटर्न मिलान करने के लिए कहते हैं।
समस्या यह है कि यह कार्य प्रत्येक लूप पुनरावृत्ति के लिए दोहराया जाता है। समाधान कोड को इस प्रकार से पुनर्गठन करना है:
private static Pattern ALPHA_NUMERIC = Pattern.compile("[A-Za-z0-9]*");
public boolean allAlphanumeric(List<String> strings) {
Matcher matcher = ALPHA_NUMERIC.matcher("");
for (String s : strings) {
matcher.reset(s);
if (!matcher.matches()) {
return false;
}
}
return true;
}
ध्यान दें कि Pattern
स्टेट्स के लिए javadoc :
इस वर्ग के उदाहरण अपरिवर्तनीय हैं और कई समवर्ती धागों द्वारा उपयोग के लिए सुरक्षित हैं।
Matcher
वर्ग के उदाहरण ऐसे उपयोग के लिए सुरक्षित नहीं हैं।
मैच का उपयोग तब न करें (जब आपको खोज का उपयोग करना चाहिए)
आप परीक्षण करना चाहते हैं तो एक स्ट्रिंग मान लीजिए s
एक पंक्ति में तीन या अधिक अंक हैं। आप इसे विभिन्न तरीकों से व्यक्त करते हैं:
if (s.matches(".*[0-9]{3}.*")) {
System.out.println("matches");
}
या
if (Pattern.compile("[0-9]{3}").matcher(s).find()) {
System.out.println("matches");
}
पहला एक अधिक संक्षिप्त है, लेकिन यह भी कम कुशल होने की संभावना है। इसके चेहरे पर, पहला संस्करण पैटर्न के खिलाफ पूरे स्ट्रिंग को मिलाने की कोशिश करने वाला है। इसके अलावा, चूंकि "। *" एक "लालची" पैटर्न है, पैटर्न मिलान करने वाले को स्ट्रिंग के अंत तक "उत्सुकता" से आगे बढ़ने की संभावना है, और जब तक यह एक मैच नहीं मिल जाता है तब तक पीछे जाता है।
इसके विपरीत, दूसरा संस्करण बाएं से दाएं की खोज करेगा और जैसे ही यह एक पंक्ति में 3 अंकों का पता लगाएगा, खोज बंद कर देगा।
नियमित अभिव्यक्ति के लिए अधिक कुशल विकल्पों का उपयोग करें
नियमित अभिव्यक्ति एक शक्तिशाली उपकरण है, लेकिन वे आपके एकमात्र उपकरण नहीं होना चाहिए। बहुत सारे कार्यों को अन्य तरीकों से अधिक कुशलता से किया जा सकता है। उदाहरण के लिए:
Pattern.compile("ABC").matcher(s).find()
के रूप में एक ही बात करता है:
s.contains("ABC")
सिवाय इसके कि उत्तरार्द्ध बहुत अधिक कुशल है। (भले ही आप नियमित अभिव्यक्ति को संकलित करने की लागत को बढ़ा सकते हैं।)
अक्सर, गैर-रेगेक्स रूप अधिक जटिल होता है। उदाहरण के लिए, matches()
द्वारा किए गए परीक्षण matches()
पहले के allAlplanumeric
पद्धति को कॉल करते हैं, इसे इस तरह से फिर से लिखा जा सकता है:
public boolean matches(String s) {
for (char c : s) {
if ((c >= 'A' && c <= 'Z') ||
(c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9')) {
return false;
}
}
return true;
}
अब एक Matcher
का उपयोग करने की तुलना में अधिक कोड है, लेकिन यह भी काफी तेज होने जा रहा है।
प्रलयकारी पीछे
(यह नियमित अभिव्यक्ति के सभी कार्यान्वयनों के साथ संभावित रूप से एक समस्या है, लेकिन हम यहां इसका उल्लेख करेंगे क्योंकि यह Pattern
उपयोग के लिए एक नुकसान है।)
इस पर विचार करें (contrived) उदाहरण:
Pattern pat = Pattern.compile("(A+)+B");
System.out.println(pat.matcher("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB").matches());
System.out.println(pat.matcher("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC").matches());
पहला println
कॉल जल्दी से true
प्रिंट होगा। दूसरा false
छापेगा। अंततः। वास्तव में, यदि आप ऊपर दिए गए कोड के साथ प्रयोग करते हैं, तो आप देखेंगे कि हर बार जब आप C
से पहले A
जोड़ते हैं, तो समय लगेगा दोगुना।
यह व्यवहार भयावह बैकट्रैकिंग का एक उदाहरण है। पैटर्न मिलान इंजन जो रेगेक्स मिलान को लागू करता है, वह सभी संभव तरीकों की कोशिश कर रहा है जो पैटर्न से मेल खा सकते हैं ।
आइए हम देखें कि (A+)+B
वास्तव में क्या मतलब है। सतही तौर पर, यह "एक या एक से अधिक A
वर्णों के बाद B
मान" कहता है, लेकिन वास्तव में यह एक या अधिक समूहों को कहता है, जिनमें से प्रत्येक में एक या अधिक A
वर्ण होते हैं। इसलिए, उदाहरण के लिए:
- 'AB' एक ही तरह से मेल खाता है: '(A) B'
- 'AAB' दो तरह से मेल खाता है: '(AA) B' या '(A) (A) B`
- 'AAAB' चार तरीकों से मेल खाता है: '(AAA) B' या '(AA) B
or '(A)(AA)B
या '(A) (A) (A) B` - और इसी तरह
दूसरे शब्दों में, संभावित मैचों की संख्या 2 N है जहां N A
वर्णों की संख्या है।
उपरोक्त उदाहरण स्पष्ट रूप से वंचित है, लेकिन इस तरह के प्रदर्शन विशेषताओं (यानी O(2^N)
या O(N^K)
को एक बड़े K
लिए प्रदर्शित करने वाले पैटर्न अक्सर तब उत्पन्न होते हैं जब अशुभ नियमित अभिव्यक्तियों का उपयोग किया जाता है। कुछ मानक उपाय हैं:
- अन्य दोहराए जाने वाले पैटर्न के भीतर नेस्टिंग रिपीटिंग पैटर्न से बचें।
- कई दोहराए जाने वाले पैटर्न का उपयोग करने से बचें।
- गैर-बैकट्रैकिंग पुनरावृत्ति का उपयोग उपयुक्त के रूप में करें।
- जटिल पार्सिंग कार्यों के लिए regexes का उपयोग न करें। (इसके बजाय एक उचित पार्सर लिखें।)
अंत में, उन स्थितियों से सावधान रहें जहां एक उपयोगकर्ता या एक एपीआई क्लाइंट पैथोलॉजिकल विशेषताओं के साथ रेगेक्स स्ट्रिंग की आपूर्ति कर सकता है। जिससे आकस्मिक या जानबूझकर "सेवा से वंचित" हो सकता है।
संदर्भ:
- रेगुलर एक्सप्रेशंस टैग, विशेष रूप से http://www.riptutorial.com/regex/topic/259/getting-started-with- अनियमित-expressions/ 977/backtracking # t =201610010339131361163 और http://www.riptutorial.com/ regex / विषय / 259 / आरंभ करने संबंधी-साथ नियमित रूप से-भाव / 4527 / जब-यू-चाहिए-न-उपयोग नियमित रूप से-भाव # टी = 201610010339593564913
- जेफ एटवुड द्वारा "रेगेक्स प्रदर्शन" ।
- "कैसे एक नियमित अभिव्यक्ति के साथ जावा को मारने के लिए" एंड्रियास Haufler द्वारा।
नुकसान - आंतरिक तार ताकि आप उपयोग कर सकते हैं == एक बुरा विचार है
जब कुछ प्रोग्रामर इस सलाह को देखते हैं:
"
==
का उपयोग करते हुए परीक्षण स्ट्रिंग गलत है (जब तक कि तार को नजरबंद नहीं किया जाता है)"
उनकी प्रारंभिक प्रतिक्रिया आंतरिक तारों के लिए है ताकि वे ==
उपयोग कर सकें। (सब के बाद ==
String.equals(...)
कॉल करने से अधिक तेज़ है, यह नहीं है।)
यह गलत दृष्टिकोण है, कई दृष्टिकोणों से:
भंगुरता
सबसे पहले, आप केवल सुरक्षित रूप से ==
उपयोग कर सकते हैं यदि आप जानते हैं कि आपके द्वारा परीक्षण किए जा रहे सभी String
ऑब्जेक्ट्स को नजरबंद कर दिया गया है। JLS गारंटी देता है कि आपके स्रोत कोड में स्ट्रिंग शाब्दिक को नजरबंद कर दिया गया होगा। हालाँकि, कोई भी मानक जावा एसई एपीआई String.intern(String)
अलावा, इंटर्न स्ट्रिंग्स को वापस करने की गारंटी नहीं देता है। यदि आप String
ऑब्जेक्ट्स का सिर्फ एक स्रोत याद करते हैं जिसे नजरबंद नहीं किया गया है, तो आपका आवेदन अविश्वसनीय होगा। यह अविश्वसनीयता अपवादों के बजाय खुद को झूठे नकारात्मक के रूप में प्रकट करेगी जो कि पता लगाने के लिए कठिन बनाने के लिए उत्तरदायी है।
'इंटर्न ()' का उपयोग करने की लागत
हुड के तहत, इंटेनिंग एक हैश तालिका को बनाए रखकर काम करता है जिसमें पहले से रखी गई String
ऑब्जेक्ट्स होते हैं। कुछ प्रकार के कमजोर संदर्भ तंत्र का उपयोग किया जाता है ताकि इंटेंसिंग हैश तालिका भंडारण रिसाव न बने। जबकि हैश टेबल मूल कोड ( HashMap
, HashTable
और इसी तरह के विपरीत) में लागू किया गया है, intern
कॉल अभी भी उपयोग किए जाने वाले सीपीयू और मेमोरी के संदर्भ में अपेक्षाकृत महंगा है।
इस लागत की तुलना हम बचत के साथ करने जा रहे हैं ==
का उपयोग करके equals
बजाय। वास्तव में, हम तब तक भी नहीं तोड़ने जा रहे हैं जब तक कि प्रत्येक इंटर्न स्ट्रिंग की तुलना अन्य स्ट्रिंग्स "कुछ" के साथ न हो।
(एक तरफ: कुछ स्थितियों में जहां इंटर्नशिप करना सार्थक होता है, एक एप्लिकेशन के मेमोरी फुट प्रिंट को कम करने के बारे में होता है, जहां एक ही तार कई बार पीछे हट जाता है, और उन तारों का जीवनकाल लंबा होता है।)
कचरा संग्रहण पर असर
ऊपर वर्णित प्रत्यक्ष सीपीयू और मेमोरी लागतों के अलावा, कचरा कलेक्टर प्रदर्शन पर स्ट्रिंग्स को प्रभावित करता है।
जावा 7 के पूर्व जावा के संस्करणों के लिए, इंटर्नल स्ट्रिंग्स को "पर्मगेन" स्पेस में आयोजित किया जाता है, जिसे अक्सर एकत्र किया जाता है। यदि PermGen को एकत्र करने की आवश्यकता है, तो यह (आमतौर पर) एक पूर्ण कचरा संग्रह को ट्रिगर करता है। अगर PermGen स्पेस पूरी तरह से भर जाता है, JVM क्रैश हो जाता है, भले ही रेग्युलर हीप स्पेस में फ्री स्पेस हो।
Java 7 में, स्ट्रिंग पूल को "PermGen" से सामान्य ढेर में ले जाया गया। हालाँकि, हैश तालिका अभी भी एक लंबे समय तक रहने वाली डेटा संरचना है, जो किसी भी आंतरिक तार को लंबे समय तक रहने का कारण बनने जा रही है। (भले ही नजरबंद स्ट्रिंग वस्तुओं को ईडन अंतरिक्ष में आवंटित किया गया था, वे एकत्र होने से पहले सबसे अधिक संभावना को बढ़ावा देंगे।)
इस प्रकार सभी मामलों में, एक स्ट्रिंग को इंटर्न करना एक साधारण स्ट्रिंग के सापेक्ष अपने जीवनकाल को लम्बा करने वाला है। यह JVM के जीवनकाल में कचरा संग्रहण ओवरहेड्स को बढ़ा देगा।
दूसरा मुद्दा यह है कि स्ट्रिंग इंटर्निंग लीक मेमोरी को रोकने के लिए हैश टेबल को किसी तरह के कमजोर संदर्भ तंत्र का उपयोग करने की आवश्यकता है। लेकिन इस तरह के एक तंत्र कचरा कलेक्टर के लिए अधिक काम है।
ये कचरा संग्रह ओवरहेड्स को निर्धारित करना मुश्किल है, लेकिन इसमें कोई संदेह नहीं है कि वे मौजूद हैं। यदि आप बड़े पैमाने पर intern
उपयोग करते हैं, तो वे महत्वपूर्ण हो सकते हैं।
स्ट्रिंग पूल में हैशटेबल आकार है
इस स्रोत के अनुसार, जावा 6 के बाद से, स्ट्रिंग पूल को उसी बाल्टी से हैश करने वाले तारों से निपटने के लिए चेन के साथ निश्चित आकार के हैश टेबल के रूप में लागू किया जाता है। जावा 6 के शुरुआती रिलीज में, हैश टेबल में एक (हार्ड-वायर्ड) स्थिर आकार था। एक ट्यूनिंग पैरामीटर ( -XX:StringTableSize
) को जावा 6. के मध्य-जीवन अद्यतन के रूप में जोड़ा गया था। तब जावा 7 के मध्य-जीवन अद्यतन में, पूल का डिफ़ॉल्ट आकार 1009
से 60013
में बदल दिया गया था।
लब्बोलुआब यह है कि यदि आप अपने कोड में intern
गहनता से उपयोग करने का इरादा रखते हैं, तो जावा के एक संस्करण को चुनना उचित है जहां हैशटेबल आकार ट्यून करने योग्य है और सुनिश्चित करें कि आप आकार को उचित रूप से ट्यून करते हैं। अन्यथा, पूल के बड़े होते ही intern
का प्रदर्शन नीचा दिखाने के लिए उत्तरदायी है।
सेवा वेक्टर के संभावित इनकार के रूप में इंटर्निंग
स्ट्रिंग्स के लिए हैशकोड एल्गोरिथ्म प्रसिद्ध है। यदि आप दुर्भावनापूर्ण उपयोगकर्ताओं या अनुप्रयोगों द्वारा आपूर्ति की गई तारों को नजरअंदाज करते हैं, तो इसे सेवा से इनकार (DoS) हमले के हिस्से के रूप में इस्तेमाल किया जा सकता है। यदि दुर्भावनापूर्ण एजेंट व्यवस्था करता है कि उसके द्वारा उपलब्ध कराए गए सभी तार में समान हैश कोड होता है, तो यह intern
लिए असंतुलित हैश तालिका और O(N)
प्रदर्शन को जन्म दे सकता है ... जहां N
टकराए गए तारों की संख्या है।
(एक सेवा के खिलाफ DoS हमले को शुरू करने के लिए सरल / अधिक प्रभावी तरीके हैं। हालांकि, इस वेक्टर का उपयोग किया जा सकता है यदि DoS हमले का लक्ष्य सुरक्षा को तोड़ना है, या पहली पंक्ति के DoS सुरक्षा को खाली करना है।)
नुकसान - असंबद्ध धाराओं पर छोटे पढ़े / लिखे अक्षम हैं
एक फ़ाइल को दूसरे में कॉपी करने के लिए निम्नलिखित कोड पर विचार करें:
import java.io.*;
public class FileCopy {
public static void main(String[] args) throws Exception {
try (InputStream is = new FileInputStream(args[0]);
OutputStream os = new FileOutputStream(args[1])) {
int octet;
while ((octet = is.read()) != -1) {
os.write(octet);
}
}
}
}
(हमने सामान्य तर्क जाँच, त्रुटि रिपोर्टिंग आदि को जानबूझकर छोड़ दिया है क्योंकि वे इस उदाहरण के बिंदु के लिए प्रासंगिक नहीं हैं।)
यदि आप उपरोक्त कोड संकलित करते हैं और इसका उपयोग किसी बड़ी फ़ाइल को कॉपी करने के लिए करते हैं, तो आप देखेंगे कि यह बहुत धीमा है। वास्तव में, यह मानक ओएस फ़ाइल कॉपी उपयोगिताओं की तुलना में कम से कम परिमाण के आदेशों की एक जोड़ी होगी।
( वास्तविक प्रदर्शन माप यहाँ जोड़ें! )
प्राथमिक कारण यह है कि ऊपर दिया गया उदाहरण धीमा है (बड़ी फ़ाइल मामले में) यह है कि यह एक-बाइट रीडिंग का प्रदर्शन कर रहा है और एक-बाइट बिना बाइट स्ट्रीम पर लिखता है। प्रदर्शन में सुधार करने का सरल तरीका यह है कि धाराओं को बफ़र्ड धाराओं के साथ लपेटा जाए। उदाहरण के लिए:
import java.io.*;
public class FileCopy {
public static void main(String[] args) throws Exception {
try (InputStream is = new BufferedInputStream(
new FileInputStream(args[0]));
OutputStream os = new BufferedOutputStream(
new FileOutputStream(args[1]))) {
int octet;
while ((octet = is.read()) != -1) {
os.write(octet);
}
}
}
}
ये छोटे बदलाव विभिन्न प्लेटफॉर्म-संबंधित कारकों के आधार पर, कम से कम परिमाण के आदेशों की एक-एक करके डेटा कॉपी रेट में सुधार करेंगे। बफ़र किए गए स्ट्रीम रैपर से डेटा को बड़ी मात्रा में पढ़ा और लिखा जा सकता है। दोनों उदाहरणों में बफ़र्स को बाइट सरणियों के रूप में लागू किया गया है।
साथ
is
, डेटा एक समय में बफ़र में कुछ किलोबाइट में फ़ाइल से पढ़ा जाता है। जबread()
जाता हैread()
कहा जाता है, तो कार्यान्वयन आमतौर पर बफर से एक बाइट लौटाएगा। यह केवल अंतर्निहित इनपुट स्ट्रीम से पढ़ेगा यदि बफर खाली कर दिया गया है।os
लिए व्यवहार अनुरूप है।os.write(int)
को कॉल बफर में सिंगल बाइट्स लिखते हैं। डेटा केवल आउटपुट स्ट्रीम को लिखा जाता है जब बफर भरा होता है, या जबos
फ्लश या बंद होता है।
चरित्र-आधारित धाराओं के बारे में क्या?
जैसा कि आपको पता होना चाहिए, जावा आई / ओ द्विआधारी और पाठ डेटा पढ़ने और लिखने के लिए अलग-अलग एपीआई प्रदान करता है।
-
InputStream
औरOutputStream
स्ट्रीम-आधारित बाइनरी I / O के लिए आधार API हैं -
Reader
औरWriter
स्ट्रीम-आधारित टेक्स्ट I / O के लिए आधार API हैं।
पाठ आई / ओ के लिए, BufferedReader
और BufferedWriter
के लिए समकक्ष हैं BufferedInputStream
और BufferedOutputStream
।
बफ़र्ड धाराएँ क्यों इतना अंतर करती हैं?
वास्तविक कारण जो बफ़र किए गए स्ट्रीम प्रदर्शन में मदद करते हैं, वह इस तरह से करना है कि कोई एप्लिकेशन ऑपरेटिंग सिस्टम से बात करता है:
जावा एप्लिकेशन में जावा विधि, या जेवीएम के मूल रनटाइम लाइब्रेरी में देशी प्रक्रिया कॉल तेज हैं। वे आमतौर पर मशीन निर्देशों के एक जोड़े को लेते हैं और न्यूनतम प्रदर्शन प्रभाव डालते हैं।
इसके विपरीत, ऑपरेटिंग सिस्टम के लिए जेवीएम रनटाइम कॉल तेज नहीं हैं। वे एक "syscall" के रूप में जाना जाता है कुछ शामिल है। एक syscall के लिए विशिष्ट पैटर्न निम्नानुसार है:
- Syscall तर्कों को रजिस्टरों में रखें।
- एक SYSENTER जाल निर्देश निष्पादित करें।
- ट्रैप हैंडलर विशेषाधिकार प्राप्त राज्य में बदल गया और वर्चुअल मेमोरी मैपिंग को बदल देता है। फिर यह विशिष्ट syscall को संभालने के लिए कोड को भेजता है।
- Syscall हैंडलर तर्कों की जाँच करता है, इस बात का ख्याल रखते हुए कि यह मेमोरी एक्सेस करने के लिए नहीं कहा जा रहा है जिसे उपयोगकर्ता प्रक्रिया को नहीं देखना चाहिए।
- Syscall विशिष्ट कार्य किया जाता है। एक
read
के मामले में, यह शामिल हो सकता है:- जाँचना कि फ़ाइल डिस्क्रिप्टर की वर्तमान स्थिति में पढ़ने के लिए डेटा है
- बफ़र कैश में डिस्क (या जहाँ भी यह संग्रहीत है) से आवश्यक डेटा प्राप्त करने के लिए फ़ाइल सिस्टम हैंडलर को कॉल करना,
- बफर कैश से JVM द्वारा प्रदत्त पते पर डेटा कॉपी करना
- एडजस्ट करने के लिए थ्रस्ट पाइंट फाइल डिस्क्रिप्टर पोजिशन
- Syscall से लौटें। यह वीएम मैपिंग को फिर से बदलने और विशेषाधिकार प्राप्त राज्य से बाहर जाने पर जोर देता है।
जैसा कि आप कल्पना कर सकते हैं, एक एकल syscall प्रदर्शन मशीन के हजारों निर्देश दे सकता है। रूढ़िवादी रूप से, एक नियमित विधि कॉल की तुलना में लंबे समय तक कम से कम दो आदेश। (शायद तीन या अधिक।)
यह देखते हुए, यह कारण है कि बफर स्ट्रीम एक बड़ा अंतर है कि वे काफी हद तक syscalls की संख्या कम कर देते हैं। प्रत्येक read()
कॉल के लिए एक syscall करने के बजाय, बफ़र किए गए इनपुट स्ट्रीम बड़ी मात्रा में डेटा को बफर में पढ़ता है। बफ़र्ड स्ट्रीम पर अधिकांश read()
कॉल कुछ साधारण सीमा की जाँच करते हैं और एक byte
वापस करते हैं जो पहले पढ़ा गया था। इसी तरह का तर्क आउटपुट स्ट्रीम मामले में भी लागू होता है, और चरित्र स्ट्रीम मामलों में भी।
(कुछ लोगों को लगता है कि बफ़र किया गया I / O प्रदर्शन रीड रिक्वेस्ट साइज़ और डिस्क ब्लॉक के आकार, डिस्क रोटेशनल लेटेंसी और इस तरह की चीज़ों के बीच बेमेल से आता है। वास्तव में, एक आधुनिक ओएस यह सुनिश्चित करने के लिए कई रणनीतियों का उपयोग करता है। एप्लिकेशन को आमतौर पर डिस्क के लिए प्रतीक्षा करने की आवश्यकता नहीं होती है। यह वास्तविक स्पष्टीकरण नहीं है।)
क्या बफ़र्ड स्ट्रीम हमेशा एक जीत होती हैं?
हर बार नहीं। बफ़र्ड स्ट्रीम निश्चित रूप से एक जीत हैं यदि आपका एप्लिकेशन बहुत सारे "छोटे" पढ़ने या लिखने वाला है। हालाँकि, यदि आपके आवेदन को केवल बड़े byte[]
या char[]
से बड़े रीड या लिखने की आवश्यकता है, तो बफर स्ट्रीम आपको कोई वास्तविक लाभ नहीं देगी। वास्तव में वहाँ भी (छोटे) प्रदर्शन जुर्माना हो सकता है।
क्या जावा में किसी फ़ाइल को कॉपी करने का यह सबसे तेज़ तरीका है?
नहीं, यह नहीं है। जब आप किसी फ़ाइल को कॉपी करने के लिए जावा के स्ट्रीम-आधारित एपीआई का उपयोग करते हैं, तो आप डेटा की कम से कम एक अतिरिक्त मेमोरी-टू-मेमोरी कॉपी का खर्च उठाते हैं। इससे बचने के लिए संभव है अगर आपका NIO ByteBuffer
और Channel
एपीआई का उपयोग करता है। ( यहां एक अलग उदाहरण के लिए एक लिंक जोड़ें। )