Mengapa panjang lebih lambat dari int di x64 Java?

92

Saya menjalankan Windows 8.1 x64 dengan pembaruan Java 7 45 x64 (tidak ada Java 32 bit yang diinstal) di tablet Surface Pro 2.

Kode di bawah ini membutuhkan waktu 1688ms ketika tipe i adalah panjang dan 109ms ketika i adalah sebuah int. Mengapa long (tipe 64 bit) urutan besarnya lebih lambat daripada int pada platform 64 bit dengan 64 bit JVM?

Satu-satunya spekulasi saya adalah bahwa CPU membutuhkan waktu lebih lama untuk menambahkan integer 64 bit daripada yang 32 bit, tetapi itu tampaknya tidak mungkin. Saya menduga Haswell tidak menggunakan penambah ripple-carry.

Saya menjalankan ini di Eclipse Kepler SR1, btw.

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

}

Sunting: Berikut adalah hasil dari kode C ++ setara yang dikompilasi oleh VS 2013 (di bawah), sistem yang sama. panjang: 72265ms int: 74656ms Hasil tersebut dalam mode debug 32 bit.

Dalam mode rilis 64 bit: panjang: 875ms panjang panjang: 906ms int: 1047ms

Ini menunjukkan bahwa hasil yang saya amati adalah keanehan pengoptimalan JVM daripada batasan CPU.

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



}

Edit: Coba ini lagi di Java 8 RTM, tidak ada perubahan signifikan.

Techrocket9
sumber
8
Tersangka yang paling mungkin adalah pengaturan Anda, bukan CPU atau berbagai bagian JVM. Dapatkah Anda mereproduksi pengukuran ini dengan andal? Tidak mengulangi perulangan, tidak menghangatkan JIT, menggunakan currentTimeMillis(), menjalankan kode yang dapat dengan mudah dioptimalkan sepenuhnya, dll. Berbau hasil yang tidak dapat diandalkan.
1
Saya melakukan benchmarking beberapa waktu yang lalu, saya harus menggunakan a longsebagai penghitung loop, karena kompiler JIT mengoptimalkan loop keluar, ketika saya menggunakan file int. Seseorang perlu melihat pembongkaran kode mesin yang dihasilkan.
Sam
7
Ini bukan microbenchmark yang benar, dan saya tidak berharap bahwa hasilnya mencerminkan kenyataan dengan cara apa pun.
Louis Wasserman
7
Semua komentar yang memarahi OP karena gagal menulis microbenchmark Java yang benar sangat malas. Ini adalah jenis hal yang sangat mudah diketahui jika Anda hanya melihat dan melihat apa yang dilakukan JVM pada kode.
tmyklebu
2
@maaartinus: Praktik yang diterima adalah praktik yang diterima karena bekerja di sekitar daftar perangkap yang diketahui. Dalam kasus Benchmark Java yang Tepat, Anda ingin memastikan bahwa Anda mengukur kode yang dioptimalkan dengan benar, bukan pengganti on-stack, dan Anda ingin memastikan pengukuran Anda bersih di akhir. OP menemukan masalah yang sama sekali berbeda dan patokan yang dia berikan cukup menunjukkannya. Dan, seperti yang telah disebutkan, mengubah kode ini menjadi Patokan Java yang Benar tidak benar-benar menghilangkan keanehan. Dan membaca kode assembly tidaklah sulit.
tmyklebu

Jawaban:

82

JVM saya melakukan hal yang cukup mudah ini ke loop dalam saat Anda menggunakan longs:

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

Ini curang, sulit, saat Anda menggunakan ints; pertama-tama ada beberapa kekeliruan yang saya klaim tidak saya pahami tetapi sepertinya pengaturan untuk loop yang tidak diputar:

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

lalu loop yang tidak digulung itu sendiri:

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

kemudian kode pembongkaran untuk loop yang tidak digulung, itu sendiri merupakan tes dan loop lurus:

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

Jadi itu berjalan 16 kali lebih cepat untuk int karena JIT membuka gulungan intloop 16 kali, tetapi tidak membuka gulungan longsama sekali.

Untuk kelengkapannya, berikut ini kode yang sebenarnya saya coba:

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

Tempat pembuangan perakitan dibuat menggunakan opsi -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly. Perhatikan bahwa Anda perlu mengotak-atik instalasi JVM Anda agar ini berfungsi untuk Anda juga; Anda perlu meletakkan beberapa pustaka bersama secara acak di tempat yang tepat atau itu akan gagal.

