Tại sao lâu chậm hơn int trong Java x64?


90

Tôi đang chạy Windows 8.1 x64 với bản cập nhật Java 7 45 x64 (không cài đặt Java 32 bit) trên máy tính bảng Surface Pro 2.

Đoạn mã dưới đây mất 1688ms khi loại i là dài và 109ms khi i là int. Tại sao long (loại 64 bit) là thứ tự cường độ chậm hơn int trên nền tảng 64 bit với JVM 64 bit?

Suy đoán duy nhất của tôi là CPU mất nhiều thời gian hơn để thêm số nguyên 64 bit so với số nguyên 32 bit, nhưng điều đó có vẻ khó xảy ra. Tôi nghi ngờ Haswell không sử dụng các trình bổ sung ripple-carry.

Tôi đang chạy điều này trong 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;
    }

}

Chỉnh sửa: Đây là kết quả từ mã C ++ tương đương được biên dịch bởi VS 2013 (bên dưới), cùng một hệ thống. dài: 72265ms int: 74656ms Các kết quả đó ở chế độ 32 bit gỡ lỗi.

Trong chế độ phát hành 64 bit: dài: 875ms long long: 906ms int: 1047ms

Điều này cho thấy rằng kết quả mà tôi quan sát được là sự kỳ lạ khi tối ưu hóa JVM chứ không phải là các hạn chế của 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;



}

Chỉnh sửa: Chỉ cần thử lại điều này trong Java 8 RTM, không có thay đổi đáng kể.


8
Điều đáng ngờ nhất là do bạn thiết lập, không phải CPU hoặc các bộ phận khác nhau của JVM. Bạn có thể lặp lại phép đo này một cách đáng tin cậy không? Không lặp lại vòng lặp, không làm nóng JIT, sử dụng currentTimeMillis(), chạy mã có thể được tối ưu hóa hoàn toàn một cách tầm thường, v.v. hàng loạt kết quả không đáng tin cậy.

1
Tôi đã đo điểm chuẩn cách đây một thời gian, tôi phải sử dụng a longlàm bộ đếm vòng lặp, vì trình biên dịch JIT đã tối ưu hóa vòng lặp ra, khi tôi sử dụng một int. Người ta sẽ cần phải xem xét việc tháo rời mã máy được tạo.
Sam

7
Đây không phải là một microbenchmark chính xác và tôi không mong đợi rằng kết quả của nó phản ánh thực tế theo bất kỳ cách nào.
Louis Wasserman

7
Tất cả các bình luận trách móc OP vì không viết được dấu vi mô Java thích hợp là những kẻ lười biếng không thể tả được. Đây là loại điều rất dễ nhận ra nếu bạn chỉ nhìn và xem JVM làm gì với mã.
tmyklebu

2
@maaartinus: Thực hành được chấp nhận là thực hành được chấp nhận vì nó hoạt động xung quanh danh sách các cạm bẫy đã biết. Trong trường hợp Điểm chuẩn Java thích hợp, bạn muốn đảm bảo rằng bạn đang đo mã được tối ưu hóa đúng cách, không phải là mã thay thế trên ngăn xếp và bạn muốn đảm bảo rằng các phép đo của mình ở cuối cùng là sạch. OP đã phát hiện ra một vấn đề hoàn toàn khác và điểm chuẩn mà anh ấy cung cấp đã chứng minh đầy đủ điều đó. Và, như đã lưu ý, việc biến mã này thành Điểm chuẩn Java phù hợp không thực sự làm mất đi sự kỳ lạ. Và đọc mã lắp ráp không khó.
tmyklebu

Câu trả lời:


80

JVM của tôi thực hiện điều này khá đơn giản với vòng lặp bên trong khi bạn sử dụng 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 */

Nó gian lận, khó, khi bạn sử dụng ints; đầu tiên có một số vấn đề rắc rối mà tôi không hiểu nhưng có vẻ như thiết lập cho một vòng lặp không được cuộn:

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

thì chính vòng lặp không được cuộn:

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

sau đó là mã xé nhỏ cho vòng lặp chưa được cuộn, bản thân nó là một thử nghiệm và một vòng lặp thẳng:

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

Vì vậy, nó đi nhanh hơn 16 lần đối với int vì JIT đã bỏ cuộn intvòng lặp 16 lần, nhưng hoàn toàn không bỏ cuộn longvòng lặp.

Để hoàn thiện, đây là mã tôi đã thực sự thử:

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

Các bãi lắp ráp được tạo ra bằng cách sử dụng các tùy chọn -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly. Lưu ý rằng bạn cần phải lộn xộn với cài đặt JVM của mình để có công việc này cho bạn; bạn cần đặt một số thư viện được chia sẻ ngẫu nhiên vào đúng vị trí nếu không sẽ thất bại.


8
OK, vậy net-net không phải là longphiên bản chậm hơn, mà là intphiên bản nhanh hơn. Điều đó có lý. Có vẻ như không có nhiều nỗ lực được đầu tư vào việc thực hiện các longbiểu thức tối ưu hóa JIT .
Hot Licks vào

