Cách nhanh nhất để lặp lại tất cả các ký tự trong Chuỗi


163

Trong Java, cách nhanh nhất để lặp lại trên tất cả các ký tự trong Chuỗi, đây là:

String str = "a really, really long string";
for (int i = 0, n = str.length(); i < n; i++) {
    char c = str.charAt(i);
}

Hoặc này:

char[] chars = str.toCharArray();
for (int i = 0, n = chars.length; i < n; i++) {
    char c = chars[i];
}

BIÊN TẬP :

Điều tôi muốn biết là nếu chi phí liên tục gọi charAtphương thức trong một lần lặp dài kết thúc sẽ nhỏ hơn hoặc lớn hơn chi phí thực hiện một cuộc gọi đến toCharArraylúc bắt đầu và sau đó truy cập trực tiếp vào mảng trong quá trình lặp.

Thật tuyệt nếu ai đó có thể cung cấp một điểm chuẩn mạnh mẽ cho các độ dài chuỗi khác nhau, lưu ý đến thời gian khởi động JIT, thời gian khởi động JVM, v.v. và không chỉ là sự khác biệt giữa hai cuộc gọi đến System.currentTimeMillis().


18
Chuyện gì đã xảy ra for (char c : chars)?
dasblinkenlight

Về mặt lý thuyết, cái đầu tiên phải nhanh hơn và dù sao cũng là một chuỗi char.
Keagan Ladds

Google thường là một tài nguyên tốt: mkyong.com/java/ khăn
Johan Sjöberg

2
Câu hỏi không yêu cầu hiệu suất của việc sử dụng các trình vòng lặp, foreach. Điều tôi muốn biết là nếu chi phí gọi điện liên tục charAtkết thúc bằng hoặc ít hơn chi phí thực hiện một cuộc gọi đếntoCharArray
Óscar López

1
Có ai đã thực hiện phân tích với StringCharacterIterator chưa?
bdrx

Câu trả lời:


352

CẬP NHẬT ĐẦU TIÊN: Trước khi bạn thử điều này trong môi trường sản xuất (không nên dùng), hãy đọc phần này trước: http://www.javaspecialists.eu/archive/Issue237.html Bắt đầu từ Java 9, giải pháp như được mô tả sẽ không còn hiệu quả nữa , bởi vì bây giờ Java sẽ lưu trữ các chuỗi dưới dạng byte [] theo mặc định.

CẬP NHẬT THỨ HAI: Kể từ 2016-10-25, trên AMDx64 8core và nguồn 1.8 của tôi, không có sự khác biệt giữa việc sử dụng 'charAt' và truy cập trường. Dường như jvm được tối ưu hóa đủ để nội tuyến và hợp lý hóa mọi cuộc gọi 'string.charAt (n)'.

Tất cả phụ thuộc vào độ dài của việc Stringđược kiểm tra. Nếu, như câu hỏi nói, đó là đối với các chuỗi dài , cách nhanh nhất để kiểm tra chuỗi là sử dụng sự phản chiếu để truy cập vào mặt sau char[]của chuỗi.

Một điểm chuẩn ngẫu nhiên hoàn toàn với JDK 8 (win32 và win64) trên lõi 64 AMD Phenom II 4 955 @ 3.2 GHZ (ở cả chế độ máy khách và chế độ máy chủ) với 9 kỹ thuật khác nhau (xem bên dưới!) Cho thấy sử dụng String.charAt(n)là nhanh nhất các chuỗi và việc sử dụng reflectionđể truy cập mảng sao lưu Chuỗi nhanh hơn gần gấp đôi đối với các chuỗi lớn.

CUỘC THÍ NGHIỆM

  • 9 kỹ thuật tối ưu hóa khác nhau được thử.

  • Tất cả nội dung chuỗi được chọn ngẫu nhiên

  • Thử nghiệm được thực hiện cho kích thước chuỗi theo bội số của hai bắt đầu bằng 0,1,2,4,8,16, v.v.

  • Các bài kiểm tra được thực hiện 1.000 lần cho mỗi kích thước chuỗi

  • Các bài kiểm tra được xáo trộn theo thứ tự ngẫu nhiên mỗi lần. Nói cách khác, các bài kiểm tra được thực hiện theo thứ tự ngẫu nhiên mỗi khi chúng được thực hiện, hơn 1000 lần.

  • Toàn bộ bộ kiểm thử được thực hiện chuyển tiếp và ngược lại, để hiển thị hiệu quả của việc khởi động JVM đối với tối ưu hóa và thời gian.

  • Toàn bộ bộ được thực hiện hai lần, một lần trong -clientchế độ và khác trong -serverchế độ.

KẾT LUẬN

chế độ -client (32 bit)

Đối với các chuỗi có độ dài từ 1 đến 256 ký tự , gọi string.charAt(i)chiến thắng với mức xử lý trung bình từ 13,4 triệu đến 588 triệu ký tự mỗi giây.

Ngoài ra, nó nhanh hơn 5,5% (máy khách) và 13,9% (máy chủ) như thế này:

    for (int i = 0; i < data.length(); i++) {
        if (data.charAt(i) <= ' ') {
            doThrow();
        }
    }

hơn như thế này với một biến độ dài cuối cùng cục bộ:

    final int len = data.length();
    for (int i = 0; i < len; i++) {
        if (data.charAt(i) <= ' ') {
            doThrow();
        }
    }

Đối với các chuỗi dài , độ dài 512 đến 256K ký tự , sử dụng phản xạ để truy cập mảng sao lưu của Chuỗi là nhanh nhất. Kỹ thuật này nhanh gần gấp đôi so với String.charAt (i) (nhanh hơn 178%). Tốc độ trung bình trên phạm vi này là 1,11 tỷ ký tự mỗi giây.