tmyklebu.dll
sumber
9
Oke, jadi net-net bukan berarti longversinya lebih lambat, tetapi intversinya lebih cepat. Itu masuk akal. Kemungkinan tidak banyak upaya yang diinvestasikan dalam membuat longekspresi JIT mengoptimalkan .
Hot Licks
1
... maafkan ketidaktahuan saya, tapi apa yang "disodorkan"? Saya bahkan tidak dapat menggunakan istilah Google dengan benar, dan itu membuat ini pertama kalinya saya harus bertanya kepada seseorang apa arti sebuah kata di internet.
BrianH
1
@BrianDHall gccdigunakan -fsebagai saklar baris perintah untuk "bendera", dan unroll-loopspengoptimalan dihidupkan dengan mengatakan -funroll-loops. Saya hanya menggunakan "buka gulungan" untuk menjelaskan pengoptimalan.
chrylis -cautiouslyoptimistic-
4
@BRPocock: Kompilator Java tidak bisa, tetapi JIT yakin bisa.
tmyklebu
1
Hanya untuk memperjelas, itu tidak "mengolok-olok" itu. Ini membuka gulungannya DAN mengubah putaran yang tidak digulung menjadi i-=16, yang tentu saja 16x lebih cepat.
Aleksandr Dubinsky
22

Tumpukan JVM didefinisikan dalam istilah kata - kata , yang ukurannya adalah detail implementasi tetapi harus memiliki lebar setidaknya 32 bit. Pelaksana JVM dapat menggunakan kata 64-bit, tetapi bytecode tidak dapat mengandalkan ini, sehingga operasi dengan nilai longatau doubleharus ditangani dengan ekstra hati-hati. Secara khusus, instruksi cabang integer JVM didefinisikan tepat pada jenisnya int.

Dalam kasus kode Anda, pembongkaran bersifat instruktif. Berikut bytecode untuk intversi yang dikompilasi oleh Oracle JDK 7:

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       

Perhatikan bahwa JVM akan memuat nilai statis Anda i(0), kurangi satu (3-4), duplikat nilai pada tumpukan (5), dan dorong kembali ke variabel (6). Ia kemudian melakukan perbandingan-dengan-nol cabang dan kembali.

Versi dengan longsedikit lebih rumit:

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       

Pertama, ketika JVM menduplikasi nilai baru pada stack (5), JVM harus menduplikasi dua stack word. Dalam kasus Anda, sangat mungkin bahwa ini tidak lebih mahal daripada menduplikasi satu, karena JVM bebas menggunakan kata 64-bit jika nyaman. Namun, Anda akan melihat bahwa logika cabang lebih panjang di sini. JVM tidak memiliki instruksi untuk membandingkan longdengan nol, sehingga memiliki untuk mendorong konstan 0Lke dalam stack (9), melakukan umum longperbandingan (10), dan kemudian cabang pada nilai yang perhitungan.

Berikut dua skenario yang masuk akal:

  • JVM mengikuti jalur bytecode dengan tepat. Dalam hal ini, ia melakukan lebih banyak pekerjaan dalam longversi, mendorong dan memunculkan beberapa nilai tambahan, dan ini ada di tumpukan terkelola virtual , bukan tumpukan CPU yang dibantu perangkat keras yang sebenarnya. Jika demikian, Anda masih akan melihat perbedaan kinerja yang signifikan setelah pemanasan.
  • JVM menyadari bahwa ia dapat mengoptimalkan kode ini. Dalam hal ini, perlu waktu ekstra untuk mengoptimalkan beberapa logika dorong / perbandingan yang praktis tidak perlu. Jika demikian, Anda akan melihat perbedaan kinerja yang sangat kecil setelah pemanasan.

Saya sarankan Anda menulis microbenchmark yang benar untuk menghilangkan efek memiliki JIT kick in, dan juga mencoba ini dengan kondisi akhir yang tidak nol, untuk memaksa JVM melakukan perbandingan yang sama pada intyang dilakukannya dengan long.

