4 बिलियन-इटरेशन जावा लूप केवल 2 एमएस क्यों लेता है?


113

मैं 2.7 गीगाहर्ट्ज इंटेल कोर i7 वाले लैपटॉप पर निम्नलिखित जावा कोड चला रहा हूं। मेरा इरादा इसे मापने का है कि 2 ^ 32 पुनरावृत्तियों के साथ एक लूप खत्म करने में कितना समय लगता है, जिसकी मुझे लगभग 1.48 सेकंड (4 / 2.7 = 1.48) होने की उम्मीद थी।

लेकिन वास्तव में यह केवल 1.48 सेकेंड के बजाय 2 मिलीसेकंड लेता है। मैं सोच रहा था कि यह किसी भी JVM अनुकूलन का परिणाम है?

public static void main(String[] args)
{
    long start = System.nanoTime();

    for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++){
    }
    long finish = System.nanoTime();
    long d = (finish - start) / 1000000;

    System.out.println("Used " + d);
}

69
सही है। क्योंकि लूप बॉडी का कोई साइड-इफेक्ट नहीं है, इसलिए कंपाइलर इसे खुशी-खुशी खत्म कर देता है। देखने के लिए बाइट-कोड की जाँच करें javap -v
इलियट फ्रिश

36
आप उस बाइट कोड में वापस नहीं देखेंगे। javacबहुत कम वास्तविक अनुकूलन करता है और इसका अधिकांश भाग JIT कंपाइलर पर छोड़ देता है।
जोर्न वर्नी

4
'मैं सोच रहा हूं कि क्या यह किसी जेवीएम अनुकूलन का परिणाम है?' - तुम क्या सोचते हो? JVM ऑप्टिमाइज़ेशन नहीं तो और क्या हो सकता है?
अपंगिन

7
इस सवाल का जवाब मूल रूप से stackoverflow.com/a/25323548/3182664 में निहित है । इसमें परिणामी असेंबली (मशीन कोड) भी शामिल है जो JIT ऐसे मामलों के लिए उत्पन्न होती है, जिसमें दिखाया गया है कि लूप पूरी तरह से JIT द्वारा अनुकूलित है । ( Stackoverflow.com/q/25326377/3182664 पर सवाल से पता चलता है कि लूप 4 बिलियन ऑपरेशन नहीं करता है, लेकिन 4 बिलियन माइनस एक ;-)) में थोड़ा समय लग सकता है । मैं लगभग इस प्रश्न को दूसरे की नकल के रूप में मानूंगा - कोई आपत्ति?
मार्को 13

7
आपको लगता है कि प्रोसेसर प्रति हर्ट्ज में एक पुनरावृत्ति करेगा। यह एक दूरगामी धारणा है। प्रोसेसर आज सभी प्रकार के अनुकूलन करते हैं, जैसा कि @Rahul ने उल्लेख किया है, और जब तक आप कोर i7 के काम करने के तरीके के बारे में अधिक नहीं जानते, आप ऐसा नहीं मान सकते।
त्सही अशर

जवाबों:


106

यहां दो संभावनाएं हैं:

  1. संकलक ने महसूस किया कि लूप बेमानी है और कुछ भी नहीं कर रहा है इसलिए इसे दूर अनुकूलित किया।

  2. जेआईटी (बस-इन-टाइम कंपाइलर) ने महसूस किया कि लूप बेमानी है और कुछ भी नहीं कर रहा है, इसलिए इसने इसे अनुकूलित किया।

आधुनिक संकलक बहुत बुद्धिमान हैं; वे देख सकते हैं कि कोड कब बेकार है। GodBolt में एक खाली लूप लगाने की कोशिश करें और आउटपुट देखें, फिर -O2ऑप्टिमाइज़ेशन चालू करें , आप देखेंगे कि आउटपुट लाइनों के साथ कुछ है

main():
    xor eax, eax
    ret