Trường phải được lấy trước thời hạn và sau đó nó có thể được sử dụng lại trong thư viện trên các chuỗi khác nhau. Thật thú vị, không giống như mã ở trên, với quyền truy cập Trường, có một biến có độ dài cuối cùng cục bộ nhanh hơn 9% so với sử dụng 'chars.length' trong kiểm tra vòng lặp. Đây là cách truy cập Trường có thể được thiết lập nhanh nhất:

   final Field field = String.class.getDeclaredField("value");
   field.setAccessible(true);

   try {
       final char[] chars = (char[]) field.get(data);
       final int len = chars.length;
       for (int i = 0; i < len; i++) {
           if (chars[i] <= ' ') {
               doThrow();
           }
       }
       return len;
   } catch (Exception ex) {
       throw new RuntimeException(ex);
   }

Nhận xét đặc biệt về chế độ máy chủ

Truy cập trường bắt đầu chiến thắng sau chuỗi dài 32 ký tự ở chế độ máy chủ trên máy Java 64 bit trên máy AMD 64 của tôi. Điều đó đã không được nhìn thấy cho đến khi độ dài 512 ký tự trong chế độ máy khách.

Tôi cũng đáng chú ý, khi tôi đang chạy JDK 8 (bản dựng 32 bit) ở chế độ máy chủ, hiệu suất tổng thể chậm hơn 7% cho cả chuỗi lớn và nhỏ. Điều này là với bản dựng 121 tháng 12 năm 2013 của JDK 8 phát hành sớm. Vì vậy, hiện tại, có vẻ như chế độ máy chủ 32 bit chậm hơn chế độ máy khách 32 bit.

Điều đó đang được nói ... có vẻ như chế độ máy chủ duy nhất đáng gọi là trên máy 64 bit. Nếu không, nó thực sự cản trở hiệu suất.

Đối với bản dựng 32 bit chạy -server modetrên AMD64, tôi có thể nói điều này:

  1. String.charAt (i) là người chiến thắng rõ ràng tổng thể. Mặc dù trong khoảng từ 8 đến 512 ký tự, đã có người chiến thắng trong số 'mới' 'tái sử dụng' và 'trường'.
  2. String.charAt (i) nhanh hơn 45% trong chế độ máy khách
  3. Truy cập trường nhanh gấp đôi đối với các Chuỗi lớn trong chế độ máy khách.

Cũng đáng nói, String.chars () (Luồng và phiên bản song song) là một bức tượng bán thân. Cách chậm hơn bất kỳ cách nào khác. Các StreamsAPI là một cách khá chậm chạp trong việc thực hiện các hoạt động chuỗi chung.

Danh sách mong muốn

Chuỗi Java có thể có các vị từ chấp nhận các phương thức được tối ưu hóa như chứa (vị ngữ), forEach (người tiêu dùng), forEachWithIndex (người tiêu dùng). Do đó, không cần người dùng biết độ dài hoặc lặp lại các cuộc gọi đến các phương thức String, chúng có thể giúp phân tích cú pháp các thư việnbeep-beep beep tốc độ .

Hãy cứ mơ ước :)

Chúc mừng chuỗi!

~ SH

Thử nghiệm đã sử dụng 9 phương pháp kiểm tra chuỗi sau đây cho sự hiện diện của khoảng trắng:

"charAt1" - KIỂM TRA NỘI DUNG CHIẾN LƯỢC CÁCH SỬ DỤNG:

int charAtMethod1(final String data) {
    final int len = data.length();
    for (int i = 0; i < len; i++) {
        if (data.charAt(i) <= ' ') {
            doThrow();
        }
    }
    return len;
}

"charAt2" - CÙNG NHƯ VẬY NHƯNG SỬ DỤNG String.length () CÀI ĐẶT TẠO MỘT ĐỊA ĐIỂM CUỐI CÙNG CHO LENGTh

int charAtMethod2(final String data) {
    for (int i = 0; i < data.length(); i++) {
        if (data.charAt(i) <= ' ') {
            doThrow();
        }
    }
    return data.length();
}

"stream" - SỬ DỤNG IntStream MỚI của JAVA-8 String VÀ PASS IT MỘT PREDICATE ĐỂ KIỂM TRA

int streamMethod(final String data, final IntPredicate predicate) {
    if (data.chars().anyMatch(predicate)) {
        doThrow();
    }
    return data.length();
}

"StreamPara" - CÙNG NHƯ VẬY, NHƯNG OH-LA-LA - ĐI PARALLEL !!!

// avoid this at all costs
int streamParallelMethod(final String data, IntPredicate predicate) {
    if (data.chars().parallel().anyMatch(predicate)) {
        doThrow();
    }
    return data.length();
}

"tái sử dụng" - HOÀN LẠI một char REUSABLE [] VỚI NỘI DUNG CHIẾN LƯỢC

int reuseBuffMethod(final char[] reusable, final String data) {
    final int len = data.length();
    data.getChars(0, len, reusable, 0);
    for (int i = 0; i < len; i++) {
        if (reusable[i] <= ' ') {
            doThrow();
        }
    }
    return len;
}

"new1" - OBTAIN MỘT BẢN SAO MỚI CỦA char [] TỪ CHUINGI

int newMethod1(final String data) {
    final int len = data.length();
    final char[] copy = data.toCharArray();
    for (int i = 0; i < len; i++) {
        if (copy[i] <= ' ') {
            doThrow();
        }
    }
    return len;
}

