X64 Java'da int'den neden long daha yavaş?


92

Surface Pro 2 tablette Java 7 güncelleme 45 x64 (32 bit Java yüklü değil) ile Windows 8.1 x64 çalıştırıyorum.

Aşağıdaki kod, i türü uzun olduğunda 1688 ms ve i bir int olduğunda 109 ms sürer. Neden uzun (64 bit tip) 64 bitlik bir JVM ile 64 bit platformda int'den daha büyük bir sıra daha yavaştır?

Tek spekülasyonum, CPU'nun 64 bitlik bir tamsayı eklemesinin 32 bit olandan daha uzun sürmesi, ancak bu pek olası görünmüyor. Haswell'in dalgalanma taşıma toplayıcıları kullanmadığından şüpheleniyorum.

Bunu Eclipse Kepler SR1, btw'de çalıştırıyorum.

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;
    }

}

Düzenleme: İşte VS 2013 (aşağıda), aynı sistem tarafından derlenen eşdeğer C ++ kodunun sonuçları. uzun: 72265ms int: 74656ms Bu sonuçlar 32 bitlik hata ayıklama modundaydı.

64 bit bırakma modunda: uzun: 875 ms uzun uzun: 906ms int: 1047ms

Bu, gözlemlediğim sonucun CPU sınırlamalarından ziyade JVM optimizasyonu tuhaflığı olduğunu gösteriyor.

#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;



}

Düzenleme: Bunu Java 8 RTM'de tekrar denedim, önemli bir değişiklik yok.


8
En olası şüpheli sizin kurulumunuzdur, CPU veya JVM'nin çeşitli bölümleri değil. Bu ölçümü güvenilir bir şekilde yeniden üretebilir misiniz? Döngüyü tekrar etmemek currentTimeMillis(), JIT'i ısıtmamak, tamamen optimize edilebilen kodu kullanmak , çalıştırmak vb. Güvenilmez sonuçlar doğurur.

1
Bir süre önce kıyaslama longyapıyordum, döngü sayacı olarak a kullanmak zorundaydım , çünkü JIT derleyicisi döngü çıkışını bir int. Üretilen makine kodunun sökülmesine bakmak gerekir.
Sam

7
Bu doğru bir mikro ölçüt değil ve sonuçlarının hiçbir şekilde gerçeği yansıtmasını beklemiyorum.
Louis Wasserman

7
Uygun bir Java mikro ölçüsü yazamadığı için OP'yi eleştiren tüm yorumlar, açık bir şekilde tembel. Bu, JVM'nin koda ne yaptığına bakarsanız anlaması çok kolay olan bir şeydir.
tmyklebu

2
@maaartinus: Kabul edilen uygulama, bilinen tuzaklardan oluşan bir liste etrafında çalıştığı için kabul edilmiş bir uygulamadır. Uygun Java Benchmarkları söz konusu olduğunda, yığın üzerinde bir değiştirme değil, doğru şekilde optimize edilmiş kodu ölçtüğünüzden ve ölçümlerinizin sonunda temiz olduğundan emin olmak istersiniz. OP tamamen farklı bir sorun buldu ve sağladığı kıyaslama bunu yeterince gösterdi. Ve belirtildiği gibi, bu kodu Uygun Java Benchmark'a dönüştürmek aslında tuhaflığı ortadan kaldırmaz. Ve montaj kodunu okumak zor değil.
tmyklebu

Yanıtlar:


82

Benim JVM, longs kullandığınızda bunu iç döngüye oldukça basit bir şekilde yapar :

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 */

intS kullandığınızda hile yapar, zor ; Öncelikle, anladığımı iddia etmediğim, ancak kaydırılmamış bir döngü için kurulum gibi görünen bazı saçmalıklar var:

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 */

sonra açılmış döngünün kendisi:

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

daha sonra, kıvrılmamış döngü için sökme kodu, kendisi bir test ve bir düz döngü:

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

Bu nedenle, inçler için 16 kat daha hızlı gider çünkü JIT intdöngüyü 16 kez longaçtı , ancak döngüyü hiç açmadı .

Tamlık için, işte gerçekten denediğim kod:

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;
  }
}

Seçenekler kullanılarak montaj dökümleri oluşturuldu -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly. Bunun sizin için de çalışmasını sağlamak için JVM kurulumunuzla uğraşmanız gerektiğini unutmayın; rastgele paylaşılan bir kitaplığı tam olarak doğru yere koymanız gerekir, aksi takdirde başarısız olur.


9
Tamam, yani net-net longsürümün daha yavaş olması değil, intsürümün daha hızlı olması değil. Mantıklı. Muhtemelen JIT'i optimize longifadeleri yapmak için çok fazla çaba harcanmamıştı .
Hot Licks