मैं कुछ स्पष्ट करना चाहूंगा, जावा में अधिकांश अनुकूलन जेआईटी द्वारा किए जाते हैं। कुछ अन्य भाषाओं में (C / C ++ की तरह) अधिकांश अनुकूलन पहले कंपाइलर द्वारा किए जाते हैं।


क्या संकलक को इस तरह के अनुकूलन करने की अनुमति है? मुझे जावा के लिए निश्चित रूप से पता नहीं है, लेकिन .NET कंपाइलर्स को आमतौर पर प्लेटफॉर्म के लिए JIT को सर्वोत्तम अनुकूलन करने की अनुमति देने से बचना चाहिए।
IllidanS4 मोनिका को

1
@ IllidanS4 सामान्य तौर पर, यह भाषा के मानक पर निर्भर करता है। यदि कंपाइलर ऑप्टिमाइज़ेशन कर सकता है जिसका मतलब है कि मानक द्वारा व्याख्या की गई कोड का समान प्रभाव है, तो हाँ। हालांकि, कई सूक्ष्मताओं पर विचार करना होता है, जैसे कि फ्लोटिंग पॉइंट कंप्यूटेशन के लिए कुछ परिवर्तन होते हैं, जिसके परिणामस्वरूप ओवर / अंडरफ्लो की संभावना हो सकती है, इसलिए किसी भी अनुकूलन को सावधानीपूर्वक करना होगा।
user1997744

9
@ IllidanS4 रनटाइम वातावरण को बेहतर अनुकूलन कैसे करना चाहिए? बहुत कम से कम यह कोड का विश्लेषण करना चाहिए जो संकलन के दौरान कोड को हटाने से तेज नहीं हो सकता है।
गेरहार्ड

2
@Gerhardh मैं इस सटीक मामले के बारे में बात नहीं कर रहा था, जब रनटाइम कोड के अनावश्यक भागों को हटाने में बेहतर काम नहीं कर सकता है, लेकिन इस कारण के सही होने पर कुछ मामले हो सकते हैं। और चूँकि JRE के लिए अन्य भाषाओं से अन्य कंपाइलर हो सकते हैं, रनटाइम को भी इन ऑप्टिमाइज़ेशन को करना चाहिए , इसलिए रनटाइम और कंपाइलर द्वारा दोनों के लिए संभावित रूप से कोई कारण नहीं है।
IllidanS4 मोनिका को 14

6
@ IllidanS4 कोई भी रनटाइम अनुकूलन शून्य से कम समय नहीं ले सकता है। संकलक को कोड हटाने से रोकने से कोई मतलब नहीं होगा।
गेरहार्ड

55

ऐसा लगता है कि यह JIT कंपाइलर द्वारा अनुकूलित किया गया था। जब मैं इसे बंद करता हूं ( -Djava.compiler=NONE), कोड बहुत धीमा चलता है:

$ javac MyClass.java
$ java MyClass
Used 4
$ java -Djava.compiler=NONE MyClass
Used 40409

मैंने ओपी का कोड अंदर डाला class MyClass


2
अजीब। जब मैं दोनों तरीकों से कोड चलाता हूं, तो यह ध्वज के बिना तेज होता है , लेकिन केवल 10 के एक कारक के द्वारा, और लूप में पुनरावृत्तियों की संख्या में शून्य जोड़ने या हटाने से भी, दस के कारकों से चल रहे समय को प्रभावित करता है, साथ और बिना झंडा। इसलिए (मेरे लिए) लूप को पूरी तरह से अनुकूलित नहीं किया जाना चाहिए, बस किसी भी तरह 10 गुना तेजी से बनाया गया है। (ओरेकल जावा 8-151)
टोबियास_

@tobias_k यह इस बात पर निर्भर करता है कि JIT लूप के किस चरण से गुजर रहा है, मुझे लगता है कि stackoverflow.com/a/47972226/1059372
Eugene

21

