2008-10-28 24 views
91

Tôi có một câu hỏi liên quan đến hiệu suất liên quan đến việc sử dụng StringBuilder. Trong một vòng rất dài tôi đang thao tác một StringBuilder và đi qua nó để phương pháp khác như thế này:Có tốt hơn khi sử dụng lại một StringBuilder trong một vòng lặp không?

for (loop condition) { 
    StringBuilder sb = new StringBuilder(); 
    sb.append("some string"); 
    . . . 
    sb.append(anotherString); 
    . . . 
    passToMethod(sb.toString()); 
} 

là instantiating StringBuilder tại mỗi chu kỳ vòng lặp là một giải pháp tốt? Và đang kêu gọi xóa thay vì tốt hơn, như sau?

StringBuilder sb = new StringBuilder(); 
for (loop condition) { 
    sb.delete(0, sb.length); 
    sb.append("some string"); 
    . . . 
    sb.append(anotherString); 
    . . . 
    passToMethod(sb.toString()); 
} 

Trả lời

4

JVM hiện đại thực sự thông minh về những thứ như thế này. Tôi sẽ không đoán trước và làm điều gì đó hacky ít bảo trì/dễ đọc hơn ... trừ khi bạn làm các nhãn hiệu băng ghế phù hợp với dữ liệu sản xuất xác nhận sự cải thiện hiệu suất không tầm thường (và ghi lại nó;)

+0

Trường hợp "không tầm thường" là khóa - điểm chuẩn có thể hiển thị một biểu mẫu * tỷ lệ * nhanh hơn, nhưng không có gợi ý về thời gian thực hiện trong ứng dụng thực :) –

+0

Xem điểm chuẩn trong câu trả lời của tôi bên dưới. Cách thứ hai nhanh hơn. – Epaga

+1

@Epaga: Điểm chuẩn của bạn nói rất ít về cải thiện hiệu suất trong ứng dụng thực, nơi mà thời gian thực hiện để phân bổ StringBuilder có thể tầm thường so với phần còn lại của vòng lặp. Đó là lý do tại sao ngữ cảnh lại quan trọng trong điểm chuẩn. –

0

Alas I'm trên C# bình thường, tôi nghĩ rằng thanh toán bù trừ một trọng lượng StringBuilder nhiều hơn instanciating một cái mới.

25

Trong triết lý viết mã vững chắc, nó luôn luôn tốt hơn để đặt StringBuilder của bạn bên trong vòng lặp của bạn. Bằng cách này nó không đi bên ngoài mã của nó dành cho.

Thứ hai các improvment lớn nhất trong StringBuilder đến từ cho nó một kích thước ban đầu để tránh nó ngày càng lớn trong khi vòng lặp chạy

for (loop condition) { 
    StringBuilder sb = new StringBuilder(4096); 
} 
+1

Bạn luôn luôn có thể phạm vi toàn bộ điều với dấu ngoặc nhọn, theo cách đó bạn không có Stringbuilder bên ngoài. – Epaga

+0

@Epaga: Nó vẫn ở bên ngoài vòng lặp.Có, nó không gây ô nhiễm phạm vi bên ngoài, nhưng đó là một cách không tự nhiên để viết mã cho một cải tiến hiệu suất chưa được xác minh * trong ngữ cảnh *. –

+0

Hoặc thậm chí tốt hơn, đặt toàn bộ điều trong phương pháp riêng của mình. ;-) Nhưng tôi nghe ya lại: bối cảnh. – Epaga

4

Dựa trên kinh nghiệm của tôi với phát triển phần mềm trên Windows tôi sẽ nói xóa StringBuilder ra trong vòng lặp của bạn có hiệu suất tốt hơn so với việc tạo một StringBuilder với mỗi lần lặp. Xóa nó giải phóng bộ nhớ đó sẽ bị ghi đè ngay lập tức mà không yêu cầu phân bổ bổ sung. Tôi không quen thuộc với bộ thu gom rác Java, nhưng tôi nghĩ rằng việc giải phóng và không phân bổ lại (trừ khi chuỗi tiếp theo của bạn phát triển StringBuilder) có lợi hơn so với instantiation.

(Ý kiến ​​của tôi là trái với những gì mọi người khác được đề xuất. Hmm Time. Để benchmark nó.)

