java X64 जावा में int से धीमा क्यों धीमा है?




performance 32bit-64bit (6)

रिकॉर्ड के लिए, यह संस्करण एक क्रूड "वार्मअप" करता है:

public class LongSpeed {

    private static long i = Integer.MAX_VALUE;
    private static int j = Integer.MAX_VALUE;

    public static void main(String[] args) {

        for (int x = 0; x < 10; x++) {
            runLong();
            runWord();
        }
    }

    private static void runLong() {
        System.out.println("Starting the long loop");
        i = Integer.MAX_VALUE;
        long startTime = System.currentTimeMillis();
        while(!decrementAndCheckI()){

        }
        long endTime = System.currentTimeMillis();

        System.out.println("Finished the long loop in " + (endTime - startTime) + "ms");
    }

    private static void runWord() {
        System.out.println("Starting the word loop");
        j = Integer.MAX_VALUE;
        long startTime = System.currentTimeMillis();
        while(!decrementAndCheckJ()){

        }
        long endTime = System.currentTimeMillis();

        System.out.println("Finished the word loop in " + (endTime - startTime) + "ms");
    }

    private static boolean decrementAndCheckI() {
        return --i < 0;
    }

    private static boolean decrementAndCheckJ() {
        return --j < 0;
    }

}

कुल मिलाकर 30% में सुधार होता है, लेकिन दोनों के बीच का अनुपात मोटे तौर पर वही रहता है।

मैं एक सतह प्रो 2 टैबलेट पर जावा 7 अपडेट 45 x64 (कोई 32 बिट जावा स्थापित) के साथ विंडोज 8.1 x64 चला रहा हूं।

नीचे दिया गया कोड 1688ms लेता है जब मैं एक प्रकार का लंबा और 109 मिमी होता हूं जब मैं int हूं। एक 64 बिट जेवीएम के साथ 64 बिट प्लेटफ़ॉर्म पर int की तुलना में लंबा (64 बिट प्रकार) तीव्रता का क्रम क्यों धीमा है?

मेरी एकमात्र अटकलें यह है कि सीपीयू 32 बिट एक से 64 बिट पूर्णांक जोड़ने के लिए अधिक समय लेता है, लेकिन ऐसा लगता है कि यह असंभव है। मुझे संदेह है कि हैस्वेल लहर-वाहक योजकों का उपयोग नहीं करता है।

मैं इसे ग्रहण केप्लर एसआर 1, बीटीडब्ल्यू में चला रहा हूं।

public class Main {

    private static long i = Integer.MAX_VALUE;

    public static void main(String[] args) {    
        System.out.println("Starting the loop");
        long startTime = System.currentTimeMillis();
        while(!decrementAndCheck()){
        }
        long endTime = System.currentTimeMillis();
        System.out.println("Finished the loop in " + (endTime - startTime) + "ms");
    }

    private static boolean decrementAndCheck() {
        return --i < 0;
    }

}

संपादित करें: वीएस 2013 (नीचे), उसी प्रणाली द्वारा संकलित समकक्ष सी ++ कोड के परिणाम यहां दिए गए हैं। लंबा: 72265 एमएमएस int: 74656ms वे परिणाम 32 बिट मोड डीबग में थे।

64 बिट रिलीज मोड में: लंबा: 875 एमएमएस लंबे समय तक: 906 एमएमएस int: 1047ms

इससे पता चलता है कि मैंने देखा परिणाम जेएमएम अनुकूलन अजीबता सीपीयू सीमाओं के बजाय है।

#include "stdafx.h"
#include "iostream"
#include "windows.h"
#include "limits.h"

long long i = INT_MAX;

using namespace std;


boolean decrementAndCheck() {
return --i < 0;
}


int _tmain(int argc, _TCHAR* argv[])
{


cout << "Starting the loop" << endl;

unsigned long startTime = GetTickCount64();
while (!decrementAndCheck()){
}
unsigned long endTime = GetTickCount64();

cout << "Finished the loop in " << (endTime - startTime) << "ms" << endl;



}

संपादित करें: जावा 8 आरटीएम में बस इसे फिर से प्रयास करें, कोई महत्वपूर्ण बदलाव नहीं।


जब आप long उपयोग करते हैं तो मेरा जेवीएम आंतरिक लूप को यह बहुत सीधी चीज करता है:

0x00007fdd859dbb80: test   %eax,0x5f7847a(%rip)  /* fun JVM hack */
0x00007fdd859dbb86: dec    %r11                  /* i-- */
0x00007fdd859dbb89: mov    %r11,0x258(%r10)      /* store i to memory */
0x00007fdd859dbb90: test   %r11,%r11             /* unnecessary test */
0x00007fdd859dbb93: jge    0x00007fdd859dbb80    /* go back to the loop top */

यह चीट्स, कड़ी मेहनत करता है, जब आप int उपयोग करते हैं; सबसे पहले कुछ खराबता है कि मैं समझने का दावा नहीं करता लेकिन एक अनियंत्रित पाश के लिए सेटअप की तरह दिखता है:

0x00007f3dc290b5a1: mov    %r11d,%r9d
0x00007f3dc290b5a4: dec    %r9d
0x00007f3dc290b5a7: mov    %r9d,0x258(%r10)
0x00007f3dc290b5ae: test   %r9d,%r9d
0x00007f3dc290b5b1: jl     0x00007f3dc290b662
0x00007f3dc290b5b7: add    $0xfffffffffffffffe,%r11d
0x00007f3dc290b5bb: mov    %r9d,%ecx
0x00007f3dc290b5be: dec    %ecx              
0x00007f3dc290b5c0: mov    %ecx,0x258(%r10)   
0x00007f3dc290b5c7: cmp    %r11d,%ecx
0x00007f3dc290b5ca: jle    0x00007f3dc290b5d1
0x00007f3dc290b5cc: mov    %ecx,%r9d
0x00007f3dc290b5cf: jmp    0x00007f3dc290b5bb
0x00007f3dc290b5d1: and    $0xfffffffffffffffe,%r9d
0x00007f3dc290b5d5: mov    %r9d,%r8d
0x00007f3dc290b5d8: neg    %r8d
0x00007f3dc290b5db: sar    $0x1f,%r8d
0x00007f3dc290b5df: shr    $0x1f,%r8d
0x00007f3dc290b5e3: sub    %r9d,%r8d
0x00007f3dc290b5e6: sar    %r8d
0x00007f3dc290b5e9: neg    %r8d
0x00007f3dc290b5ec: and    $0xfffffffffffffffe,%r8d
0x00007f3dc290b5f0: shl    %r8d
0x00007f3dc290b5f3: mov    %r8d,%r11d
0x00007f3dc290b5f6: neg    %r11d
0x00007f3dc290b5f9: sar    $0x1f,%r11d
0x00007f3dc290b5fd: shr    $0x1e,%r11d
0x00007f3dc290b601: sub    %r8d,%r11d
0x00007f3dc290b604: sar    $0x2,%r11d
0x00007f3dc290b608: neg    %r11d
0x00007f3dc290b60b: and    $0xfffffffffffffffe,%r11d
0x00007f3dc290b60f: shl    $0x2,%r11d
0x00007f3dc290b613: mov    %r11d,%r9d
0x00007f3dc290b616: neg    %r9d
0x00007f3dc290b619: sar    $0x1f,%r9d
0x00007f3dc290b61d: shr    $0x1d,%r9d
0x00007f3dc290b621: sub    %r11d,%r9d
0x00007f3dc290b624: sar    $0x3,%r9d
0x00007f3dc290b628: neg    %r9d
0x00007f3dc290b62b: and    $0xfffffffffffffffe,%r9d
0x00007f3dc290b62f: shl    $0x3,%r9d
0x00007f3dc290b633: mov    %ecx,%r11d
0x00007f3dc290b636: sub    %r9d,%r11d
0x00007f3dc290b639: cmp    %r11d,%ecx
0x00007f3dc290b63c: jle    0x00007f3dc290b64f
0x00007f3dc290b63e: xchg   %ax,%ax /* OK, fine; I know what a nop looks like */

फिर अनियंत्रित पाश स्वयं:

0x00007f3dc290b640: add    $0xfffffffffffffff0,%ecx
0x00007f3dc290b643: mov    %ecx,0x258(%r10)
0x00007f3dc290b64a: cmp    %r11d,%ecx
0x00007f3dc290b64d: jg     0x00007f3dc290b640

फिर अनियंत्रित पाश के लिए टियरडाउन कोड, स्वयं एक परीक्षण और सीधे लूप:

0x00007f3dc290b64f: cmp    $0xffffffffffffffff,%ecx
0x00007f3dc290b652: jle    0x00007f3dc290b662
0x00007f3dc290b654: dec    %ecx
0x00007f3dc290b656: mov    %ecx,0x258(%r10)
0x00007f3dc290b65d: cmp    $0xffffffffffffffff,%ecx
0x00007f3dc290b660: jg     0x00007f3dc290b654

