Câu hỏi Tại sao StringBuilder # chắp thêm (int) nhanh hơn trong Java 7 so với Java 8?


Trong khi điều tra cho một ít tranh luận w.r.t. sử dụng "" + n và Integer.toString(int) để chuyển đổi một số nguyên nguyên thành một chuỗi tôi đã viết JMH microbenchmark:

@Fork(1)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class IntStr {
    protected int counter;


    @GenerateMicroBenchmark
    public String integerToString() {
        return Integer.toString(this.counter++);
    }

    @GenerateMicroBenchmark
    public String stringBuilder0() {
        return new StringBuilder().append(this.counter++).toString();
    }

    @GenerateMicroBenchmark
    public String stringBuilder1() {
        return new StringBuilder().append("").append(this.counter++).toString();
    }

    @GenerateMicroBenchmark
    public String stringBuilder2() {
        return new StringBuilder().append("").append(Integer.toString(this.counter++)).toString();
    }

    @GenerateMicroBenchmark
    public String stringFormat() {
        return String.format("%d", this.counter++);
    }

    @Setup(Level.Iteration)
    public void prepareIteration() {
        this.counter = 0;
    }
}

Tôi chạy nó với các tùy chọn JMH mặc định với cả hai máy ảo Java tồn tại trên máy Linux của tôi (cập nhật Mageia 4 64-bit, CPU Intel i7-3770, RAM 32GB). JVM đầu tiên là JVM được cung cấp với Oracle JDK 8u5 64 bit:

java version "1.8.0_05"
Java(TM) SE Runtime Environment (build 1.8.0_05-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.5-b02, mixed mode)

Với JVM này tôi đã nhận được khá nhiều những gì tôi mong đợi:

Benchmark                    Mode   Samples         Mean   Mean error    Units
b.IntStr.integerToString    thrpt        20    32317.048      698.703   ops/ms
b.IntStr.stringBuilder0     thrpt        20    28129.499      421.520   ops/ms
b.IntStr.stringBuilder1     thrpt        20    28106.692     1117.958   ops/ms
b.IntStr.stringBuilder2     thrpt        20    20066.939     1052.937   ops/ms
b.IntStr.stringFormat       thrpt        20     2346.452       37.422   ops/ms

I E. sử dụng StringBuilder lớp học chậm hơn do chi phí bổ sung của việc tạo StringBuilder đối tượng và gắn thêm một chuỗi rỗng. Sử dụng String.format(String, ...) thậm chí còn chậm hơn, theo thứ tự độ lớn.

Mặt khác, trình biên dịch được phân phối cung cấp dựa trên OpenJDK 1.7:

java version "1.7.0_55"
OpenJDK Runtime Environment (mageia-2.4.7.1.mga4-x86_64 u55-b13)
OpenJDK 64-Bit Server VM (build 24.51-b03, mixed mode)

Kết quả ở đây là hấp dẫn:

Benchmark                    Mode   Samples         Mean   Mean error    Units
b.IntStr.integerToString    thrpt        20    31249.306      881.125   ops/ms
b.IntStr.stringBuilder0     thrpt        20    39486.857      663.766   ops/ms
b.IntStr.stringBuilder1     thrpt        20    41072.058      484.353   ops/ms
b.IntStr.stringBuilder2     thrpt        20    20513.913      466.130   ops/ms
b.IntStr.stringFormat       thrpt        20     2068.471       44.964   ops/ms

Tại sao StringBuilder.append(int) xuất hiện nhanh hơn rất nhiều với JVM này? Hãy nhìn vào StringBuilder mã nguồn lớp tiết lộ không có gì đặc biệt thú vị - phương pháp được đề cập gần như giống hệt nhau Integer#toString(int). Điều thú vị là đủ, phụ thêm kết quả Integer.toString(int) (các stringBuilder2 microbenchmark) dường như không nhanh hơn.

Sự khác biệt về hiệu suất này có phải là vấn đề với việc khai thác thử nghiệm không? Hoặc OpenJDK JVM của tôi có tối ưu hóa sẽ ảnh hưởng đến mã đặc biệt này (chống) -pattern?