1
... cahilliğimi affedin, ama "lakaplı" nedir? Bu terimi doğru bir şekilde google'da aratamıyorum bile ve bu, internette bir kelimenin ne anlama geldiğini ilk defa birine sormam gerektiği anlamına geliyor.
BrianH

1
@BrianDHall gcc, -f"bayrak" için komut satırı anahtarı olarak kullanır ve söylenerek unroll-loopsoptimizasyon açılır -funroll-loops. Optimizasyonu tanımlamak için sadece "unroll" kullanıyorum.
chrylis

4
@BRPocock: Java derleyicisi yapamaz, ancak JIT kesinlikle yapabilir.
tmyklebu

1
Açıkça söylemek gerekirse, "eğlence" değildi. Onu açtı VE açılmamış döngüyü i-=16, tabii ki 16 kat daha hızlı olana dönüştürdü .
Aleksandr Dubinsky

22

JVM yığını, boyutu bir uygulama ayrıntısı olan ancak en az 32 bit genişliğinde olması gereken sözcükler cinsinden tanımlanır . JVM uygulayıcısı olabilir 64 bit kelimeler kullanmak, ancak baytkodu ile bu konuda ve bu nedenle operasyonları güvenemez longveya doubledeğerler ekstra dikkatle ele alınması gerekir. Özellikle, JVM tamsayı dal talimatları tam olarak tipte tanımlanır int.

Kodunuz söz konusu olduğunda, demontaj öğreticidir. intOracle JDK 7 tarafından derlenen sürümün bayt kodu şöyledir:

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'nin statik i(0) değerinizi yükleyeceğini, birini (3-4) çıkaracağını , yığındaki (5) değeri çoğaltacağını ve değişkene (6) geri iteceğini unutmayın. Daha sonra sıfırla karşılaştırma dalı yapar ve döndürür.

İle birlikte sürüm longbiraz daha karmaşık:

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       

İlk olarak, JVM yığındaki (5) yeni değeri çoğalttığında, iki yığın sözcüğünü çoğaltması gerekir. Sizin durumunuzda, JVM uygunsa 64 bitlik bir kelime kullanmakta özgür olduğundan, bunun bir kopyasını kopyalamaktan daha pahalı olmaması oldukça olasıdır. Ancak burada dallanma mantığının daha uzun olduğunu fark edeceksiniz. JVM'nin a'yı longsıfır ile karşılaştırma talimatı yoktur , bu nedenle 0Lyığına (9) bir sabit itmesi , genel bir longkarşılaştırma yapması (10) ve ardından bu hesaplamanın değerinde dallara ayrılması gerekir.

İşte iki makul senaryo:

  • JVM, bayt kodu yolunu tam olarak izliyor. Bu durumda, longsürümde daha fazla iş yapıyor , birkaç ekstra değeri zorluyor ve patlatıyor ve bunlar gerçek donanım destekli CPU yığını değil, sanal yönetilen yığın üzerindedir . Bu durumda, ısınmadan sonra yine de önemli bir performans farkı göreceksiniz.
  • JVM, bu kodu optimize edebileceğinin farkındadır. Bu durumda, pratik olarak gereksiz bazı itme / karşılaştırma mantığını optimize etmek fazladan zaman alır. Durum buysa, ısınmadan sonra çok az performans farkı göreceksiniz.

Sana tavsiye doğru microbenchmark yazma JIT tekme sahip ve aynı zamanda aynı karşılaştırma yapmak JVM zorlamak için, sıfır olmayan bir nihai koşulu ile bu çalışmakla etkisini ortadan kaldırmak için intonunla yaptığı long.


1
@Katona Mutlaka değil. Özellikle, İstemci ve Sunucu HotSpot JVM'leri tamamen farklı uygulamalardır ve Ilya Sunucuyu seçmeyi belirtmedi (İstemci genellikle 32-bit varsayılandır).
chrylis

1
@tmyklebu Sorun şu ki, kıyaslama aynı anda birkaç farklı şeyi ölçüyor. Sıfır olmayan bir terminal koşulunun kullanılması değişkenlerin sayısını azaltır.
chrylis

1
@tmyklebu Önemli olan nokta, OP'nin inçler ile uzunlar arasındaki artışların, düşüşlerin ve karşılaştırmaların hızlarını karşılaştırmayı amaçlamış olmasıdır. Bunun yerine (bu cevabın doğru olduğunu varsayarak) sadece karşılaştırmaları ölçüyorlardı ve sadece özel bir durum olan 0'a karşı ölçüyorlardı. Hiçbir şey değilse, orijinal kriteri yanıltıcı hale getiriyor - üç genel durumu ölçüyor gibi görünüyor, aslında tek bir özel durumu ölçüyor.
yshavit