मैं बस स्पष्ट रूप से बताऊंगा - कि यह एक जेवीएम अनुकूलन है जो होता है, लूप को बिल्कुल हटा दिया जाएगा। यहाँ एक छोटा परीक्षण है जो दिखाता है कि केवल सक्षम / सक्षम होने पर कितना बड़ा अंतर JITहैC1 Compiler

अस्वीकरण: इस तरह से परीक्षण न लिखें - यह केवल यह साबित करने के लिए है कि वास्तविक लूप "हटाने" में होता है C2 Compiler:

@Benchmark
@Fork(1)
public void full() {
    long result = 0;
    for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
        ++result;
    }
}

@Benchmark
@Fork(1)
public void minusOne() {
    long result = 0;
    for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE - 1; i++) {
        ++result;
    }
}

@Benchmark
@Fork(value = 1, jvmArgsAppend = { "-XX:TieredStopAtLevel=1" })
public void withoutC2() {
    long result = 0;
    for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE - 1; i++) {
        ++result;
    }
}

@Benchmark
@Fork(value = 1, jvmArgsAppend = { "-Xint" })
public void withoutAll() {
    long result = 0;
    for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE - 1; i++) {
        ++result;
    }
}

परिणाम बताते हैं कि किस भाग के JITसक्षम होने के आधार पर , विधि तेज़ हो जाती है (इतना तेज़ कि ऐसा लगता है कि यह "कुछ भी नहीं" कर रहा है - लूप रिमूवल, जो लगता है कि इसमें हो रहा है C2 Compiler- जो कि अधिकतम स्तर है):

 Benchmark                Mode  Cnt      Score   Error  Units
 Loop.full        avgt    2      10⁻⁷          ms/op
 Loop.minusOne    avgt    2      10⁻⁶          ms/op
 Loop.withoutAll  avgt    2  51782.751          ms/op
 Loop.withoutC2   avgt    2   1699.137          ms/op 

13

जैसा कि पहले ही बताया जा चुका है, अनावश्यक पुनरावृत्तियों को दूर करने के लिए जेआईटी ( जस्ट -इन-टाइम) कंपाइलर एक खाली लूप का अनुकूलन कर सकता है। पर कैसे?

दरअसल, दो JIT कंपाइलर हैं: C1 & C2 । सबसे पहले, कोड को C1 के साथ संकलित किया जाता है। C1 आंकड़े एकत्र करता है और JVM को यह पता लगाने में मदद करता है कि 100% मामलों में हमारा खाली लूप कुछ भी नहीं बदलता है और बेकार है। इस स्थिति में सी 2 चरण में प्रवेश करता है। जब कोड को बहुत बार कहा जाता है, तो इसे एकत्र आँकड़ों का उपयोग करके C2 के साथ अनुकूलित और संकलित किया जा सकता है।

एक उदाहरण के रूप में, मैं अगले कोड स्निपेट का परीक्षण करूंगा (मेरा JDK 9-आंतरिक निर्माण को धीमा करने के लिए सेट है ):

public class Demo {
    private static void run() {
        for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
        }
        System.out.println("Done!");
    }
}

निम्नलिखित कमांड लाइन विकल्पों के साथ:

-XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,*Demo.run

और मेरी रन विधि के विभिन्न संस्करण हैं , जो C1 और C2 के साथ उचित रूप से संकलित हैं। मेरे लिए, अंतिम संस्करण (C2) कुछ इस तरह दिखता है:

...

; B1: # B3 B2 <- BLOCK HEAD IS JUNK  Freq: 1
0x00000000125461b0: mov   dword ptr [rsp+0ffffffffffff7000h], eax
0x00000000125461b7: push  rbp
0x00000000125461b8: sub   rsp, 40h
0x00000000125461bc: mov   ebp, dword ptr [rdx]
0x00000000125461be: mov   rcx, rdx
0x00000000125461c1: mov   r10, 57fbc220h
0x00000000125461cb: call  indirect r10    ; *iload_1

0x00000000125461ce: cmp   ebp, 7fffffffh  ; 7fffffff => 2147483647
0x00000000125461d4: jnl   125461dbh       ; jump if not less