1
... xin thứ lỗi cho sự thiếu hiểu biết của tôi, nhưng "vui nhộn" là gì? Tôi thậm chí không thể google thuật ngữ này một cách chính xác, và điều đó khiến đây là lần đầu tiên tôi phải hỏi ai đó về nghĩa của một từ trên internet.
BrianH

1
@BrianDHall gccsử dụng -flàm công tắc dòng lệnh cho "cờ" và tính năng unroll-loopstối ưu hóa được bật bằng cách nói -funroll-loops. Tôi chỉ sử dụng "unroll" để mô tả tối ưu hóa.
chrylis -cautilyoptimistic-

4
@BRPocock: Trình biên dịch Java không thể, nhưng chắc chắn JIT có thể.
tmyklebu

1
Chỉ cần rõ ràng, nó không "funroll" nó. Nó đã giải nén nó VÀ chuyển đổi vòng lặp chưa được cuộn thành i-=16, tất nhiên là nhanh hơn 16 lần.
Aleksandr Dubinsky

22

Ngăn xếp JVM được định nghĩa dưới dạng các từ , có kích thước là một chi tiết triển khai nhưng phải rộng ít nhất 32 bit. Trình triển khai JVM có thể sử dụng các từ 64-bit, nhưng mã bytecode không thể dựa vào điều này và do đó các hoạt động với longhoặc doublegiá trị phải được xử lý cẩn thận hơn. Đặc biệt, các lệnh nhánh số nguyên JVM được định nghĩa trên chính xác kiểu int.

Trong trường hợp mã của bạn, việc tháo gỡ mang tính hướng dẫn. Đây là mã bytecode cho intphiên bản được biên dịch bởi 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       

Lưu ý rằng JVM sẽ tải giá trị tĩnh i(0) của bạn, trừ đi một (3-4), nhân đôi giá trị trên ngăn xếp (5) và đẩy nó trở lại biến (6). Sau đó, nó thực hiện một nhánh so sánh với không và trả về.

Phiên bản có longphức tạp hơn một chút:

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       

Đầu tiên, khi JVM nhân bản giá trị mới trên ngăn xếp (5), nó phải sao chép hai từ ngăn xếp. Trong trường hợp của bạn, rất có thể việc này không đắt hơn việc sao chép một từ, vì JVM được miễn phí sử dụng từ 64 bit nếu thuận tiện. Tuy nhiên, bạn sẽ nhận thấy rằng logic nhánh ở đây dài hơn. JVM không có lệnh để so sánh a longvới 0, vì vậy nó phải đẩy một hằng số 0Llên ngăn xếp (9), thực hiện longso sánh tổng quát (10), rồi phân nhánh dựa trên giá trị của phép tính đó .

Đây là hai kịch bản hợp lý:

  • JVM đang đi theo chính xác đường dẫn bytecode. Trong trường hợp này, nó thực hiện nhiều công việc hơn trong longphiên bản, đẩy và xuất một số giá trị bổ sung và những giá trị này nằm trên ngăn xếp được quản lý ảo , không phải ngăn xếp CPU thực sự được hỗ trợ bởi phần cứng. Nếu đúng như vậy, bạn vẫn sẽ thấy sự khác biệt đáng kể về hiệu suất sau khi khởi động.
  • JVM nhận ra rằng nó có thể tối ưu hóa mã này. Trong trường hợp này, sẽ mất thêm thời gian để tối ưu hóa một số logic đẩy / so sánh thực tế không cần thiết. Nếu đúng như vậy, bạn sẽ thấy rất ít sự khác biệt về hiệu suất sau khi khởi động.

Tôi khuyên bạn nên viết một microbenchmark chính xác để loại bỏ ảnh hưởng của việc kích hoạt JIT và cũng thử điều này với điều kiện cuối cùng không phải là 0, để buộc JVM thực hiện cùng một phép so sánh với điều kiện intmà nó làm với long.


1
@Katona Không nhất thiết. Rất đặc biệt, JVM của Máy khách và Máy chủ HotSpot là cách triển khai hoàn toàn khác nhau và Ilya không chỉ ra việc chọn Máy chủ (Máy khách thường là mặc định 32-bit).
chrylis -cautilyoptimistic-

1
@tmyklebu Vấn đề là điểm chuẩn đo nhiều thứ khác nhau cùng một lúc. Sử dụng điều kiện đầu cuối khác không làm giảm số lượng biến.
chrylis -cautilyoptimistic-

1
@tmyklebu Vấn đề là OP đã có ý định so sánh tốc độ tăng, giảm và so sánh giữa ints và longs. Thay vào đó (giả sử câu trả lời này là đúng) họ chỉ đo các phép so sánh và chỉ so với 0, đây là một trường hợp đặc biệt. Nếu không có gì khác, nó làm cho điểm chuẩn ban đầu bị sai lệch - có vẻ như nó đo lường ba trường hợp chung, trong khi trên thực tế, nó đo lường một trường hợp cụ thể.
yshavit

