Tại sao StringBuilder # append (int) trong Java 7 nhanh hơn trong Java 8?


76

Trong khi điều tra một cuộc tranh luận nhỏ về việc sử dụng "" + nInteger.toString(int)chuyển đổi một số nguyên nguyên thủy thành một chuỗi, tôi đã viết microbenchmark JMH này :

@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ả máy ảo Java tồn tại trên máy Linux của tôi (Mageia 4 64-bit cập nhật, CPU Intel i7-3770, RAM 32 GB). 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 thứ mà 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

Tức là sử dụng StringBuilderlớp chậm hơn do chi phí tạo StringBuilderđối tượng và nối thêm một chuỗi trống. Việc sử dụng String.format(String, ...)thậm chí còn chậm hơn, theo thứ tự độ lớn hoặc hơn.

Mặt khác, trình biên dịch do 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 thật thú vị :

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 lại StringBuilder.append(int)xuất hiện nhanh hơn nhiều với JVM này? Nhìn vào StringBuildermã nguồn của lớp không có gì đặc biệt thú vị - phương thức được đề cập gần như giống hệt Integer#toString(int). Điều thú vị là, việc bổ sung kết quả của Integer.toString(int)( stringBuilder2microbenchmark) dường như không nhanh hơn.

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

BIÊN TẬP:

Để so sánh rõ hơn, 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)

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à vấn đề Java 7 so với Java 8 chung chung hơn. Có lẽ Java 7 đã tối ưu hóa chuỗi tích cực hơn?

CHỈNH SỬA 2 :

Để hoàn thiện, đây là các tùy chọn máy ảo 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 UseStringCachetùy chọn được loại bỏ trong Java 8 không có thay thế, vì vậy tôi nghi ngờ mà làm cho bất kỳ sự khác biệt. 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:

Một so sánh side-by-side của mã nguồn của AbstractStringBuilder, StringBuilderIntegercác lớp học từ src.ziptập tin của tiết lộ gì noteworty. Ngoài rất nhiều thay đổi về thẩm mỹ và tài liệu, Integergiờ đây đã có một số hỗ trợ cho các số nguyên không dấu và StringBuilderđã được cấu trúc lại một chút để chia sẻ nhiều mã hơn StringBuffer. Không có thay đổi nào trong số này dường như ảnh hưởng đến các đường dẫn mã được sử dụng 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()IntStr#stringBuilder0()thú vị hơn nhiều. Bố cục cơ bản của mã được tạo cho IntStr#integerToString()là tương tự cho cả hai JVM, mặc dù Oracle JDK 8u5 có vẻ tích cực hơn trong nội tuyến một số lệnh 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.

IntStr#stringBuilder0()Tuy nhiên, mã lắp ráp cho hoàn toàn khác nhau. Mã được tạo bởi Oracle JDK 8u5 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ã do OpenJDK 7 tạo ra hầu như không thể nhận ra đối với mắt chưa qua đào tạo (như của tôi). Lời new StringBuilder()gọi dường như đã bị loại bỏ, cũng như việc tạo mảng trong hàm StringBuildertạo. Thêm vào đó, plugin trình tháo gỡ không thể cung cấp nhiều tham chiếu đến mã nguồn như trong JDK 8.

Tôi giả định rằng đây là kết quả của một lần vượt qua tối ưu hóa tích cực hơn nhiều trong OpenJDK 7, hoặc nhiều hơn có thể là kết quả của việc chèn mã cấp thấp viết tay cho các StringBuilderhoạt động nhất định . Tôi không chắc tại sao tối ưu hóa này không xảy ra trong quá trình 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 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 ...


Ý 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

4
@assylias: stringBuilderPhương thức này chuyển thành bytecode giống hệt như return "" + this.counter++;. Tôi sẽ xem về cách thêm thử nghiệm thứ ba mà không nối chuỗi trống ...
thkala

@assylias: bạn hiểu rồi. Không có sự khác biệt thực sự mà tôi có thể thấy ...
thkala

bạn có thể thêm một bài kiểm tra String.format("%d",n);nữa không

1
@JarrodRoberson: còn cái này thì sao? String.format("%d",n)chậm hơn mọi thứ về độ lớn ...
thkala

Câu trả lời:


97

TL; DR: Tác dụng phụ appendrõ ràng là phá vỡ tối ưu hóa StringConcat.

Phân tích rất tốt trong câu hỏi gốc và các bản cập nhật!