; B2: # B3 <- B1  Freq: 0.999999
0x00000000125461d6: mov   ebp, 7fffffffh  ; *if_icmpge

; B3: # N44 <- B1 B2  Freq: 1       
0x00000000125461db: mov   edx, 0ffffff5dh
0x0000000012837d60: nop
0x0000000012837d61: nop
0x0000000012837d62: nop
0x0000000012837d63: call  0ae86fa0h

...

यह थोड़ा गड़बड़ है, लेकिन अगर आप बारीकी से देखते हैं, तो आप देख सकते हैं कि यहां कोई लंबे समय तक चलने वाला लूप नहीं है। 3 ब्लॉक हैं: बी 1, बी 2 और बी 3 और निष्पादन चरण B1 -> B2 -> B3या हो सकते हैं B1 -> B3। जहां Freq: 1- एक ब्लॉक निष्पादन की सामान्यीकृत अनुमानित आवृत्ति।


8

आप उस समय को माप रहे हैं जिसमें लूप का पता लगाने में कुछ भी नहीं होता है, कोड को पृष्ठभूमि थ्रेड में संकलित करें और कोड को समाप्त करें।

for (int t = 0; t < 5; t++) {
    long start = System.nanoTime();
    for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
    }
    long time = System.nanoTime() - start;

    String s = String.format("%d: Took %.6f ms", t, time / 1e6);
    Thread.sleep(50);
    System.out.println(s);
    Thread.sleep(50);
}

यदि आप इसे चलाते हैं तो आप -XX:+PrintCompilationदेख सकते हैं कि कोड को पृष्ठभूमि में 3 या C1 संकलक के लिए संकलित किया गया है और कुछ छोरों के बाद C4 के स्तर 4 तक।

    129   34 %     3       A::main @ 15 (93 bytes)
    130   35       3       A::main (93 bytes)
    130   36 %     4       A::main @ 15 (93 bytes)
    131   34 %     3       A::main @ -2 (93 bytes)   made not entrant
    131   36 %     4       A::main @ -2 (93 bytes)   made not entrant
0: Took 2.510408 ms
    268   75 %     3       A::main @ 15 (93 bytes)
    271   76 %     4       A::main @ 15 (93 bytes)
    274   75 %     3       A::main @ -2 (93 bytes)   made not entrant
1: Took 5.629456 ms
2: Took 0.000000 ms
3: Took 0.000364 ms
4: Took 0.000365 ms

यदि आप उपयोग करने के लिए लूप को बदलते हैं तो longयह अनुकूलित नहीं होता है।

    for (long i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
    }

इसके बदले आपको मिलता है

0: Took 1579.267321 ms
1: Took 1674.148662 ms
2: Took 1885.692166 ms
3: Took 1709.870567 ms
4: Took 1754.005112 ms

यह अजीब है ... क्यों एक longकाउंटर वही अनुकूलन होने से रोक सकता है?
रयान अमोस

@RyanAmos अनुकूलन केवल सामान्य आदिम लूप गणना पर लागू होता है यदि प्रकार intनोट चार और शॉर्ट बाइट कोड स्तर पर प्रभावी रूप से समान हैं।
पीटर लॉरी

-1

आप नैनोसेकंड में शुरुआत और समापन का समय मानते हैं और आप विलंबता की गणना के लिए 10 ^ 6 से विभाजित करते हैं

long d = (finish - start) / 1000000

यह 10^9इसलिए होना चाहिए क्योंकि 1दूसरा = 10^9नैनोसेकंड।


आपने जो सुझाव दिया, वह मेरी बात के लिए अप्रासंगिक है। मैं इस बारे में सोच रहा था कि इसमें कितना समय लगा है, और इससे कोई फर्क नहीं पड़ता कि यह अवधि छपी है या मिलि-सेकंड के संदर्भ में दर्शाई गई है।
ट्विमो
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.