1
@tmyklebu Đừng hiểu sai ý tôi, tôi đã ủng hộ câu hỏi, câu trả lời này và câu trả lời của bạn. Nhưng tôi không đồng ý với tuyên bố của bạn rằng @chrylis đang điều chỉnh điểm chuẩn để ngừng đo lường sự khác biệt mà nó đang cố gắng đo lường. OP có thể sửa cho tôi nếu tôi sai, nhưng có vẻ như họ không cố gắng chỉ / chủ yếu đo lường == 0, điều này dường như chiếm một phần lớn không cân xứng trong kết quả điểm chuẩn. Đối với tôi, có vẻ như OP đang cố gắng đo lường phạm vi hoạt động tổng quát hơn và câu trả lời này chỉ ra rằng điểm chuẩn bị lệch nhiều về phía chỉ một trong những hoạt động đó.
yshavit

2
@tmyklebu Không hề. Tôi là tất cả để hiểu nguyên nhân gốc rễ. Tuy nhiên, đã xác định được rằng một nguyên nhân gốc rễ chính là điểm chuẩn bị lệch, việc thay đổi điểm chuẩn để loại bỏ độ lệch đó không phải là không hợp lệ, cũng như tìm hiểu và hiểu thêm về độ lệch đó (ví dụ: nó có thể cho phép hiệu quả hơn bytecode, nó có thể giúp bạn mở các vòng lặp dễ dàng hơn, v.v.). Đó là lý do tại sao tôi ủng hộ cả câu trả lời này (xác định độ lệch) và câu trả lời của bạn (đi sâu vào độ lệch chi tiết hơn).
yshavit

8

Đơn vị cơ bản của dữ liệu trong Máy ảo Java là từ. Việc chọn kích thước từ phù hợp còn lại khi thực hiện JVM. Việc triển khai JVM nên chọn kích thước từ tối thiểu là 32 bit. Nó có thể chọn kích thước từ cao hơn để đạt được hiệu quả. Không có bất kỳ hạn chế nào rằng JVM 64 bit chỉ nên chọn từ 64 bit.

Kiến trúc cơ bản không quy định rằng kích thước từ cũng phải giống nhau. JVM đọc / ghi dữ liệu từng từ một. Đây là lý do tại sao nó có thể mất nhiều thời gian hơn int .

Ở đây bạn có thể tìm thêm về chủ đề tương tự.


4

Tôi vừa viết một điểm chuẩn bằng thước cặp .

Các kết quả khá phù hợp với mã gốc: a ~ 12x tăng tốc cho việc sử dụng inttrên long. Có vẻ như chắc chắn rằng việc hủy cuộn vòng lặp được báo cáo bởi tmyklebu hoặc một cái gì đó tương tự đang diễn ra.

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

Đây là mã của tôi; lưu ý rằng nó sử dụng ảnh chụp nhanh mới được xây dựng caliper, vì tôi không thể tìm ra cách viết mã dựa trên bản phát hành beta hiện có của họ.

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

Đối với bản ghi, phiên bản này thực hiện một "khởi động" thô:

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

}

Tổng thời gian cải thiện khoảng 30%, nhưng tỷ lệ giữa cả hai vẫn gần như giống nhau.


@TedHopp - Tôi đã thử thay đổi giới hạn vòng lặp trong của mình và về cơ bản nó vẫn không thay đổi.
Hot Licks

@ Techrocket9: Tôi nhận được những con số tương tự ( intnhanh hơn gấp 20 lần) với mã này.
tmyklebu

1

Cho các hồ sơ:

nếu tôi sử dụng

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

(đã thay đổi "l--" thành "l = l - 1l") hiệu suất dài cải thiện ~ 50%


0

Tôi không có máy 64 bit để kiểm tra, nhưng sự khác biệt khá lớn cho thấy rằng có nhiều hơn là bytecode dài hơn một chút ở nơi làm việc.

Tôi thấy thời gian rất gần cho long / int (4400 so với 4800ms) trên 1.7.0_45 32-bit của tôi.

Đây chỉ là một phỏng đoán , nhưng tôi mạnh mẽ nghi ngờ rằng đó là ảnh hưởng của một hình phạt nhớ không thẳng hàng. Để xác nhận / phủ nhận nghi ngờ, hãy thử thêm một public static int dummy = 0; trước khi khai báo i. Điều đó sẽ đẩy tôi xuống 4 byte trong bố cục bộ nhớ và có thể làm cho nó được căn chỉnh đúng cách để có hiệu suất tốt hơn. Được xác nhận là không gây ra sự cố.

BIÊN TẬP: Lý do đằng sau điều này là máy ảo có thể không sắp xếp lại các trường khi rảnh rỗi khi thêm đệm để căn chỉnh tối ưu, vì điều đó có thể gây trở ngại cho JNI (Không phải vậy).


Máy ảo chắc chắn được phép sắp xếp lại các trường và thêm phần đệm.
Hot Licks

JNI phải truy cập các đối tượng thông qua các phương thức truy cập chậm, phiền phức này, dù sao cũng có một vài xử lý mờ vì GC có thể xảy ra trong khi mã gốc đang chạy. Rất miễn phí để sắp xếp lại các trường và thêm phần đệm.
tmyklebu
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.