chrylis -cautiouslyoptimistic-
sumber
1
@Tidak harus. Terutama, JVM HotSpot Klien dan Server adalah implementasi yang sama sekali berbeda, dan Ilya tidak menunjukkan pemilihan Server (Klien biasanya default 32-bit).
chrylis -cautiouslyoptimistic-
1
@tmyklebu Masalahnya adalah bahwa tolok ukur mengukur beberapa hal yang berbeda sekaligus. Menggunakan kondisi terminal bukan nol mengurangi jumlah variabel.
chrylis -cautiouslyoptimistic-
1
@tmyklebu Intinya adalah bahwa OP bermaksud untuk membandingkan kecepatan kenaikan, penurunan dan perbandingan pada ints vs long. Sebaliknya (dengan asumsi jawaban ini benar) mereka hanya mengukur perbandingan, dan hanya terhadap 0, yang merupakan kasus khusus. Jika tidak ada yang lain, itu membuat tolok ukur asli menyesatkan - sepertinya itu mengukur tiga kasus umum, padahal sebenarnya mengukur satu kasus tertentu.
yshavit
1
@tmyklebu Jangan salah paham, saya memberi suara positif pada pertanyaan, jawaban ini, dan jawaban Anda. Tapi saya tidak setuju dengan pernyataan Anda bahwa @chrylis menyesuaikan tolok ukur untuk berhenti mengukur perbedaan yang coba diukurnya. OP dapat mengoreksi saya jika saya salah, tetapi sepertinya mereka tidak hanya mencoba / terutama mengukur == 0, yang tampaknya merupakan bagian besar yang tidak proporsional dari hasil benchmark. Menurut saya, OP mencoba mengukur rentang operasi yang lebih umum, dan jawaban ini menunjukkan bahwa tolok ukur sangat condong ke salah satu operasi tersebut.
yshavit
2
@tmyklebu Tidak sama sekali. Saya sangat ingin memahami akar penyebabnya. Namun, setelah mengidentifikasi bahwa salah satu akar penyebab utama adalah bahwa tolok ukur itu miring, tidak valid untuk mengubah tolok ukur untuk menghilangkan kemiringan tersebut, serta menggali dan memahami lebih lanjut tentang kemiringan itu (misalnya, yang dapat memungkinkan lebih efisien bytecode, agar lebih mudah untuk membuka gulungan, dll). Itulah mengapa saya memberi suara positif pada jawaban ini (yang mengidentifikasi kemiringan) dan milik Anda (yang menggali kemiringan lebih detail).
yshavit
8

Unit dasar data di Java Virtual Machine adalah kata. Memilih ukuran kata yang tepat ditinggalkan setelah implementasi JVM. Implementasi JVM harus memilih ukuran kata minimal 32 bit. Itu dapat memilih ukuran kata yang lebih tinggi untuk mendapatkan efisiensi. Tidak ada batasan bahwa 64 bit JVM harus memilih 64 bit word saja.

Arsitektur yang mendasari tidak mengatur bahwa ukuran kata juga harus sama. JVM membaca / menulis data kata demi kata. Inilah alasan mengapa mungkin butuh waktu lebih lama untuk waktu yang lama daripada int .

Di sini Anda dapat menemukan lebih banyak tentang topik yang sama.

Vaibhav Raj
sumber
4

Saya baru saja menulis patokan menggunakan caliper .

The hasil yang cukup konsisten dengan kode asli: 12x speedup ~ untuk menggunakan intlebih long. Tampaknya loop membuka gulungan yang dilaporkan oleh tmyklebu atau sesuatu yang sangat mirip sedang terjadi.

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

Ini adalah kode saya; perhatikan bahwa ia menggunakan snapshot yang baru dibuat caliper, karena saya tidak tahu cara membuat kode terhadap rilis beta yang ada.

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;
    }
}
tucuxi
sumber
1

Sebagai catatan, versi ini melakukan "pemanasan" yang kasar:

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

}

Waktu keseluruhan meningkat sekitar 30%, tetapi rasio antara keduanya kurang lebih tetap sama.

Licks panas
sumber
@TedHopp - Saya mencoba mengubah batas loop di milik saya dan pada dasarnya tetap tidak berubah.
Hot Licks
@ Techrocket9: Saya mendapatkan nomor yang sama ( int20ish kali lebih cepat) dengan kode ini.
tmyklebu
1

Sebagai catatan:

jika saya menggunakan

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

(mengubah "l--" menjadi "l = l - 1l") performa lama meningkat ~ 50%

R. Moeller
sumber
0

Saya tidak memiliki mesin 64 bit untuk diuji, tetapi perbedaan yang agak besar menunjukkan bahwa ada lebih dari sekedar bytecode yang sedikit lebih panjang di tempat kerja.

Saya melihat waktu yang sangat dekat untuk long / int (4400 vs 4800ms) pada 1.7.0_45 32-bit saya.

Ini hanya tebakan , tetapi saya sangat curiga bahwa ini adalah efek dari hukuman misalignment memory. Untuk mengkonfirmasi / menyangkal kecurigaan, coba tambahkan public static int dummy = 0; sebelum deklarasi i. Itu akan mendorong saya ke bawah sebesar 4 byte dalam tata letak memori dan dapat membuatnya selaras dengan benar untuk kinerja yang lebih baik. Dipastikan tidak menyebabkan masalah.

EDIT: Alasan di balik ini adalah bahwa VM tidak dapat menyusun ulang kolom dengan mudah menambahkan padding untuk penyelarasan yang optimal, karena hal itu dapat mengganggu JNI. (Bukan kasusnya).

Durandal
sumber
VM tentu ini diperbolehkan untuk bidang menyusun ulang dan add padding.
Hot Licks
JNI harus mengakses objek melalui metode aksesor yang menjengkelkan dan lambat ini yang tetap menggunakan beberapa tuas buram karena GC dapat terjadi saat kode native berjalan. Banyak gratis untuk menyusun ulang bidang dan menambahkan bantalan.
tmyklebu