इसलिए यह स्याही के लिए 16 गुना तेजी से चला जाता है क्योंकि जेआईटी ने 16 बार int लूप को अनलॉक किया, लेकिन long लूप को अनलॉक नहीं किया।

पूर्णता के लिए, यह कोड है जिसे मैंने वास्तव में कोशिश की थी:

public class foo136 {
  private static int i = Integer.MAX_VALUE;
  public static void main(String[] args) {
    System.out.println("Starting the loop");
    for (int foo = 0; foo < 100; foo++)
      doit();
  }

  static void doit() {
    i = Integer.MAX_VALUE;
    long startTime = System.currentTimeMillis();
    while(!decrementAndCheck()){
    }
    long endTime = System.currentTimeMillis();
    System.out.println("Finished the loop in " + (endTime - startTime) + "ms");
  }

  private static boolean decrementAndCheck() {
    return --i < 0;
  }
}

असेंबली डंप विकल्पों का उपयोग करके जेनरेट किए गए -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly । ध्यान दें कि आपके लिए यह काम करने के लिए आपको अपने JVM स्थापना के साथ गड़बड़ करने की आवश्यकता है; आपको कुछ यादृच्छिक साझा लाइब्रेरी को सही जगह पर रखना होगा या यह असफल हो जाएगा।


रिकॉर्ड के लिए:

अगर मैं उपयोग करता हूँ

boolean decrementAndCheckLong() {
    lo = lo - 1l;
    return lo < -1l;
}

("एल--" से "एल = एल - 1 एल" में बदल गया) लंबे प्रदर्शन में ~ 50%


जेवीएम स्टैक को शब्दों के संदर्भ में परिभाषित किया गया है, जिसका आकार एक कार्यान्वयन विस्तार है लेकिन कम से कम 32 बिट चौड़ा होना चाहिए। जेवीएम कार्यान्वयन 64-बिट शब्दों का उपयोग कर सकता है , लेकिन बाइटकोड इस पर भरोसा नहीं कर सकता है, और इसलिए long या double मूल्यों वाले संचालन को अतिरिक्त देखभाल के साथ संभालना होगा। विशेष रूप से, JVM पूर्णांक शाखा निर्देश बिल्कुल प्रकार int पर परिभाषित किए जाते हैं।

आपके कोड के मामले में, disassembly निर्देशक है। ओरेकल जेडीके 7 द्वारा संकलित int वर्जन के लिए बाइटकोड यहां दिया गया है:

private static boolean decrementAndCheck();
  Code:
     0: getstatic     #14  // Field i:I
     3: iconst_1      
     4: isub          
     5: dup           
     6: putstatic     #14  // Field i:I
     9: ifge          16
    12: iconst_1      
    13: goto          17
    16: iconst_0      
    17: ireturn       

ध्यान दें कि JVM आपके स्थिर i (0) के मान को लोड करेगा, एक (3-4) घटाएं, स्टैक (5) पर मान डुप्लिकेट करें, और इसे चर (6) में वापस धक्का दें। इसके बाद यह एक तुलना-शून्य-शून्य शाखा और रिटर्न करता है।

long साथ संस्करण थोड़ा और जटिल है:

private static boolean decrementAndCheck();
  Code:
     0: getstatic     #14  // Field i:J
     3: lconst_1      
     4: lsub          
     5: dup2          
     6: putstatic     #14  // Field i:J
     9: lconst_0      
    10: lcmp          
    11: ifge          18
    14: iconst_1      
    15: goto          19
    18: iconst_0      
    19: ireturn       

सबसे पहले, जब JVM स्टैक (5) पर नए मान को डुप्लिकेट करता है, तो उसे दो स्टैक शब्दों को डुप्लिकेट करना होगा। आपके मामले में, यह काफी संभव है कि यह एक डुप्लिकेट करने से अधिक महंगा नहीं है, क्योंकि JVM सुविधाजनक होने पर 64-बिट शब्द का उपयोग करने के लिए स्वतंत्र है। हालांकि, आप देखेंगे कि शाखा तर्क यहां लंबा है। JVM में शून्य के साथ long तुलना करने के लिए कोई निर्देश नहीं है, इसलिए इसे स्थिर 0L को स्टैक (9) पर धक्का देना है, सामान्य गणना (10), और उसके बाद उस गणना के मूल्य पर शाखा करें।