"new2" - CÙNG NHƯ TRÊN, NHƯNG SỬ DỤNG "FOR-EACH"

int newMethod2(final String data) {
    for (final char c : data.toCharArray()) {
        if (c <= ' ') {
            doThrow();
        }
    }
    return data.length();
}

"trường1" - FANCY !! L FINH VỰC OBTAIN ĐỂ TIẾP CẬN VỚI char NỘI BỘ CỦA STRING []

int fieldMethod1(final Field field, final String data) {
    try {
        final char[] chars = (char[]) field.get(data);
        final int len = chars.length;
        for (int i = 0; i < len; i++) {
            if (chars[i] <= ' ') {
                doThrow();
            }
        }
        return len;
    } catch (Exception ex) {
        throw new RuntimeException(ex);
    }
}

"field2" - CÙNG NHƯ TRÊN, NHƯNG SỬ DỤNG "FOR-EACH"

int fieldMethod2(final Field field, final String data) {
    final char[] chars;
    try {
        chars = (char[]) field.get(data);
    } catch (Exception ex) {
        throw new RuntimeException(ex);
    }
    for (final char c : chars) {
        if (c <= ' ') {
            doThrow();
        }
    }
    return chars.length;
}

KẾT QUẢ COMPOSITE CHO KHÁCH HÀNG -client CHẾ ĐỘ (kết hợp kiểm tra tiến và lùi)

Lưu ý: chế độ -client với Java 32 bit và chế độ máy chủ với Java 64 bit giống như bên dưới trên máy AMD64 của tôi.

Size     WINNER  charAt1 charAt2  stream streamPar   reuse    new1    new2  field1  field2
1        charAt    77.0     72.0   462.0     584.0   127.5    89.5    86.0   159.5   165.0
2        charAt    38.0     36.5   284.0   32712.5    57.5    48.3    50.3    89.0    91.5
4        charAt    19.5     18.5   458.6    3169.0    33.0    26.8    27.5    54.1    52.6
8        charAt     9.8      9.9   100.5    1370.9    17.3    14.4    15.0    26.9    26.4
16       charAt     6.1      6.5    73.4     857.0     8.4     8.2     8.3    13.6    13.5
32       charAt     3.9      3.7    54.8     428.9     5.0     4.9     4.7     7.0     7.2
64       charAt     2.7      2.6    48.2     232.9     3.0     3.2     3.3     3.9     4.0
128      charAt     2.1      1.9    43.7     138.8     2.1     2.6     2.6     2.4     2.6
256      charAt     1.9      1.6    42.4      90.6     1.7     2.1     2.1     1.7     1.8
512      field1     1.7      1.4    40.6      60.5     1.4     1.9     1.9     1.3     1.4
1,024    field1     1.6      1.4    40.0      45.6     1.2     1.9     2.1     1.0     1.2
2,048    field1     1.6      1.3    40.0      36.2     1.2     1.8     1.7     0.9     1.1
4,096    field1     1.6      1.3    39.7      32.6     1.2     1.8     1.7     0.9     1.0
8,192    field1     1.6      1.3    39.6      30.5     1.2     1.8     1.7     0.9     1.0
16,384   field1     1.6      1.3    39.8      28.4     1.2     1.8     1.7     0.8     1.0
32,768   field1     1.6      1.3    40.0      26.7     1.3     1.8     1.7     0.8     1.0
65,536   field1     1.6      1.3    39.8      26.3     1.3     1.8     1.7     0.8     1.0
131,072  field1     1.6      1.3    40.1      25.4     1.4     1.9     1.8     0.8     1.0
262,144  field1     1.6      1.3    39.6      25.2     1.5     1.9     1.9     0.8     1.0

KẾT QUẢ COMPOSITE CHO MÁY CHỦ -server CHẾ ĐỘ (kết hợp kiểm tra tiến và lùi)

Lưu ý: đây là thử nghiệm cho Java 32 bit chạy ở chế độ máy chủ trên AMD64. Chế độ máy chủ cho Java 64 bit giống như Java 32 bit ở chế độ máy khách ngoại trừ việc truy cập Trường bắt đầu chiến thắng sau kích thước 32 ký tự.

Size     WINNER  charAt1 charAt2  stream streamPar   reuse    new1    new2  field1  field2
1        charAt     74.5    95.5   524.5     783.0    90.5   102.5    90.5   135.0   151.5
2        charAt     48.5    53.0   305.0   30851.3    59.3    57.5    52.0    88.5    91.8
4        charAt     28.8    32.1   132.8    2465.1    37.6    33.9    32.3    49.0    47.0
8          new2     18.0    18.6    63.4    1541.3    18.5    17.9    17.6    25.4    25.8
16         new2     14.0    14.7   129.4    1034.7    12.5    16.2    12.0    16.0    16.6
32         new2      7.8     9.1    19.3     431.5     8.1     7.0     6.7     7.9     8.7
64        reuse      6.1     7.5    11.7     204.7     3.5     3.9     4.3     4.2     4.1
128       reuse      6.8     6.8     9.0     101.0     2.6     3.0     3.0     2.6     2.7
256      field2      6.2     6.5     6.9      57.2     2.4     2.7     2.9     2.3     2.3
512       reuse      4.3     4.9     5.8      28.2     2.0     2.6     2.6     2.1     2.1
1,024    charAt      2.0     1.8     5.3      17.6     2.1     2.5     3.5     2.0     2.0
2,048    charAt      1.9     1.7     5.2      11.9     2.2     3.0     2.6     2.0     2.0
4,096    charAt      1.9     1.7     5.1       8.7     2.1     2.6     2.6     1.9     1.9
8,192    charAt      1.9     1.7     5.1       7.6     2.2     2.5     2.6     1.9     1.9
16,384   charAt      1.9     1.7     5.1       6.9     2.2     2.5     2.5     1.9     1.9
32,768   charAt      1.9     1.7     5.1       6.1     2.2     2.5     2.5     1.9     1.9
65,536   charAt      1.9     1.7     5.1       5.5     2.2     2.4     2.4     1.9     1.9
131,072  charAt      1.9     1.7     5.1       5.4     2.3     2.5     2.5     1.9     1.9
262,144  charAt      1.9     1.7     5.1       5.1     2.3     2.5     2.5     1.9     1.9