CHỈNH SỬA:

Để so sánh thẳng về phía trước, tôi đã cài đặt Oracle JDK 1.7u55:

java version "1.7.0_55"
Java(TM) SE Runtime Environment (build 1.7.0_55-b13)
Java HotSpot(TM) 64-Bit Server VM (build 24.55-b03, mixed mode)

Các kết quả tương tự như của OpenJDK:

Benchmark                    Mode   Samples         Mean   Mean error    Units
b.IntStr.integerToString    thrpt        20    32502.493      501.928   ops/ms
b.IntStr.stringBuilder0     thrpt        20    39592.174      428.967   ops/ms
b.IntStr.stringBuilder1     thrpt        20    40978.633      544.236   ops/ms

Có vẻ như đây là một vấn đề chung Java 7 so với Java 8. Có lẽ Java 7 đã tối ưu hóa chuỗi tích cực hơn?

CHỈNH SỬA 2:

Để có đầy đủ, dưới đây là các tùy chọn VM liên quan đến chuỗi cho cả hai JVM này:

Đối với Oracle JDK 8u5:

$ /usr/java/default/bin/java -XX:+PrintFlagsFinal 2>/dev/null | grep String
     bool OptimizeStringConcat                      = true            {C2 product}
     intx PerfMaxStringConstLength                  = 1024            {product}
     bool PrintStringTableStatistics                = false           {product}
    uintx StringTableSize                           = 60013           {product}

Đối với OpenJDK 1.7:

$ java -XX:+PrintFlagsFinal 2>/dev/null | grep String
     bool OptimizeStringConcat                      = true            {C2 product}        
     intx PerfMaxStringConstLength                  = 1024            {product}           
     bool PrintStringTableStatistics                = false           {product}           
    uintx StringTableSize                           = 60013           {product}           
     bool UseStringCache                            = false           {product}   

Các UseStringCache tùy chọn đã được gỡ bỏ trong Java 8 mà không cần thay thế, vì vậy tôi nghi ngờ rằng sẽ tạo ra bất kỳ sự khác biệt nào. Phần còn lại của các tùy chọn dường như có cùng cài đặt.

CHỈNH SỬA 3:

So sánh song song về mã nguồn của AbstractStringBuilder, StringBuilder và Integer các lớp học từ src.zip tập tin tiết lộ không có gì đáng chú ý. Ngoài nhiều thay đổi về mỹ phẩm và tài liệu, Integer bây giờ có một số hỗ trợ cho số nguyên không dấu và StringBuilder đã được tái cấu trúc một chút để chia sẻ nhiều mã hơn với StringBuffer. Không có thay đổi nào trong số này có vẻ ảnh hưởng đến các đường dẫn mã được sử dụng bởi StringBuilder#append(int), mặc dù tôi có thể đã bỏ lỡ điều gì đó.

So sánh mã lắp ráp được tạo cho IntStr#integerToString() và IntStr#stringBuilder0() thú vị hơn nhiều. Bố cục cơ bản của mã được tạo cho IntStr#integerToString() tương tự cho cả hai JVM, mặc dù Oracle JDK 8u5 dường như tích cực hơn với w.r.t. nhấn mạnh một số cuộc gọi trong Integer#toString(int) mã. Có một sự tương ứng rõ ràng với mã nguồn Java, ngay cả đối với một người có kinh nghiệm lắp ráp tối thiểu.

Mã lắp ráp cho IntStr#stringBuilder0()tuy nhiên, hoàn toàn khác nhau. Mã được tạo ra bởi Oracle JDK 8u5 lại một lần nữa liên quan trực tiếp đến mã nguồn Java - tôi có thể dễ dàng nhận ra cùng một bố cục. Ngược lại, mã được tạo ra bởi OpenJDK 7 gần như không thể nhận ra với mắt chưa được đào tạo (như của tôi). Các new StringBuilder()cuộc gọi dường như bị xóa, cũng như việc tạo mảng trong StringBuilder constructor. Ngoài ra, plugin disassembler không thể cung cấp nhiều tham chiếu đến mã nguồn giống như trong JDK 8.

