जावा 8 में विधि संदर्भ एक अच्छा विचार है?


81

विचार करें कि मेरे पास निम्नलिखित की तरह कोड है:

class Foo {

   Y func(X x) {...} 

   void doSomethingWithAFunc(Function<X,Y> f){...}

   void hotFunction(){
        doSomethingWithAFunc(this::func);
   }

}

मान लीजिए कि hotFunctionबहुत बार कहा जाता है। क्या फिर इसे कैश करना उचित होगा this::func, शायद इस तरह:

class Foo {
     Function<X,Y> f = this::func;
     ...
     void hotFunction(){
        doSomethingWithAFunc(f);
     }
}

जहाँ तक जावा विधि संदर्भों के बारे में मेरी समझ जाती है, वर्चुअल मशीन एक अनाम वर्ग की एक वस्तु बनाता है जब एक विधि संदर्भ का उपयोग किया जाता है। इस प्रकार, संदर्भ को कैशिंग करने से वह वस्तु केवल एक बार बनती है जबकि पहला दृष्टिकोण इसे प्रत्येक फ़ंक्शन कॉल पर बनाता है। क्या ये सही है?

कोड में गर्म स्थान पर दिखाई देने वाले विधि संदर्भों को कैश किया जाना चाहिए या क्या वीएम इसे ऑप्टिमाइज़ कर सकता है और कैशिंग को शानदार बना सकता है? क्या इस बारे में एक सामान्य सर्वोत्तम अभ्यास है या क्या यह अत्यधिक वीएम-इम्प्लिमेंटेशन विशिष्ट है कि क्या इस तरह के कैशिंग का कोई फायदा नहीं है?


मैं कहूंगा कि आप उस फ़ंक्शन का इतना उपयोग करते हैं कि ट्यूनिंग के इस स्तर की आवश्यकता / वांछनीय है, शायद आप लंबोदा को छोड़ने और सीधे फ़ंक्शन को लागू करने से बेहतर होंगे, जो अन्य अनुकूलन के लिए अधिक जगह देता है।
SJuan76

@ SJuan76: मैं इस बारे में निश्चित नहीं हूँ! यदि एक विधि संदर्भ को एनोनिमस क्लास में संकलित किया जाता है, तो यह सामान्य इंटरफ़ेस कॉल के रूप में तेजी से होता है। इस प्रकार, मुझे नहीं लगता कि कार्यात्मक शैली को गर्म कोड के लिए बचा जाना चाहिए।
१२:१३ पर gexicide

4
विधि संदर्भों के साथ कार्यान्वित किया जाता है invokedynamic। मुझे संदेह है कि आप फ़ंक्शन ऑब्जेक्ट को कैशिंग करके एक प्रदर्शन सुधार देखेंगे। इसके विपरीत: यह संकलक अनुकूलन को रोक सकता है। क्या आपने दो वेरिएंट के प्रदर्शन की तुलना की?
nosid

@ एनोसिड: नहीं, तुलना नहीं की। लेकिन मैं OpenJDK के एक बहुत ही शुरुआती संस्करण का उपयोग कर रहा हूं, इसलिए मेरी संख्याओं का वैसे भी कोई महत्व नहीं हो सकता है क्योंकि मुझे लगता है कि पहला संस्करण केवल नई सुविधाओं को त्वरित रूप से लागू करता है 'n' गंदा है और यह प्रदर्शन की तुलना में नहीं किया जा सकता है जब विशेषताएं परिपक्व हो गई हैं अधिक समय तक। क्या युक्ति वास्तव में जनादेश है invokedynamicजिसका उपयोग किया जाना चाहिए? मुझे इसका कोई कारण नहीं दिखता है!
gexicide

4
इसे स्वचालित रूप से कैश किया जाना चाहिए (यह हर बार एक नया अनाम वर्ग बनाने के बराबर नहीं है) इसलिए आपको उस अनुकूलन के बारे में चिंता नहीं करनी चाहिए।
21

जवाबों:


83