MÃ CHƯƠNG TRÌNH RUNNABLE ĐẦY ĐỦ

(để kiểm tra trên Java 7 trở về trước, hãy xóa hai kiểm tra luồng)

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.function.IntPredicate;

/**
 * @author Saint Hill <http://stackoverflow.com/users/1584255/saint-hill>
 */
public final class TestStrings {

    // we will not test strings longer than 512KM
    final int MAX_STRING_SIZE = 1024 * 256;

    // for each string size, we will do all the tests
    // this many times
    final int TRIES_PER_STRING_SIZE = 1000;

    public static void main(String[] args) throws Exception {
        new TestStrings().run();
    }

    void run() throws Exception {

        // double the length of the data until it reaches MAX chars long
        // 0,1,2,4,8,16,32,64,128,256 ... 
        final List<Integer> sizes = new ArrayList<>();
        for (int n = 0; n <= MAX_STRING_SIZE; n = (n == 0 ? 1 : n * 2)) {
            sizes.add(n);
        }

        // CREATE RANDOM (FOR SHUFFLING ORDER OF TESTS)
        final Random random = new Random();

        System.out.println("Rate in nanoseconds per character inspected.");
        System.out.printf("==== FORWARDS (tries per size: %s) ==== \n", TRIES_PER_STRING_SIZE);

        printHeadings(TRIES_PER_STRING_SIZE, random);

        for (int size : sizes) {
            reportResults(size, test(size, TRIES_PER_STRING_SIZE, random));
        }

        // reverse order or string sizes
        Collections.reverse(sizes);

        System.out.println("");
        System.out.println("Rate in nanoseconds per character inspected.");
        System.out.printf("==== BACKWARDS (tries per size: %s) ==== \n", TRIES_PER_STRING_SIZE);

        printHeadings(TRIES_PER_STRING_SIZE, random);

        for (int size : sizes) {
            reportResults(size, test(size, TRIES_PER_STRING_SIZE, random));

        }
    }

    ///
    ///
    ///  METHODS OF CHECKING THE CONTENTS
    ///  OF A STRING. ALWAYS CHECKING FOR
    ///  WHITESPACE (CHAR <=' ')
    ///  
    ///
    // CHECK THE STRING CONTENTS
    int charAtMethod1(final String data) {
        final int len = data.length();
        for (int i = 0; i < len; i++) {
            if (data.charAt(i) <= ' ') {
                doThrow();
            }
        }
        return len;
    }

    // SAME AS ABOVE BUT USE String.length()
    // instead of making a new final local int 
    int charAtMethod2(final String data) {
        for (int i = 0; i < data.length(); i++) {
            if (data.charAt(i) <= ' ') {
                doThrow();
            }
        }
        return data.length();
    }

    // USE new Java-8 String's IntStream
    // pass it a PREDICATE to do the checking
    int streamMethod(final String data, final IntPredicate predicate) {
        if (data.chars().anyMatch(predicate)) {
            doThrow();
        }
        return data.length();
    }

    // OH LA LA - GO PARALLEL!!!
    int streamParallelMethod(final String data, IntPredicate predicate) {
        if (data.chars().parallel().anyMatch(predicate)) {
            doThrow();
        }
        return data.length();
    }

    // Re-fill a resuable char[] with the contents
    // of the String's char[]
    int reuseBuffMethod(final char[] reusable, final String data) {
        final int len = data.length();
        data.getChars(0, len, reusable, 0);
        for (int i = 0; i < len; i++) {
            if (reusable[i] <= ' ') {
                doThrow();
            }
        }
        return len;
    }

    // Obtain a new copy of char[] from String
    int newMethod1(final String data) {
        final int len = data.length();
        final char[] copy = data.toCharArray();
        for (int i = 0; i < len; i++) {
            if (copy[i] <= ' ') {
                doThrow();
            }
        }
        return len;
    }

    // Obtain a new copy of char[] from String
    // but use FOR-EACH
    int newMethod2(final String data) {
        for (final char c : data.toCharArray()) {
            if (c <= ' ') {
                doThrow();
            }
        }
        return data.length();
    }