Để hoàn thiện, dưới đây là một số bước còn thiếu:

  • Xem qua -XX:+PrintInliningcho 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 nông hơn và có vẻ như không có gì được gọi sau StringBuilder các phương thức - đây là một dấu hiệu tốt cho thấy việc tối ưu hóa chuỗi đang có hiệu lực. Thật vậy, nếu bạn chạy 7u55 với -XX:-OptimizeStringConcat, các cuộc gọi con sẽ xuất hiện lại và hiệu suất giảm xuống mức 8u5.

  • OK, vì vậy chúng ta 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àosrc/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 các chủ đề đánh giá trên danh sách gửi thư OpenJDK (đủ dễ dàng để google để biết tóm tắt tập thay đổi): http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2013-October/012084.html

  • Spot "Tối ưu hóa kết nối chuỗi sẽ thu gọn mẫu [...] thành 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 lỗi có thể xảy ra trong mã được tối ưu hóa hãy khởi động lại mẫu này từ đầu (bắt đầu từ phân bổ StringBuffer) . Điều đó có nghĩa là toàn bộ mô hình phải cho tôi không có tác dụng phụ. "Eureka?

  • Viết ra điểm 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, thấy hiệu suất tương tự đối với các tác dụng phụ nội tuyến / nối:

    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, xem 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 VM guys. Cơ sở lý luận cho bản sửa lỗi ban đầu là rất chắc chắn, tuy nhiên, thật thú vị nếu chúng ta có thể / nên lấy lại sự tối ưu hóa này trong một số trường hợp nhỏ nhặt như thế này.

  • ???

  • LỢI NHUẬN.

Và đúng vậy, tôi nên đăng kết quả cho điểm chuẩn để di chuyển mức tăng khỏi StringBuilderchuỗi, thực hiện trước 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 về sự phá vỡ tác dụng phụ trong chuỗi SB là thủ phạm chính trong câu hỏi ban đầu.


1
Hoàn thành rất tốt! Đây là một vấn đề nhỏ tinh tế - không hoàn toàn là điều mà hầu hết các lập trình viên Java thường mong đợi. Tôi đã tìm thấy một số tối ưu hóa chuỗi wrt tham chiếu có vấn đề về tính đúng đắn, vì vậy tôi đã nghi ngờ, nhưng tôi không có thời gian để ghim nó xuống. Tôi cũng đánh giá cao báo cáo lỗi, ngay cả khi nó không đi đến đâu.
thkala

1
Ồ, tôi cũng đã xác nhận phát hiện của bạn bằng cách di chuyển số tăng bộ đếm trước StringBuildercuộc gọi và điểm chuẩn. Tôi tự hỏi những viên ngọc nhỏ khác thuộc loại này có thể có ...
thkala

5

Tôi nghĩ rằng điều này phải làm với CompileThresholdcờ kiểm soát 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 dưới dạng tài liệu tại http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html .

Ở nơi OpenJDK, tôi không thể tìm thấy tài liệu mới nhất về cờ này; nhưng một số chuỗi 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ác cờ Oracle JDK như -XX:+UseCompressedStrings-XX:+OptimizeStringConcat. Tôi không chắc liệu những cờ đó có được bật theo mặc định trên OpenJDK hay không. Ai đó có thể vui lòng đề nghị.

Một thử nghiệm bạn có thể làm, trước tiên là chạy chương trình nhiều lần, chẳng hạn như 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ẽ đạt được như nhau.

Và tôi cho rằng cài đặt 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ó thể là phần chính trong thời gian chạy của bạn.


6
JMH thực hiện 20 lần lặp khởi động theo mặc định, mỗi lần lặp lại chứa vài triệu lệnh gọi cho các phương thức microbenchmark trong trường hợp này. Về mặt lý thuyết CompileThreshold không có nhiều ảnh hưởng ...
thkala

@thkala Tôi đang tự hỏi kết quả là gì nếu OP thử 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 lượng lớn cải tiến. Ngoài ra, một số JDK thay thế mã hiệu suất cốt lõi chung, tức là những mã có hoạt động chuỗi bằng mã gốc. Tuy nhiên, không chắc chắn lắm về việc triển khai OpenJDK.
Alex Suo

Xin lỗi chỉ nhận ra bạn là OP :)
Alex Suo

Có vẻ như đây là vấn đề Java7 / Java8 hơn là vấn đề OpenJDK / HotSpot - Tôi đã thêm điểm chuẩn trên Oracle JDK 7u55 ...
thkala

Có vẻ như các tùy chọn VM liên quan đến chuỗi đều giống nhau trên cả hai phiên bản. Điều đó nói rằng, Java 8 có một cơ chế GC khác ...
thkala
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.