Tôi cho rằng đây là kết quả của việc vượt qua tối ưu hóa tích cực hơn trong OpenJDK 7, hoặc nhiều hơn có lẽ là kết quả của việc chèn mã cấp thấp viết tay cho một số StringBuilder hoạt động. Tôi không chắc tại sao tối ưu hóa này không xảy ra trong triển khai JVM 8 của tôi hoặc tại sao các tối ưu hóa tương tự không được triển khai cho Integer#toString(int) trong JVM 7. Tôi đoán ai đó quen thuộc với các phần liên quan của mã nguồn JRE sẽ phải trả lời những câu hỏi này ...


76
2018-05-20 10:13


gốc


Ý bạn không phải là: new StringBuilder().append(this.counter++).toString(); và thử nghiệm thứ ba với return "" + this.counter++; ? - assylias
@assylias: The stringBuilder phương thức dịch thành chính xác cùng một bytecode như return "" + this.counter++;. Tôi sẽ xem thêm về việc thêm thử nghiệm thứ ba mà không cần thêm chuỗi trống ... - thkala
@assylias: có bạn đi. Không có sự khác biệt thực sự nào mà tôi có thể thấy ... - thkala
bạn có thể thêm một thử nghiệm cho String.format("%d",n); cũng - feeling unwelcome
@JarrodRoberson: làm thế nào về điều này? String.format("%d",n) là về thứ tự cường độ chậm hơn mọi thứ ... - thkala


Các câu trả lời:


TL; DR: Tác dụng phụ trong append dường như phá vỡ tối ưu hóa StringConcat.

Phân tích rất tốt trong câu hỏi và cập nhật ban đầu!