    // FANCY!
    // OBTAIN FIELD FOR ACCESS TO THE STRING'S
    // INTERNAL CHAR[]
    int fieldMethod1(final Field field, final String data) {
        try {
            final char[] chars = (char[]) field.get(data);
            final int len = chars.length;
            for (int i = 0; i < len; i++) {
                if (chars[i] <= ' ') {
                    doThrow();
                }
            }
            return len;
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    // same as above but use FOR-EACH
    int fieldMethod2(final Field field, final String data) {
        final char[] chars;
        try {
            chars = (char[]) field.get(data);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
        for (final char c : chars) {
            if (c <= ' ') {
                doThrow();
            }
        }
        return chars.length;
    }

    /**
     *
     * Make a list of tests. We will shuffle a copy of this list repeatedly
     * while we repeat this test.
     *
     * @param data
     * @return
     */
    List<Jobber> makeTests(String data) throws Exception {
        // make a list of tests
        final List<Jobber> tests = new ArrayList<Jobber>();

        tests.add(new Jobber("charAt1") {
            int check() {
                return charAtMethod1(data);
            }
        });

        tests.add(new Jobber("charAt2") {
            int check() {
                return charAtMethod2(data);
            }
        });

        tests.add(new Jobber("stream") {
            final IntPredicate predicate = new IntPredicate() {
                public boolean test(int value) {
                    return value <= ' ';
                }
            };

            int check() {
                return streamMethod(data, predicate);
            }
        });

        tests.add(new Jobber("streamPar") {
            final IntPredicate predicate = new IntPredicate() {
                public boolean test(int value) {
                    return value <= ' ';
                }
            };

            int check() {
                return streamParallelMethod(data, predicate);
            }
        });

        // Reusable char[] method
        tests.add(new Jobber("reuse") {
            final char[] cbuff = new char[MAX_STRING_SIZE];

            int check() {
                return reuseBuffMethod(cbuff, data);
            }
        });

        // New char[] from String
        tests.add(new Jobber("new1") {
            int check() {
                return newMethod1(data);
            }
        });

        // New char[] from String
        tests.add(new Jobber("new2") {
            int check() {
                return newMethod2(data);
            }
        });

        // Use reflection for field access
        tests.add(new Jobber("field1") {
            final Field field;

            {
                field = String.class.getDeclaredField("value");
                field.setAccessible(true);
            }

            int check() {
                return fieldMethod1(field, data);
            }
        });

        // Use reflection for field access
        tests.add(new Jobber("field2") {
            final Field field;

            {
                field = String.class.getDeclaredField("value");
                field.setAccessible(true);
            }

            int check() {
                return fieldMethod2(field, data);
            }
        });

        return tests;
    }

    /**
     * We use this class to keep track of test results
     */
    abstract class Jobber {

        final String name;
        long nanos;
        long chars;
        long runs;

        Jobber(String name) {
            this.name = name;
        }

        abstract int check();

        final double nanosPerChar() {
            double charsPerRun = chars / runs;
            long nanosPerRun = nanos / runs;
            return charsPerRun == 0 ? nanosPerRun : nanosPerRun / charsPerRun;
        }

        final void run() {
            runs++;
            long time = System.nanoTime();
            chars += check();
            nanos += System.nanoTime() - time;
        }
    }

    // MAKE A TEST STRING OF RANDOM CHARACTERS A-Z
    private String makeTestString(int testSize, char start, char end) {
        Random r = new Random();
        char[] data = new char[testSize];
        for (int i = 0; i < data.length; i++) {
            data[i] = (char) (start + r.nextInt(end));
        }
        return new String(data);
    }

    // WE DO THIS IF WE FIND AN ILLEGAL CHARACTER IN THE STRING
    public void doThrow() {
        throw new RuntimeException("Bzzzt -- Illegal Character!!");
    }

    /**
     * 1. get random string of correct length 2. get tests (List<Jobber>) 3.
     * perform tests repeatedly, shuffling each time
     */
    List<Jobber> test(int size, int tries, Random random) throws Exception {
        String data = makeTestString(size, 'A', 'Z');
        List<Jobber> tests = makeTests(data);
        List<Jobber> copy = new ArrayList<>(tests);
        while (tries-- > 0) {
            Collections.shuffle(copy, random);
            for (Jobber ti : copy) {
                ti.run();
            }
        }
        // check to make sure all char counts the same
        long runs = tests.get(0).runs;
        long count = tests.get(0).chars;
        for (Jobber ti : tests) {
            if (ti.runs != runs && ti.chars != count) {
                throw new Exception("Char counts should match if all correct algorithms");
            }
        }
        return tests;
    }

    private void printHeadings(final int TRIES_PER_STRING_SIZE, final Random random) throws Exception {
        System.out.print("  Size");
        for (Jobber ti : test(0, TRIES_PER_STRING_SIZE, random)) {
            System.out.printf("%9s", ti.name);
        }
        System.out.println("");
    }

    private void reportResults(int size, List<Jobber> tests) {
        System.out.printf("%6d", size);
        for (Jobber ti : tests) {
            System.out.printf("%,9.2f", ti.nanosPerChar());
        }
        System.out.println("");
    }
}

1
Thử nghiệm này có chạy trong máy chủ JVM hoặc máy khách JVM không? Các tối ưu hóa tốt nhất chỉ được thực hiện trong máy chủ JVM. Nếu bạn đã chạy bằng JVM 32 bit mặc định và không có đối số, thì bạn đã chạy trong chế độ máy khách.
ceklock

2
Nhận bộ đệm sao lưu có vấn đề trong trường hợp chuỗi con hoặc chuỗi được tạo bằng String (char [], int, int), khi bạn lấy toàn bộ bộ đệm (ít nhất là trên Android), nhưng việc lập chỉ mục của bạn sẽ không dựa trên. Tuy nhiên, nếu bạn biết rằng bạn không có chuỗi con, nó sẽ hoạt động tốt.
prewett

5
Bất kỳ ý tưởng tại sao "for (int i = 0; i <data.length (); i ++)" nhanh hơn định nghĩa data.length () là biến cục bộ cuối cùng?
skyin

2
Việc xác định một biến, tất cả, đòi hỏi một hoạt động ngăn xếp trong mã byte phương thức. Nhưng các tối ưu hóa, từ việc nhận ra thuật toán của bạn, có thể nhanh chóng theo dõi hoạt động lặp lại đó trong mã máy thực tế, mà không cần chi phí thay đổi. Tối ưu hóa như vậy đôi khi tồn tại trong trình biên dịch mã byte, đôi khi không. Tất cả phụ thuộc vào việc jvm có đủ thông minh hay không :-)
Điều phối viên

2
@DavidS các con số là tốc độ (tính bằng nano giây) trên mỗi ký tự được kiểm tra. Nhỏ hơn là tốt hơn.
Điều phối viên

14

Đây chỉ là tối ưu hóa vi mô mà bạn không nên lo lắng.

char[] chars = str.toCharArray();

trả về cho bạn một bản sao của strcác mảng ký tự (trong JDK, nó trả về một bản sao của các ký tự bằng cách gọi System.arrayCopy).

Ngoài ra, str.charAt()chỉ kiểm tra xem chỉ mục có thực sự nằm trong giới hạn không và trả về một ký tự trong chỉ mục mảng.

Cái đầu tiên không tạo thêm bộ nhớ trong JVM.


Không trả lời câu hỏi. Câu hỏi này là về hiệu suất. Đối với tất cả những gì bạn biết, OP có thể đã phát hiện ra rằng việc lặp qua các chuỗi là một chi phí lớn trong ứng dụng của họ.
rghome

9

Chỉ vì tò mò và để so sánh với câu trả lời của Saint Hill.

Nếu bạn cần xử lý dữ liệu nặng, bạn không nên sử dụng JVM trong chế độ máy khách. Chế độ máy khách không được thực hiện để tối ưu hóa.

Hãy so sánh kết quả của điểm chuẩn @Saint Hill bằng cách sử dụng JVM ở chế độ Máy khách và chế độ Máy chủ.

Core2Quad Q6600 G0 @ 2.4GHz
JavaSE 1.7.0_40

Xem thêm: Sự khác biệt thực sự giữa "java -server" và "java -client"?


CHẾ ĐỘ KHÁCH HÀNG:

len =      2:    111k charAt(i),  105k cbuff[i],   62k new[i],   17k field access.   (chars/ms) 
len =      4:    285k charAt(i),  166k cbuff[i],  114k new[i],   43k field access.   (chars/ms) 
len =      6:    315k charAt(i),  230k cbuff[i],  162k new[i],   69k field access.   (chars/ms) 
len =      8:    333k charAt(i),  275k cbuff[i],  181k new[i],   85k field access.   (chars/ms) 
len =     12:    342k charAt(i),  342k cbuff[i],  222k new[i],  117k field access.   (chars/ms) 
len =     16:    363k charAt(i),  347k cbuff[i],  275k new[i],  152k field access.   (chars/ms) 
len =     20:    363k charAt(i),  392k cbuff[i],  289k new[i],  180k field access.   (chars/ms) 
len =     24:    375k charAt(i),  428k cbuff[i],  311k new[i],  205k field access.   (chars/ms) 
len =     28:    378k charAt(i),  474k cbuff[i],  341k new[i],  233k field access.   (chars/ms) 
len =     32:    376k charAt(i),  492k cbuff[i],  340k new[i],  251k field access.   (chars/ms) 
len =     64:    374k charAt(i),  551k cbuff[i],  374k new[i],  367k field access.   (chars/ms) 
len =    128:    385k charAt(i),  624k cbuff[i],  415k new[i],  509k field access.   (chars/ms) 
len =    256:    390k charAt(i),  675k cbuff[i],  436k new[i],  619k field access.   (chars/ms) 
len =    512:    394k charAt(i),  703k cbuff[i],  439k new[i],  695k field access.   (chars/ms) 
len =   1024:    395k charAt(i),  718k cbuff[i],  462k new[i],  742k field access.   (chars/ms) 
len =   2048:    396k charAt(i),  725k cbuff[i],  471k new[i],  767k field access.   (chars/ms) 
len =   4096:    396k charAt(i),  727k cbuff[i],  459k new[i],  780k field access.   (chars/ms) 
len =   8192:    397k charAt(i),  712k cbuff[i],  446k new[i],  772k field access.   (chars/ms) 

CHẾ ĐỘ MÁY CHỦ:

len =      2:     86k charAt(i),   41k cbuff[i],   46k new[i],   80k field access.   (chars/ms) 
len =      4:    571k charAt(i),  250k cbuff[i],   97k new[i],  222k field access.   (chars/ms) 
len =      6:    666k charAt(i),  333k cbuff[i],  125k new[i],  315k field access.   (chars/ms) 
len =      8:    800k charAt(i),  400k cbuff[i],  181k new[i],  380k field access.   (chars/ms) 
len =     12:    800k charAt(i),  521k cbuff[i],  260k new[i],  545k field access.   (chars/ms) 
len =     16:    800k charAt(i),  592k cbuff[i],  296k new[i],  640k field access.   (chars/ms) 
len =     20:    800k charAt(i),  666k cbuff[i],  408k new[i],  800k field access.   (chars/ms) 
len =     24:    800k charAt(i),  705k cbuff[i],  452k new[i],  800k field access.   (chars/ms) 
len =     28:    777k charAt(i),  736k cbuff[i],  368k new[i],  933k field access.   (chars/ms) 
len =     32:    800k charAt(i),  780k cbuff[i],  571k new[i],  969k field access.   (chars/ms) 
len =     64:    800k charAt(i),  901k cbuff[i],  800k new[i],  1306k field access.   (chars/ms) 
len =    128:    1084k charAt(i),  888k cbuff[i],  633k new[i],  1620k field access.   (chars/ms) 
len =    256:    1122k charAt(i),  966k cbuff[i],  729k new[i],  1790k field access.   (chars/ms) 
len =    512:    1163k charAt(i),  1007k cbuff[i],  676k new[i],  1910k field access.   (chars/ms) 
len =   1024:    1179k charAt(i),  1027k cbuff[i],  698k new[i],  1954k field access.   (chars/ms) 
len =   2048:    1184k charAt(i),  1043k cbuff[i],  732k new[i],  2007k field access.   (chars/ms) 
len =   4096:    1188k charAt(i),  1049k cbuff[i],  742k new[i],  2031k field access.   (chars/ms) 
len =   8192:    1157k charAt(i),  1032k cbuff[i],  723k new[i],  2048k field access.   (chars/ms) 

PHẦN KẾT LUẬN:

Như bạn thấy, chế độ máy chủ nhanh hơn nhiều.


2
Cảm ơn vì đăng. Vì vậy, đối với các chuỗi lớn, truy cập trường vẫn nhanh hơn gấp 2 lần so với charAt (). Trên thực tế, truy cập trường thậm chí còn trở nên nhanh hơn với tổng thể dẫn đầu sau 28 chuỗi dài (crazy !!) Vì vậy ... chế độ máy chủ giúp mọi thứ nhanh hơn. Rất thú vị!
Điều phối viên

1
Vâng, phương pháp phản chiếu thực sự nhanh hơn. Hấp dẫn.
ceklock

2
btw: các JVM mới hơn sẽ tự động tìm ra máy chủ nào hoặc máy chủ nào hoạt động tốt nhất (thường): docs.oracle.com/javase/7/docs/technotes/guides/vm/ trộm
jontejj

2
@jontejj trong thực tế nó không đơn giản như vậy. Nếu bạn đang chạy JVM 32 bit trên Windows, thì JVM sẽ luôn mặc định cho máy khách.
ceklock

7

Việc đầu tiên sử dụng str.charAtnên nhanh hơn.

Nếu bạn đào bên trong mã nguồn của Stringlớp, chúng ta có thể thấy điều đó charAtđược triển khai như sau:

public char charAt(int index) {
    if ((index < 0) || (index >= count)) {
        throw new StringIndexOutOfBoundsException(index);
    }
    return value[index + offset];
}

Ở đây, tất cả những gì nó làm là lập chỉ mục một mảng và trả về giá trị.

Bây giờ, nếu chúng ta thấy việc thực hiện toCharArray, chúng ta sẽ tìm thấy bên dưới:

public char[] toCharArray() {
    char result[] = new char[count];
    getChars(0, count, result, 0);
    return result;
}

public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
    if (srcBegin < 0) {
        throw new StringIndexOutOfBoundsException(srcBegin);
    }
    if (srcEnd > count) {
        throw new StringIndexOutOfBoundsException(srcEnd);
    }
    if (srcBegin > srcEnd) {
        throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
    }
    System.arraycopy(value, offset + srcBegin, dst, dstBegin,
         srcEnd - srcBegin);
}

Như bạn thấy, nó đang làm một việc System.arraycopychắc chắn sẽ chậm hơn một chút so với không làm.


2
Thật ngớ ngẩn khi String # charAt nên thực hiện kiểm tra chỉ mục bổ sung, khi chỉ mục được kiểm tra bằng cách truy cập mảng.
Ingo

1
Có nguy cơ hồi sinh một chuỗi 8 năm tuổi ... Mảng char phía sau một chuỗi có thể lớn hơn chính chuỗi đó. Đó là, nếu bạn có một chuỗi "abcde" và sau đó bạn đã sử dụng chuỗi con để trích xuất "bcd" thành một chuỗi mới, chuỗi mới sẽ được hỗ trợ bởi chính xác chuỗi char như chuỗi đầu tiên. Đó là lý do tại sao lớp chuỗi duy trì một giá trị bù và số đếm - vì vậy nó biết ký tự nào trong mảng là đại diện cho chuỗi này. Vì vậy, việc kiểm tra phạm vi rất quan trọng nếu không có thể truy cập các ký tự nằm ngoài phần cuối của chuỗi này.
DTY

3

Bất chấp câu trả lời của @Saint Hill nếu bạn xem xét độ phức tạp thời gian của str.toCharArray () ,

cái đầu tiên nhanh hơn ngay cả đối với các chuỗi rất lớn. Bạn có thể chạy mã dưới đây để xem nó cho chính mình.