आप एक ही के लगातार फांसी के बीच एक अंतर बनाने के लिए है कॉल-साइट , राज्यविहीन लैम्ब्डा या स्टेटफुल lambdas, और एक के लगातार उपयोग के लिए विधि-संदर्भ एक ही विधि के लिए (अलग कॉल-साइटों द्वारा)।

निम्नलिखित उदाहरण देखें:

    Runnable r1=null;
    for(int i=0; i<2; i++) {
        Runnable r2=System::gc;
        if(r1==null) r1=r2;
        else System.out.println(r1==r2? "shared": "unshared");
    }

यहां, एक ही कॉल-साइट को दो बार निष्पादित किया जाता है, एक स्टेटलेस लैम्ब्डा का उत्पादन होता है और वर्तमान कार्यान्वयन प्रिंट होगा "shared"

Runnable r1=null;
for(int i=0; i<2; i++) {
  Runnable r2=Runtime.getRuntime()::gc;
  if(r1==null) r1=r2;
  else {
    System.out.println(r1==r2? "shared": "unshared");
    System.out.println(
        r1.getClass()==r2.getClass()? "shared class": "unshared class");
  }
}

इस दूसरे उदाहरण में, एक ही कॉल-साइट को दो बार निष्पादित किया जाता है, एक लंबो का निर्माण करता है जिसमें एक Runtimeउदाहरण के संदर्भ में और वर्तमान कार्यान्वयन प्रिंट होगा "unshared"लेकिन "shared class"

Runnable r1=System::gc, r2=System::gc;
System.out.println(r1==r2? "shared": "unshared");
System.out.println(
    r1.getClass()==r2.getClass()? "shared class": "unshared class");

इसके विपरीत, अंतिम उदाहरण में दो अलग-अलग कॉल-साइट एक समान विधि संदर्भ का उत्पादन कर रहे हैं लेकिन जैसा कि 1.8.0_05यह प्रिंट करेगा "unshared"और "unshared class"


प्रत्येक लैम्ब्डा अभिव्यक्ति या विधि संदर्भ के लिए कंपाइलर एक invokedynamicनिर्देश का उत्सर्जन करेगा जो कि क्लास में JRE प्रदान की गई बूटस्ट्रैप विधि LambdaMetafactoryऔर वांछित लैम्ब्डा कार्यान्वयन वर्ग के उत्पादन के लिए आवश्यक स्थिर तर्क को संदर्भित करता है । यह वास्तविक JRE के लिए छोड़ दिया जाता है कि मेटा फैक्ट्री क्या पैदा करती है, लेकिन यह पहले आह्वान पर बनाए invokedynamicगए CallSiteउदाहरण को याद रखने और फिर से उपयोग करने के लिए निर्देश का एक निर्दिष्ट व्यवहार है ।

वर्तमान जेआरई स्टेटलेस लैम्ब्डा के लिए एक स्थिर ऑब्जेक्ट से ConstantCallSiteयुक्त है MethodHandle(और इसे अलग करने के लिए कोई कल्पना करने योग्य कारण नहीं है)। और विधि के लिए विधि संदर्भ staticहमेशा स्टेटलेस होते हैं। इसलिए स्टेटलेस लैम्ब्डा और सिंगल कॉल-साइट्स के लिए इसका उत्तर होना चाहिए: कैश न करें, जेवीएम करेगा और यदि ऐसा नहीं है, तो इसके मजबूत कारण होने चाहिए कि आपको जवाबी कार्रवाई नहीं करनी चाहिए।

लैम्ब्डा के मापदंडों के लिए, और this::funcएक लैम्ब्डा है जिसमें thisउदाहरण के लिए एक संदर्भ है , चीजें थोड़ी अलग हैं। JRE को उन्हें कैश करने की अनुमति है, लेकिन यह Mapवास्तविक पैरामीटर मानों और परिणामी लैम्ब्डा के बीच कुछ प्रकार को बनाए रखेगा, जो कि उस सरल संरचित लैम्बडा उदाहरण को फिर से बनाने की तुलना में अधिक महंगा हो सकता है। वर्तमान जेआरई राज्य में लंबा उदाहरणों को कैश नहीं करता है।