Để hoàn thành, dưới đây là một vài bước mất tích:

  • Xem qua -XX:+PrintInlining cho cả 7u55 và 8u5. Trong 7u55, bạn sẽ thấy một cái gì đó như thế này:

     @ 16   org.sample.IntStr::inlineSideEffect (25 bytes)   force inline by CompilerOracle
       @ 4   java.lang.StringBuilder::<init> (7 bytes)   inline (hot)
       @ 18   java.lang.StringBuilder::append (8 bytes)   already compiled into a big method
       @ 21   java.lang.StringBuilder::toString (17 bytes)   inline (hot)
    

    ... và trong 8u5:

     @ 16   org.sample.IntStr::inlineSideEffect (25 bytes)   force inline by CompilerOracle
       @ 4   java.lang.StringBuilder::<init> (7 bytes)   inline (hot)
         @ 3   java.lang.AbstractStringBuilder::<init> (12 bytes)   inline (hot)
           @ 1   java.lang.Object::<init> (1 bytes)   inline (hot)
       @ 18   java.lang.StringBuilder::append (8 bytes)   inline (hot)
         @ 2   java.lang.AbstractStringBuilder::append (62 bytes)   already compiled into a big method
       @ 21   java.lang.StringBuilder::toString (17 bytes)   inline (hot)
         @ 13   java.lang.String::<init> (62 bytes)   inline (hot)
           @ 1   java.lang.Object::<init> (1 bytes)   inline (hot)
           @ 55   java.util.Arrays::copyOfRange (63 bytes)   inline (hot)
             @ 54   java.lang.Math::min (11 bytes)   (intrinsic)
             @ 57   java.lang.System::arraycopy (0 bytes)   (intrinsic)
    

    Bạn có thể nhận thấy rằng phiên bản 7u55 sẽ cạn hơn và có vẻ như không có gì được gọi sau StringBuilder phương pháp - đây là một dấu hiệu tốt cho phép tối ưu hóa chuỗi có hiệu lực. Thật vậy, nếu bạn chạy 7u55 với -XX:-OptimizeStringConcat, các subcalls sẽ xuất hiện trở lại, và hiệu suất giảm xuống mức 8u5.

  • OK, vì vậy chúng tôi cần tìm ra lý do tại sao 8u5 không thực hiện tối ưu hóa tương tự. Grep http://hg.openjdk.java.net/jdk9/jdk9/hotspot cho "StringBuilder" để tìm ra nơi VM xử lý tối ưu hóa StringConcat; điều này sẽ đưa bạn vào src/share/vm/opto/stringopts.cpp

  • hg log src/share/vm/opto/stringopts.cpp để tìm ra những thay đổi mới nhất ở đó. Một trong những ứng cử viên sẽ là:

    changeset:   5493:90abdd727e64
    user:        iveresov
    date:        Wed Oct 16 11:13:15 2013 -0700
    summary:     8009303: Tiered: incorrect results in VM tests stringconcat...
    
  • Tìm kiếm các chủ đề đánh giá trên danh sách gửi thư OpenJDK (dễ dàng đủ để google cho tóm tắt changeset): http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2013-October/012084.html

  • Spot "String concat tối ưu hóa tối ưu hóa sụp đổ các mô hình [...] vào một phân bổ duy nhất của một chuỗi và tạo thành kết quả trực tiếp. Tất cả các deopts có thể xảy ra trong mã tối ưu khởi động lại mô hình này từ đầu (bắt đầu từ việc phân bổ StringBuffer) . Điều đó có nghĩa là toàn bộ khuôn mẫu phải cho tôi tác dụng phụ miễn phí."Eureka?

  • Viết ra các tiêu chuẩn tương phản:

    @Fork(5)
    @Warmup(iterations = 5)
    @Measurement(iterations = 5)
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    @State(Scope.Benchmark)
    public class IntStr {
        private int counter;
    
        @GenerateMicroBenchmark
        public String inlineSideEffect() {
            return new StringBuilder().append(counter++).toString();
        }
    
        @GenerateMicroBenchmark
        public String spliceSideEffect() {
            int cnt = counter++;
            return new StringBuilder().append(cnt).toString();
        }
    }
    
  • Đo nó trên JDK 7u55, nhìn thấy hiệu suất tương tự cho các hiệu ứng phụ được chèn vào / ghép:

    Benchmark                       Mode   Samples         Mean   Mean error    Units
    o.s.IntStr.inlineSideEffect     avgt        25       65.460        1.747    ns/op
    o.s.IntStr.spliceSideEffect     avgt        25       64.414        1.323    ns/op
    
  • Đo nó trên JDK 8u5, thấy sự suy giảm hiệu suất với hiệu ứng nội tuyến:

    Benchmark                       Mode   Samples         Mean   Mean error    Units
    o.s.IntStr.inlineSideEffect     avgt        25       84.953        2.274    ns/op
    o.s.IntStr.spliceSideEffect     avgt        25       65.386        1.194    ns/op
    
  • Gửi báo cáo lỗi (https://bugs.openjdk.java.net/browse/JDK-8043677) để thảo luận về hành vi này với các máy ảo. Lý do để sửa chữa ban đầu là đá rắn, nó là thú vị tuy nhiên nếu chúng ta có thể / nên lấy lại tối ưu hóa này trong một số trường hợp tầm thường như thế này.

  • ???

  • LỢI NHUẬN.

Và vâng, tôi nên đăng kết quả cho điểm chuẩn mà di chuyển số gia tăng từ StringBuilder chuỗi, làm điều đó trước khi toàn bộ chuỗi. Ngoài ra, chuyển sang thời gian trung bình và ns / op. Đây là JDK 7u55:

Benchmark                      Mode   Samples         Mean   Mean error    Units
o.s.IntStr.integerToString     avgt        25      153.805        1.093    ns/op
o.s.IntStr.stringBuilder0      avgt        25      128.284        6.797    ns/op
o.s.IntStr.stringBuilder1      avgt        25      131.524        3.116    ns/op
o.s.IntStr.stringBuilder2      avgt        25      254.384        9.204    ns/op
o.s.IntStr.stringFormat        avgt        25     2302.501      103.032    ns/op

Và đây là 8u5:

Benchmark                      Mode   Samples         Mean   Mean error    Units
o.s.IntStr.integerToString     avgt        25      153.032        3.295    ns/op
o.s.IntStr.stringBuilder0      avgt        25      127.796        1.158    ns/op
o.s.IntStr.stringBuilder1      avgt        25      131.585        1.137    ns/op
o.s.IntStr.stringBuilder2      avgt        25      250.980        2.773    ns/op
o.s.IntStr.stringFormat        avgt        25     2123.706       25.105    ns/op

stringFormatthực sự nhanh hơn một chút trong 8u5, và tất cả các thử nghiệm khác đều giống nhau. Điều này củng cố giả thuyết rằng sự phá vỡ tác dụng phụ trong chuỗi SB trong thủ phạm chính trong câu hỏi ban đầu.


94
2018-05-21 19:23



Hoàn thành rất tốt! Đây là một ít bi nhỏ ... err ... vấn đề - không hoàn toàn là những gì hầu hết các lập trình viên Java được sử dụng để mong đợi. Tôi đã tìm thấy một vài tài liệu tham khảo w.r.t. tối ưu hóa chuỗi có vấn đề chính xác, vì vậy tôi đã có những nghi ngờ của tôi, nhưng tôi không có thời gian để ghim nó xuống. Tôi cũng đánh giá cao các báo cáo lỗi, ngay cả khi nó được hư không. - thkala
Ồ, tôi cũng đã xác nhận những phát hiện của bạn bằng cách di chuyển tăng số lượt truy cập trước StringBuilder cuộc gọi và điểm chuẩn. Tôi tự hỏi những loại đá quý nhỏ khác thuộc loại này có thể là ... - thkala


Tôi nghĩ điều này liên quan đến CompileThreshold cờ điều khiển khi mã byte được biên dịch thành mã máy bởi JIT.

Oracle JDK có số lượng mặc định là 10.000 tài liệu tại http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html.

Ở đâu OpenJDK tôi không thể tìm thấy một tài liệu mới nhất trên lá cờ này; nhưng một số luồng thư đề xuất ngưỡng thấp hơn nhiều: http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2010-November/004239.html

Ngoài ra, hãy thử bật / tắt cờ Oracle JDK như -XX:+UseCompressedStrings và -XX:+OptimizeStringConcat. Tôi không chắc chắn nếu những lá cờ được bật theo mặc định trên OpenJDK mặc dù. Ai đó có thể đề nghị.

Một thử nghiệm bạn có thể làm, trước tiên là chạy chương trình bằng nhiều lần, ví dụ: 30.000 vòng, thực hiện System.gc () và sau đó thử xem hiệu suất. Tôi tin rằng họ sẽ mang lại như vậy.

Và tôi cho rằng thiết lập GC của bạn cũng vậy. Nếu không, bạn đang phân bổ rất nhiều đối tượng và GC cũng có thể là phần chính trong thời gian chạy của bạn.


5
2018-05-20 10:36



JMH thực hiện 20 lần lặp khởi động theo mặc định, mỗi lần chứa một số triệu các cuộc gọi cho các phương pháp microbenchmark trong trường hợp này. Về mặt lý thuyết  CompileThreshold không nên có nhiều ảnh hưởng ... - thkala
@thkala Tôi tự hỏi kết quả là gì nếu OP thử với khởi động ở đây. Nhưng tôi đồng ý với bạn rằng mã của anh ấy quá đơn giản đối với một phòng cải tiến lớn. Ngoài ra, một số JDK thay thế mã hiệu suất cốt lõi chung, tức là các mã có hoạt động chuỗi bằng mã gốc. Tuy nhiên, không chắc lắm về việc triển khai OpenJDK. - Alex Suo
Xin lỗi vừa nhận ra bạn là OP :) - Alex Suo
Có vẻ như đây là vấn đề Java7 / Java8 nhiều hơn là một OpenJDK / HotSpot - tôi đã thêm một điểm chuẩn trên Oracle JDK 7u55 ... - thkala
@AlexSuo jmh đã làm hầu hết những gì bạn đề xuất. - assylias