        char [] ch = new char[1_000_000_00];
    String str = new String(ch); // to create a large string

    // ---> from here
    long currentTime = System.nanoTime();
    for (int i = 0, n = str.length(); i < n; i++) {
        char c = str.charAt(i);
    }
    // ---> to here
    System.out.println("str.charAt(i):"+(System.nanoTime()-currentTime)/1000000.0 +" (ms)");

    /**
     *   ch = str.toCharArray() itself takes lots of time   
     */
    // ---> from here
    currentTime = System.nanoTime();
    ch = str.toCharArray();
    for (int i = 0, n = str.length(); i < n; i++) {
        char c = ch[i];
    }
    // ---> to  here
    System.out.println("ch = str.toCharArray() + c = ch[i] :"+(System.nanoTime()-currentTime)/1000000.0 +" (ms)");

đầu ra:

str.charAt(i):5.492102 (ms)
ch = str.toCharArray() + c = ch[i] :79.400064 (ms)

2

Hình như niether nhanh hơn hay chậm hơn

    public static void main(String arguments[]) {


        //Build a long string
        StringBuilder sb = new StringBuilder();
        for(int j = 0; j < 10000; j++) {
            sb.append("a really, really long string");
        }
        String str = sb.toString();
        for (int testscount = 0; testscount < 10; testscount ++) {


            //Test 1
            long start = System.currentTimeMillis();
            for(int c = 0; c < 10000000; c++) {
                for (int i = 0, n = str.length(); i < n; i++) {
                    char chr = str.charAt(i);
                    doSomethingWithChar(chr);//To trick JIT optimistaion
                }
            }

            System.out.println("1: " + (System.currentTimeMillis() - start));

            //Test 2
            start = System.currentTimeMillis();
            char[] chars = str.toCharArray();
            for(int c = 0; c < 10000000; c++) {
                for (int i = 0, n = chars.length; i < n; i++) {
                    char chr = chars[i];
                    doSomethingWithChar(chr);//To trick JIT optimistaion
                }
            }
            System.out.println("2: " + (System.currentTimeMillis() - start));
            System.out.println();
        }


    }