यहां दो व्यावहारिक परिदृश्य हैं:

  • JVM बाइटकोड पथ का पालन कर रहा है। इस मामले में, यह long संस्करण में अधिक काम कर रहा है, कई अतिरिक्त मूल्यों को दबाकर और पॉपिंग कर रहा है, और ये वर्चुअल प्रबंधित स्टैक पर हैं , असली हार्डवेयर-समर्थित CPU स्टैक नहीं। यदि ऐसा है, तो भी आप गर्मियों के बाद एक महत्वपूर्ण प्रदर्शन अंतर देखेंगे।
  • JVM को पता चलता है कि यह इस कोड को अनुकूलित कर सकता है। इस मामले में, व्यावहारिक रूप से अनावश्यक धक्का / तर्क की तुलना में कुछ ऑप्टिमाइज़ करने में अतिरिक्त समय लग रहा है। यदि ऐसा है, तो आप गर्मियों के बाद बहुत कम प्रदर्शन अंतर देखेंगे।

मैं आपको जेआईटी किक करने के प्रभाव को खत्म करने के लिए एक सही माइक्रोबाइंचमार्क लिखने की सलाह देता हूं, और यह भी अंतिम स्थिति के साथ कोशिश कर रहा हूं जो कि शून्य नहीं है, ताकि JVM को उस int पर समान तुलना करने के लिए मजबूर किया जा सके जो long करता है ।


मैंने अभी caliper का उपयोग करके एक बेंचमार्क लिखा है।

results मूल कोड के साथ काफी संगत हैं: long int का उपयोग करने के लिए ~ 12x स्पीडअप। यह निश्चित रूप से लगता है कि लूप अनलॉकिंग tmyklebu द्वारा रिपोर्ट की गई है या कुछ बहुत समान है।

timeIntDecrements         195,266,845.000
timeLongDecrements      2,321,447,978.000

यह मेरा कोड है; ध्यान दें कि यह caliper ताजा निर्मित स्नैपशॉट का उपयोग करता है, क्योंकि मैं यह नहीं समझ पाया कि उनके मौजूदा बीटा रिलीज के खिलाफ कोड कैसे करें।

package test;

import com.google.caliper.Benchmark;
import com.google.caliper.Param;

public final class App {

    @Param({""+1}) int number;

    private static class IntTest {
        public static int v;
        public static void reset() {
            v = Integer.MAX_VALUE;
        }
        public static boolean decrementAndCheck() {
            return --v < 0;
        }
    }

    private static class LongTest {
        public static long v;
        public static void reset() {
            v = Integer.MAX_VALUE;
        }
        public static boolean decrementAndCheck() {
            return --v < 0;
        }
    }

    @Benchmark
    int timeLongDecrements(int reps) {
        int k=0;
        for (int i=0; i<reps; i++) {
            LongTest.reset();
            while (!LongTest.decrementAndCheck()) { k++; }
        }
        return (int)LongTest.v | k;
    }    

    @Benchmark
    int timeIntDecrements(int reps) {
        int k=0;
        for (int i=0; i<reps; i++) {
            IntTest.reset();
            while (!IntTest.decrementAndCheck()) { k++; }
        }
        return IntTest.v | k;
    }
}

मेरे पास परीक्षण करने के लिए 64 बिट मशीन नहीं है, लेकिन इसके बजाय बड़े अंतर से पता चलता है कि काम पर थोड़ा लंबा बाइटकोड से अधिक है।

मैं अपने 32-बिट 1.7.0_45 पर लंबे / int (4400 बनाम 4800ms) के लिए बहुत करीबी समय देखता हूं।

यह केवल एक अनुमान है , लेकिन मुझे दृढ़ता से संदेह है कि यह एक स्मृति गलत संरेखण दंड का प्रभाव है। संदेह की पुष्टि / इनकार करने के लिए, एक सार्वजनिक स्थैतिक int dummy = 0 जोड़ने का प्रयास करें; i की घोषणा से पहले । इससे मुझे मेमोरी लेआउट में 4 बाइट्स तक धक्का दिया जाएगा और बेहतर प्रदर्शन के लिए इसे सही तरीके से गठबंधन किया जा सकता है। इस मुद्दे को उत्पन्न नहीं करने की पुष्टि की।

संपादित करें: इसके पीछे तर्क यह है कि वीएम इष्टतम संरेखण के लिए पैडिंग जोड़कर अपने अवकाश में खेतों को पुन: व्यवस्थित नहीं कर सकता है, क्योंकि इससे जेएनआई में हस्तक्षेप हो सकता है (मामला नहीं)।





long-integer