1
@tmyklebu Beni yanlış anlamayın, soruyu, bu cevabı ve cevabınızı ekledim. Ancak, @chrylis'in ölçmeye çalıştığı farkı ölçmeyi durdurmak için kıyaslamayı ayarladığı şeklindeki ifadenize katılmıyorum. Yanılıyorsam OP beni düzeltebilir, ancak sadece / öncelikli olarak ölçmeye çalışıyorlar gibi görünmüyor == 0, bu da kıyaslama sonuçlarının orantısız bir şekilde büyük bir parçası gibi görünüyor. Bana göre OP'nin daha genel bir operasyon aralığını ölçmeye çalıştığı daha muhtemel görünüyor ve bu cevap, kıyas ölçütünün bu operasyonlardan sadece birine doğru oldukça çarpık olduğuna işaret ediyor.
yshavit

2
@tmyklebu Hiç de değil. Ben tamamen kök nedenleri anlamak için varım. Ama, bir büyük kök neden kriter çarpık olduğunu olduğunu tespit ettikten, bu çarpık kaldırmak için kriter değiştirmek için geçersiz değil aynı zamanda kazmak ve daha verimli etkinleştirebilirsiniz ki bu çarpıklık (için örnek daha iyi anlamak için bayt kodu, döngüleri açmayı kolaylaştırabilir, vb.) Bu yüzden hem bu yanıtı (çarpıklığı belirleyen) hem de sizinkini (çarpıklığı daha ayrıntılı olarak kazıyor) yükselttim.
yshavit

8

Java Sanal Makinesindeki temel veri birimi word'dür. Doğru kelime boyutunu seçmek, JVM'nin uygulanmasına bırakılır. Bir JVM uygulaması minimum 32 bitlik bir kelime boyutu seçmelidir. Verimlilik kazanmak için daha yüksek bir kelime boyutu seçebilir. 64 bitlik bir JVM'nin yalnızca 64 bit kelimeyi seçmesi konusunda da herhangi bir kısıtlama yoktur.

Altta yatan mimari, kelime boyutunun da aynı olması gerektiğine hükmetmez. JVM verileri kelime kelime okur / yazar. Bir int'ten daha uzun sürmesinin nedeni budur .

Burada aynı konu hakkında daha fazlasını bulabilirsiniz.


4

Sadece kullanarak bir kriter yazdım pergeli .

Sonuçlar kullanmak için ~ 12x Speedup: Orijinal kodu ile oldukça tutarlıdır intüzerinde long. Görünüşe göre tmyklebu veya buna çok benzer bir şey tarafından bildirilen döngü açma işlemi devam ediyor.

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

Bu benim kodum; calipermevcut beta sürümlerine karşı nasıl kod yazacağımı çözemediğim için yeni oluşturulmuş bir anlık görüntüsünü kullandığına dikkat edin .

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;
    }
}

1

Kayıt için, bu sürüm kaba bir "ısınma" yapıyor:

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;
    }

}

Genel süreler yaklaşık% 30 iyileşir, ancak ikisi arasındaki oran kabaca aynı kalır.


@TedHopp - Benimki döngü sınırlarını değiştirmeyi denedim ve esasen değişmeden kaldı.
Hot Licks

@ Techrocket9: intBu kodla benzer sayılar alıyorum ( 20 kat daha hızlı).
tmyklebu

1

Kayıtlar için:

kullanırsam

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

("l--", "l = l - 1l" olarak değiştirildi) uzun performans ~% 50 arttı


0

Test edilecek 64 bit makinem yok, ancak oldukça büyük fark, işte biraz daha uzun bayt kodundan daha fazlası olduğunu gösteriyor.

32-bit 1.7.0_45'imde long / int (4400 vs 4800ms) için çok yakın zamanlar görüyorum.

Bu sadece bir tahmin , ancak bunun bir hafıza yanlış hizalama cezasının etkisi olduğundan kesinlikle şüpheleniyorum. Şüpheyi onaylamak / reddetmek için, public static int kukla = 0 eklemeyi deneyin; i. beyanından önce . Bu, bellek düzeninde i'yi 4 bayt aşağı iter ve daha iyi performans için düzgün şekilde hizalanmasını sağlayabilir. Soruna neden olmadığı onaylandı.

DÜZENLE: Bunun arkasındaki mantık, sanal makinenin alanları boş zamanlarında en uygun hizalama için doldurma ekleyerek yeniden sıralayamayacağıdır , çünkü bu, JNI ile çakışabilir. (Durum böyle değil).


VM kesinlikle edilir sipariş alanlar ve eklenti dolgusu izin verdi.
Hot Licks

GC, yerel kod çalışırken gerçekleşebileceğinden, yine de birkaç opak tutamaç alan bu sinir bozucu, yavaş erişimci yöntemleri aracılığıyla nesnelere erişmelidir. Alanları yeniden sıralamak ve dolgu eklemek oldukça ücretsizdir.
tmyklebu
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.