    public static void doSomethingWithChar(char chr) {
        int newInt = chr << 2;
    }

Đối với chuỗi dài, tôi sẽ chọn chuỗi đầu tiên. Tại sao sao chép xung quanh chuỗi dài? Tài liệu nói:

public char [] toCharArray () Chuyển đổi chuỗi này thành một mảng ký tự mới.

Trả về: một mảng ký tự mới được phân bổ có độ dài là độ dài của chuỗi này và có nội dung được khởi tạo để chứa chuỗi ký tự được đại diện bởi chuỗi này.

// Chỉnh sửa 1

Tôi đã thay đổi thử nghiệm để đánh lừa tối ưu hóa JIT.

// Chỉnh sửa 2

Lặp lại thử nghiệm 10 lần để JVM ấm lên.

// Chỉnh sửa 3

Kết luận:

Trước hết str.toCharArray();sao chép toàn bộ chuỗi trong bộ nhớ. Nó có thể tiêu thụ bộ nhớ cho chuỗi dài. Phương thức String.charAt( )tra cứu char trong mảng char bên trong chỉ mục kiểm tra lớp String. Có vẻ như đối với phương thức chuỗi đầu tiên đủ ngắn (tức là chatAtphương thức) chậm hơn một chút do kiểm tra chỉ mục này. Nhưng nếu Chuỗi đủ dài, việc sao chép toàn bộ mảng char sẽ chậm hơn và phương thức đầu tiên nhanh hơn. Chuỗi càng dài thì hiệu suất càng chậm toCharArray. Cố gắng thay đổi giới hạn trongfor(int j = 0; j < 10000; j++) vòng lặp để xem nó. Nếu chúng ta để mã làm nóng JVM chạy nhanh hơn, nhưng tỷ lệ là như nhau.

Sau tất cả, nó chỉ là tối ưu hóa vi mô.


Bạn có thể thử for:intùy chọn, chỉ cho niềm vui của nó?
dasblinkenlight

2
Điểm chuẩn của bạn là thiếu sót: nó không cho phép JIT thực hiện tối ưu hóa; JIT có thể loại bỏ hoàn toàn các vòng lặp, vì chúng không làm gì cả.
JB Nizet

Chuỗi không phải là na Iterablecũng không phải là mảng.
Piotr Gwiazda

2
Đây không phải là một thử nghiệm hợp lệ, bạn đã 'làm nóng' JVM của mình với Thử nghiệm 1, có thể làm lệch kết quả theo hướng có lợi cho Thử nghiệm 2. Toàn bộ câu hỏi của OP có mùi tối ưu hóa vi mô.
Nhận thức

1
Thật. Sau khi khởi động (xem Chỉnh sửa 2) cả hai lần nhỏ hơn vẫn gần nhau. Trong ví dụ thử nghiệm thứ hai của tôi nhanh hơn một chút. Nhưng nếu tôi làm cho String dài hơn, cái đầu tiên sẽ nhanh hơn. Chuỗi dài hơn, thử nghiệm thứ hai chậm hơn, do sao chép mảng char. Chỉ cần làm theo cách đầu tiên.
Piotr Gwiazda

2

String.toCharArray()tạo mảng char mới, có nghĩa là cấp phát bộ nhớ cho độ dài chuỗi, sau đó sao chép mảng char ban đầu của chuỗi bằng cách sử dụng System.arraycopy()và sau đó trả lại bản sao này cho người gọi. String.charAt () trả về ký tự ở vị trí itừ bản sao gốc, đó là lý do tại sao String.charAt()sẽ nhanh hơn String.toCharArray(). Mặc dù, String.toCharArray()trả về bản sao và không phải char từ mảng String gốc, nơi String.charAt()trả về ký tự từ mảng char ban đầu. Mã dưới đây trả về giá trị tại chỉ mục được chỉ định của chuỗi này.

public char charAt(int index) {
    if ((index < 0) || (index >= value.length)) {
        throw new StringIndexOutOfBoundsException(index);
    }
    return value[index];
}

mã dưới đây trả về một mảng ký tự mới được phân bổ có độ dài là độ dài của chuỗi này

public char[] toCharArray() {
    // Cannot use Arrays.copyOf because of class initialization order issues
    char result[] = new char[value.length];
    System.arraycopy(value, 0, result, 0, value.length);
    return result;
}

1

Cái thứ hai làm cho một mảng char mới được tạo ra và tất cả các ký tự từ Chuỗi được sao chép sang mảng char mới này, vì vậy tôi đoán rằng cái thứ nhất nhanh hơn (và ít bộ nhớ hơn).

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.