लेकिन इसका मतलब यह नहीं है कि मेमना वर्ग हर बार बनाया जाता है। इसका सीधा सा मतलब यह है कि सुलझी हुई कॉल-साइट एक सामान्य वस्तु निर्माण की तरह व्यवहार करेगी, जो लैम्ब्डा क्लास को इंस्टेंट करने वाली है जो पहले आह्वान पर उत्पन्न हुई है।

अलग-अलग कॉल-साइट्स द्वारा बनाई गई एक ही लक्ष्य पद्धति के संदर्भ में समान बातें लागू होती हैं। JRE को उनके बीच एक एकल लंबो इंस्टेंस को साझा करने की अनुमति है, लेकिन वर्तमान संस्करण में यह संभव नहीं है, क्योंकि यह स्पष्ट नहीं है कि कैश रखरखाव का भुगतान करना होगा या नहीं। यहां, उत्पन्न वर्ग भी भिन्न हो सकते हैं।


इसलिए आपके उदाहरण की तरह कैशिंग करने से आपका कार्यक्रम बिना किसी से अलग हो सकता है। लेकिन जरूरी नहीं कि वह अधिक कुशल हो। एक कैश्ड वस्तु हमेशा एक अस्थायी वस्तु की तुलना में अधिक कुशल नहीं होती है। जब तक आप वास्तव में एक लैम्ब्डा निर्माण के कारण प्रदर्शन प्रभाव को मापते हैं, आपको कोई कैशिंग नहीं जोड़ना चाहिए।

मुझे लगता है, केवल कुछ विशेष मामले हैं जहां कैशिंग उपयोगी हो सकता है:

  • हम कई अलग-अलग कॉल-साइटों के बारे में बात कर रहे हैं जो एक ही विधि का उल्लेख करते हैं
  • कंस्ट्रक्टर / क्लास में लैम्ब्डा बनाया जाता है, क्योंकि बाद में उपयोग-साइट वसीयत में होता है
    • कई सूत्र द्वारा समवर्ती रूप से बुलाया जा सकता है
    • पहले आह्वान के निचले प्रदर्शन से पीड़ित हैं

5
स्पष्टता: शब्द "कॉल-साइट" उस invokedynamicनिर्देश के निष्पादन को संदर्भित करता है जो लैम्बडा बनाएगा। यह वह स्थान नहीं है जहां कार्यात्मक इंटरफ़ेस विधि निष्पादित की जाएगी।
होल्गर

1
मुझे लगा कि this-कैप्टिंग लैम्ब्डा उदाहरण-स्कोप्ड सिंगलेट्स (ऑब्जेक्ट पर एक सिंथेटिक इंस्टेंस चर) थे। ऐसा नहीं है?
मार्को टोपोलनिक

2
@Marko Topolnik: यह एक संक्षिप्त संकलन रणनीति होगी लेकिन नहीं, जैसा कि ओरेकल की जद में 1.8.0_40है, ऐसा नहीं है। इन लंबोदरों को याद नहीं किया जाता है, इस प्रकार कचरा एकत्र किया जा सकता है। लेकिन याद रखें कि एक बार एक invokedynamicकॉलसाइट लिंक हो जाने के बाद, यह साधारण कोड की तरह अनुकूलित हो सकता है, अर्थात ऐसे लैम्ब्डा उदाहरणों के लिए एस्केप एनालिसिस काम करता है।
होल्गर

2
वहाँ एक मानक पुस्तकालय वर्ग नाम नहीं लगता है MethodReference। क्या आपका मतलब MethodHandleयहाँ है?
Lii

2
@ एलआईआई: आप सही कह रहे हैं, यह एक टाइपो है। दिलचस्प है कि कोई भी पहले देखा है लगता है।
होल्गर

11

एक स्थिति जहां यह एक अच्छा आदर्श है, दुर्भाग्य से, यदि लंबर को एक श्रोता के रूप में पारित किया जाता है जिसे आप भविष्य में किसी बिंदु पर निकालना चाहते हैं। कैश्ड संदर्भ की आवश्यकता होगी क्योंकि यह एक और गुजरता है :: विधि संदर्भ को हटाने में समान ऑब्जेक्ट के रूप में नहीं देखा जाएगा और मूल को हटाया नहीं जाएगा। उदाहरण के लिए:

public class Example
{
    public void main( String[] args )
    {
        new SingleChangeListenerFail().listenForASingleChange();
        SingleChangeListenerFail.observableValue.set( "Here be a change." );
        SingleChangeListenerFail.observableValue.set( "Here be another change that you probably don't want." );

        new SingleChangeListenerCorrect().listenForASingleChange();
        SingleChangeListenerCorrect.observableValue.set( "Here be a change." );
        SingleChangeListenerCorrect.observableValue.set( "Here be another change but you'll never know." );
    }

    static class SingleChangeListenerFail
    {
        static SimpleStringProperty observableValue = new SimpleStringProperty();

        public void listenForASingleChange()
        {
            observableValue.addListener(this::changed);
        }

        private<T> void changed( ObservableValue<? extends T> observable, T oldValue, T newValue )
        {
            System.out.println( "New Value: " + newValue );
            observableValue.removeListener(this::changed);
        }
    }

    static class SingleChangeListenerCorrect
    {
        static SimpleStringProperty observableValue = new SimpleStringProperty();
        ChangeListener<String> lambdaRef = this::changed;

        public void listenForASingleChange()
        {
            observableValue.addListener(lambdaRef);
        }

        private<T> void changed( ObservableValue<? extends T> observable, T oldValue, T newValue )
        {
            System.out.println( "New Value: " + newValue );
            observableValue.removeListener(lambdaRef);
        }
    }
}

इस मामले में lambdaRef की जरूरत नहीं है अच्छा होगा।


आह, मैं अब बात देख रहा हूं। उचित लगता है, हालांकि शायद परिदृश्य के बारे में ओपी नहीं बोल रहा है। फिर भी उखाड़ा गया।
टैगिर वलेव

9

जहाँ तक मैं भाषा विनिर्देश को समझता हूँ, यह इस तरह के अनुकूलन की अनुमति देता है, भले ही यह देखने योग्य व्यवहार को बदल दे। अनुभाग JSL8 following15.13.3 से निम्नलिखित उद्धरण देखें :

§15.13.3 विधि संदर्भ का रन-टाइम मूल्यांकन

रन समय में, एक विधि संदर्भ अभिव्यक्ति का मूल्यांकन एक वर्ग उदाहरण सृजन अभिव्यक्ति के मूल्यांकन के समान है, सामान्य पूर्णता के रूप में इनोफ़र एक वस्तु के संदर्भ का उत्पादन करता है। [..]

[..] या तो नीचे के गुणों के साथ एक वर्ग का एक नया उदाहरण आवंटित किया गया है और प्रारंभिक है, या नीचे के गुणों के साथ एक वर्ग का मौजूदा उदाहरण संदर्भित है।

एक साधारण परीक्षण से पता चलता है, स्थैतिक तरीकों के लिए विधि संदर्भ (कर सकते हैं) प्रत्येक मूल्यांकन के लिए एक ही संदर्भ में परिणाम। निम्नलिखित कार्यक्रम तीन पंक्तियों को प्रिंट करता है, जिनमें से पहले दो समान हैं:

public class Demo {
    public static void main(String... args) {
        foobar();
        foobar();
        System.out.println((Runnable) Demo::foobar);
    }
    public static void foobar() {
        System.out.println((Runnable) Demo::foobar);
    }
}

मैं गैर-स्थिर कार्यों के लिए एक ही प्रभाव को पुन: पेश नहीं कर सकता। हालाँकि, मुझे भाषा विनिर्देश में कुछ भी नहीं मिला है, जो इस अनुकूलन को रोकता है।

इसलिए, जब तक कि इस मैनुअल अनुकूलन के मूल्य को निर्धारित करने के लिए कोई प्रदर्शन विश्लेषण नहीं है , मैं इसके खिलाफ दृढ़ता से सलाह देता हूं। कैशिंग कोड की पठनीयता को प्रभावित करता है, और यह स्पष्ट नहीं है कि इसका कोई मूल्य है या नहीं। सभी बुराईयो की जड़ समयपूर्व इष्टतमीकरण है।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.