+0

Vấn đề là nhiều bộ nhớ hơn phải được phân bổ lại, vì dữ liệu hiện có đang được sử dụng bởi chuỗi mới được tạo ra ở cuối vòng lặp trước đó. –

+0

Oh điều đó có nghĩa, tôi đã mặc dù toString đã phân bổ và trả về một thể hiện chuỗi mới và bộ đệm byte cho trình tạo đã được xóa thay vì phân bổ lại. – cfeduke

+0

Điểm chuẩn của Epaga cho thấy rằng việc xóa và sử dụng lại là mức tăng trên quá trình khởi tạo ở mọi điểm. – cfeduke

65

Điều thứ hai là khoảng 25% nhanh hơn trong mini-benchmark của tôi.

public class ScratchPad { 

    static String a; 

    public static void main(String[] args) throws Exception { 
     long time = System.currentTimeMillis(); 
     for(int i = 0; i < 10000000; i++) { 
      StringBuilder sb = new StringBuilder(); 
      sb.append("someString"); 
      sb.append("someString2"+i); 
      sb.append("someStrin4g"+i); 
      sb.append("someStr5ing"+i); 
      sb.append("someSt7ring"+i); 
      a = sb.toString(); 
     } 
     System.out.println(System.currentTimeMillis()-time); 
     time = System.currentTimeMillis(); 
     StringBuilder sb = new StringBuilder(); 
     for(int i = 0; i < 10000000; i++) { 
      sb.delete(0, sb.length()); 
      sb.append("someString"); 
      sb.append("someString2"+i); 
      sb.append("someStrin4g"+i); 
      sb.append("someStr5ing"+i); 
      sb.append("someSt7ring"+i); 
      a = sb.toString(); 
     } 
     System.out.println(System.currentTimeMillis()-time); 
    } 
} 

Kết quả:

25265 
17969 

Lưu ý rằng đây là với JRE 1.6.0_07.


Dựa trên ý tưởng của Jon Skeet trong chỉnh sửa, đây là phiên bản 2. Kết quả tương tự.

public class ScratchPad { 

    static String a; 

    public static void main(String[] args) throws Exception { 
     long time = System.currentTimeMillis(); 
     StringBuilder sb = new StringBuilder(); 
     for(int i = 0; i < 10000000; i++) { 
      sb.delete(0, sb.length()); 
      sb.append("someString"); 
      sb.append("someString2"); 
      sb.append("someStrin4g"); 
      sb.append("someStr5ing"); 
      sb.append("someSt7ring"); 
      a = sb.toString(); 
     } 
     System.out.println(System.currentTimeMillis()-time); 
     time = System.currentTimeMillis(); 
     for(int i = 0; i < 10000000; i++) { 
      StringBuilder sb2 = new StringBuilder(); 
      sb2.append("someString"); 
      sb2.append("someString2"); 
      sb2.append("someStrin4g"); 
      sb2.append("someStr5ing"); 
      sb2.append("someSt7ring"); 
      a = sb2.toString(); 
     } 
     System.out.println(System.currentTimeMillis()-time); 
    } 
} 

Kết quả:

5016 
7516 
+4

Tôi đã thêm bản chỉnh sửa vào câu trả lời của mình để giải thích lý do tại sao điều này có thể xảy ra. Tôi sẽ xem xét cẩn thận hơn trong một thời gian (45 phút). Lưu ý rằng việc ghép nối các cuộc gọi nối thêm làm giảm điểm sử dụng StringBuilder ở vị trí đầu tiên một chút :) –

+3

Ngoài ra nó sẽ rất thú vị để xem điều gì sẽ xảy ra nếu bạn đảo ngược hai khối - JIT vẫn đang "khởi động" StringBuilder trong lần đầu tiên kiểm tra. Nó cũng có thể không liên quan, nhưng thú vị để thử. –

+1

Tôi vẫn đi với phiên bản đầu tiên vì nó * sạch hơn *. Nhưng tốt là bạn đã thực sự làm điểm chuẩn :) Thay đổi được đề xuất tiếp theo: hãy thử # 1 với dung lượng thích hợp được truyền vào hàm tạo. –

12

rồi, bây giờ tôi hiểu những gì đang xảy ra, và nó có ý nghĩa.

Tôi đã bị ấn tượng rằng toString chỉ cần vượt qua phần cơ bản char[] thành một công cụ xây dựng chuỗi mà không lấy một bản sao. Sau đó, bản sao sẽ được thực hiện trên thao tác "ghi" tiếp theo (ví dụ: delete). Tôi tin rằng đây là trường hợp có StringBuffer trong một số phiên bản trước đó. (Không phải bây giờ.) Nhưng không - toString chỉ cần vượt qua mảng (và chỉ mục và chiều dài) để công khai xây dựng String mà mất một bản sao.

Vì vậy, trong trường hợp "tái sử dụng StringBuilder", chúng tôi thực sự tạo một bản sao dữ liệu cho mỗi chuỗi, sử dụng cùng một mảng char trong bộ đệm toàn bộ thời gian. Rõ ràng tạo ra một StringBuilder mới mỗi lần tạo một bộ đệm cơ bản mới - và sau đó bộ đệm đó được sao chép (hơi vô nghĩa, trong trường hợp cụ thể của chúng ta, nhưng được thực hiện vì lý do an toàn) khi tạo một chuỗi mới.

Tất cả điều này dẫn đến phiên bản thứ hai chắc chắn hiệu quả hơn - nhưng đồng thời tôi vẫn cho rằng đó là mã xấu hơn.

+0

Chỉ cần một số thông tin vui về .NET, có tình huống khác. .NET StringBuilder nội bộ sửa đổi đối tượng "chuỗi" thông thường và phương thức toString chỉ trả về nó (đánh dấu nó là không thể sửa đổi, do đó các thao tác StringBuilder hậu quả sẽ tạo lại nó). Vì vậy, chuỗi "StringBuilder-> sửa đổi nó-> thành chuỗi" điển hình sẽ không tạo thêm bất kỳ bản sao nào (chỉ để mở rộng lưu trữ hoặc thu hẹp nó, nếu kết quả chuỗi dài hơn ngắn hơn dung lượng của nó). Trong Java, chu kỳ này luôn tạo ra ít nhất một bản sao (trong StringBuilder.toString()). –

+0

Sun JDK pre-1.5 đã tối ưu hóa bạn đã giả định: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6219959 –

-2

Khai báo một lần và chỉ định mỗi lần. Nó là một khái niệm thực dụng hơn và có thể tái sử dụng hơn là một sự tối ưu hóa.

0

Đầu tiên là tốt hơn cho con người. Nếu thứ hai nhanh hơn một chút trên một số phiên bản của một số JVM, vậy thì sao?

Nếu hiệu suất là quan trọng, bỏ qua StringBuilder và viết của riêng bạn. Nếu bạn là một lập trình viên giỏi và tính đến cách ứng dụng của bạn đang sử dụng chức năng này, bạn sẽ có thể làm cho nó nhanh hơn nữa. Đáng giá? Chắc là không.

Tại sao câu hỏi này được xem là "câu hỏi yêu thích"? Bởi vì tối ưu hóa hiệu suất là rất nhiều niềm vui, không có vấn đề cho dù đó là thực tế hay không.

+0

Nó không phải là một câu hỏi học thuật mà thôi. Trong khi hầu hết thời gian (đọc 95%) tôi thích khả năng đọc và bảo trì, có những trường hợp thực sự là những cải tiến nhỏ tạo nên sự khác biệt lớn ... –

+0

OK, tôi sẽ thay đổi câu trả lời của mình. Nếu một đối tượng cung cấp một phương thức cho phép nó được xóa và sử dụng lại, thì hãy làm như vậy. Kiểm tra mã đầu tiên nếu bạn muốn đảm bảo rõ ràng là hiệu quả; có lẽ nó phát hành một mảng riêng! Nếu hiệu quả, sau đó phân bổ các đối tượng bên ngoài vòng lặp và tái sử dụng nó bên trong. – dongilmore

9

Vì tôi không nghĩ rằng nó đã được chỉ ra, vì tối ưu hóa được tích hợp vào trình biên dịch Sun Java, tự động tạo StringBuilders (StringBuffers pre-J2SE 5.0) khi thấy chuỗi nối, ví dụ đầu tiên trong câu hỏi là tương đương với:

for (loop condition) { 
    String s = "some string"; 
    . . . 
    s += anotherString; 
    . . . 
    passToMethod(s); 
} 

Cách nào dễ đọc hơn, IMO, cách tiếp cận tốt hơn. Những nỗ lực của bạn để tối ưu hóa có thể dẫn đến lợi ích trong một số nền tảng, nhưng có khả năng mất mát những nền tảng khác.

Nhưng nếu bạn thực sự đang gặp sự cố với hiệu suất thì hãy chắc chắn, tối ưu hóa. Tôi bắt đầu với việc xác định rõ ràng kích thước bộ đệm của StringBuilder, mặc dù, theo Jon Skeet.

23

nhanh hơn vẫn:

public class ScratchPad { 

    private static String a; 

    public static void main(String[] args) throws Exception { 
     long time = System.currentTimeMillis(); 
     StringBuilder sb = new StringBuilder(128); 

     for(int i = 0; i < 10000000; i++) { 
      // Resetting the string is faster than creating a new object. 
      // Since this is a critical loop, every instruction counts. 
      // 
      sb.setLength(0); 
      sb.append("someString"); 
      sb.append("someString2"); 
      sb.append("someStrin4g"); 
      sb.append("someStr5ing"); 
      sb.append("someSt7ring"); 
      setA(sb.toString()); 
     } 

     System.out.println(System.currentTimeMillis()-time); 
    } 

    private static void setA(String aString) { 
     a = aString; 
    } 
} 

Trong triết lý của việc viết mã rắn, các hoạt động bên của phương pháp này nên được ẩn từ các đối tượng sử dụng phương pháp này. Do đó, nó không có sự khác biệt so với quan điểm của hệ thống cho dù bạn redeclare StringBuilder trong vòng lặp hoặc bên ngoài vòng lặp. Kể từ khi tuyên bố nó bên ngoài vòng lặp là nhanh hơn, và nó không làm cho mã phức tạp hơn để đọc, sau đó tái sử dụng các đối tượng hơn là reinstantiate nó.

Ngay cả khi mã phức tạp hơn và bạn biết chắc chắn rằng sự kiện đối tượng là nút cổ chai, hãy nhận xét nó.

Ba chạy với câu trả lời này:

$ java ScratchPad 
1567 
$ java ScratchPad 
1569 
$ java ScratchPad 
1570 

Ba chạy với câu trả lời khác:

$ java ScratchPad2 
1663 
2231 
$ java ScratchPad2 
1656 
2233 
$ java ScratchPad2 
1658 
2242 

Mặc dù không có ý nghĩa, thiết lập kích thước bộ đệm ban đầu của StringBuilder sẽ cung cấp một lợi nhỏ.

+2

Đây là câu trả lời hay nhất. StringBuilder được hỗ trợ bởi một mảng char và có thể thay đổi. Tại điểm .toString() được gọi, mảng char được sao chép và được sử dụng để trả về một chuỗi không thay đổi được. Tại thời điểm này, bộ đệm có thể thay đổi của StringBuilder có thể được tái sử dụng, đơn giản bằng cách di chuyển con trỏ chèn về 0 (thông qua .setLength (0)). Những câu trả lời gợi ý phân bổ một StringBuilder mới cho mỗi vòng lặp dường như không nhận ra rằng .toString tạo ra một bản sao khác, vì vậy mỗi lần lặp lại yêu cầu hai bộ đệm trái với phương thức .setLength (0) chỉ yêu cầu một bộ đệm mới trên mỗi vòng lặp. – Chris

1

Lý do tại sao thực hiện 'setLength' hoặc 'delete' cải thiện hiệu suất chủ yếu là mã 'học' kích thước phù hợp của bộ đệm và ít hơn để thực hiện cấp phát bộ nhớ. Nói chung, I recommend letting the compiler do the string optimizations. Tuy nhiên, nếu hiệu suất là rất quan trọng, tôi sẽ thường tính toán trước kích thước mong đợi của bộ đệm. Kích thước StringBuilder mặc định là 16 ký tự. Nếu bạn phát triển vượt ra khỏi đó, thì nó phải thay đổi kích thước. Thay đổi kích thước là nơi hiệu suất bị mất. Dưới đây là một điểm chuẩn nhỏ khác minh họa điều này:

private void clear() throws Exception { 
    long time = System.currentTimeMillis(); 
    int maxLength = 0; 
    StringBuilder sb = new StringBuilder(); 

    for(int i = 0; i < 10000000; i++) { 
     // Resetting the string is faster than creating a new object. 
     // Since this is a critical loop, every instruction counts. 
     // 
     sb.setLength(0); 
     sb.append("someString"); 
     sb.append("someString2").append(i); 
     sb.append("someStrin4g").append(i); 
     sb.append("someStr5ing").append(i); 
     sb.append("someSt7ring").append(i); 
     maxLength = Math.max(maxLength, sb.toString().length()); 
    } 

    System.out.println(maxLength); 
    System.out.println("Clear buffer: " + (System.currentTimeMillis()-time)); 
} 

private void preAllocate() throws Exception { 
    long time = System.currentTimeMillis(); 
    int maxLength = 0; 

    for(int i = 0; i < 10000000; i++) { 
     StringBuilder sb = new StringBuilder(82); 
     sb.append("someString"); 
     sb.append("someString2").append(i); 
     sb.append("someStrin4g").append(i); 
     sb.append("someStr5ing").append(i); 
     sb.append("someSt7ring").append(i); 
     maxLength = Math.max(maxLength, sb.toString().length()); 
    } 

    System.out.println(maxLength); 
    System.out.println("Pre allocate: " + (System.currentTimeMillis()-time)); 
} 

public void testBoth() throws Exception { 
    for(int i = 0; i < 5; i++) { 
     clear(); 
     preAllocate(); 
    } 
} 

Kết quả cho thấy việc sử dụng lại đối tượng nhanh hơn khoảng 10% so với kích thước mong muốn.

1

LOL, lần đầu tiên tôi thấy mọi người so sánh hiệu suất bằng cách kết hợp chuỗi trong StringBuilder. Với mục đích đó, nếu bạn sử dụng "+", nó có thể còn nhanh hơn; D. Mục đích của việc sử dụng StringBuilder để tăng tốc độ truy xuất toàn bộ chuỗi như khái niệm "địa phương".

Trong trường hợp bạn truy xuất một chuỗi giá trị thường xuyên mà không cần thay đổi thường xuyên, Stringbuilder cho phép hiệu năng thu hồi chuỗi cao hơn. Và đó là mục đích của việc sử dụng Stringbuilder .. xin vui lòng không MIS-Kiểm tra mục đích cốt lõi của điều đó ..

Một số người nói, Máy bay bay nhanh hơn. Vì vậy, tôi thử nghiệm nó với chiếc xe đạp của tôi, và thấy rằng máy bay di chuyển chậm hơn. Bạn có biết làm thế nào tôi thiết lập các thiết lập thử nghiệm, D

1

Không nhanh hơn đáng kể, nhưng từ các thử nghiệm của tôi nó cho thấy trung bình là một vài millis nhanh hơn bằng cách sử dụng 1.6.0_45 64 bit: sử dụng StringBuilder.setLength (0) thay vì StringBuilder.delete():

time = System.currentTimeMillis(); 
StringBuilder sb2 = new StringBuilder(); 
for (int i = 0; i < 10000000; i++) { 
    sb2.append("someString"); 
    sb2.append("someString2"+i); 
    sb2.append("someStrin4g"+i); 
    sb2.append("someStr5ing"+i); 
    sb2.append("someSt7ring"+i); 
    a = sb2.toString(); 
    sb2.setLength(0); 
} 
System.out.println(System.currentTimeMillis()-time); 
1

Cách nhanh nhất là sử dụng "setLength". Nó sẽ không liên quan đến hoạt động sao chép. Cách tạo StringBuilder mới phải hoàn toàn ra khỏi. Sự chậm chạp của StringBuilder.delete (int int, int end) là vì nó sẽ sao chép mảng một lần nữa cho phần thay đổi kích thước.

System.arraycopy(value, start+len, value, start, count-end); 

Sau đó, StringBuilder.delete() sẽ cập nhật StringBuilder.count thành kích thước mới. Trong khi StringBuilder.setLength() chỉ đơn giản hóa việc cập nhật StringBuilder.count thành kích thước mới.

Các vấn đề liên quan