วิธีที่เร็วที่สุดในการวนซ้ำตัวอักษรทั้งหมดในสตริง


163

ใน Java สิ่งที่จะเป็นวิธีที่เร็วที่สุดในการวนซ้ำทุกตัวอักษรใน String สิ่งนี้:

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

หรือสิ่งนี้:

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

แก้ไข:

สิ่งที่ฉันต้องการทราบคือถ้าค่าใช้จ่ายในการเรียกcharAtวิธีการซ้ำ ๆ ซ้ำ ๆ ในระหว่างการทำซ้ำที่ยาวนานนั้นจะน้อยกว่าหรือสูงกว่าค่าใช้จ่ายในการโทรครั้งเดียวไปtoCharArrayยังจุดเริ่มต้นจากนั้นเข้าถึงอาร์เรย์โดยตรงระหว่างการทำซ้ำ

มันจะดีถ้ามีใครสามารถสร้างมาตรฐานที่แข็งแกร่งสำหรับความยาวของสตริงที่แตกต่างกันโดยคำนึงถึงเวลาการอุ่นเครื่องของ JIT เวลาเริ่มต้นของ JVM เป็นต้นและไม่ใช่แค่ความแตกต่างระหว่างการโทรสองSystem.currentTimeMillis()ครั้ง


18
เกิดอะไรขึ้นกับfor (char c : chars)?
dasblinkenlight

อันแรกควรจะเร็วกว่านี้อีกแล้วและก็เป็นอาร์เรย์ของถ่าน
Keagan Ladds

Google มักเป็นแหล่งข้อมูลที่ดี: mkyong.com/java/…
Johan Sjöberg

2
คำถามไม่ได้ถามถึงประสิทธิภาพของการใช้ตัววนซ้ำ foreach สิ่งที่ฉันอยากรู้คือราคาค่าโทรซ้ำ ๆcharAtนั้นต่ำกว่าหรือสูงกว่าค่าโทรเพียงครั้งเดียวtoCharArray
ÓscarLópez

1
มีใครทำการวิเคราะห์ด้วยStringCharacterIteratorหรือไม่
bdrx

คำตอบ:


352

การอัปเดตครั้งแรก: ก่อนที่คุณจะลองในสภาพแวดล้อมการผลิต (ไม่แนะนำ) ให้อ่านสิ่งนี้ก่อน: http://www.javaspecialists.eu/archive/Issue237.html เริ่มต้นจาก Java 9 โซลูชันที่อธิบายไว้จะไม่ทำงานอีกต่อไป เพราะตอนนี้ Java จะเก็บสตริงเป็นไบต์ [] โดยค่าเริ่มต้น

การอัพเดทครั้งที่สอง: ตั้งแต่วันที่ 2016-10-25 บน AMDx64 8core และ 1.8 ที่มาของฉันไม่มีความแตกต่างระหว่างการใช้ 'charAt' และการเข้าถึงฟิลด์ ดูเหมือนว่า jvm ได้รับการปรับให้เหมาะสมเพียงพอที่จะอินไลน์และปรับปรุงการเรียก 'string.charAt (n)' ใด ๆ

ทุกอย่างขึ้นอยู่กับความยาวของStringการตรวจสอบ หากเป็นคำถามที่บอกว่ามันเป็นสตริงที่ยาววิธีที่เร็วที่สุดในการตรวจสอบสตริงคือการใช้การสะท้อนเพื่อเข้าถึงการสำรองข้อมูลchar[]ของสตริง

เกณฑ์มาตรฐานแบบสุ่มเต็มรูปแบบด้วย JDK 8 (win32 และ win64) บน 64 AMD Phenom II 4 คอร์ 955 @ 3.2 GHZ (ทั้งในโหมดไคลเอนต์และโหมดเซิร์ฟเวอร์) ด้วย 9 เทคนิคที่แตกต่างกัน (ดูด้านล่าง!) แสดงว่าการใช้String.charAt(n)นั้นเร็วที่สุดสำหรับขนาดเล็ก สตริงและที่ใช้reflectionในการเข้าถึงอาร์เรย์สำรองสตริงเกือบสองเท่าเร็วสำหรับสตริงขนาดใหญ่

การทดลอง

  • มีการลองใช้เทคนิคการปรับให้เหมาะสมที่สุด 9 แบบ

  • เนื้อหาสตริงทั้งหมดจะถูกสุ่ม

  • การทดสอบจะทำสำหรับขนาดสตริงในทวีคูณของสองเริ่มต้นด้วย 0,1,2,4,8,16 เป็นต้น

  • การทดสอบ 1,000 ครั้งต่อขนาดสตริง

  • การทดสอบจะถูกสับเป็นลำดับแบบสุ่มในแต่ละครั้ง กล่าวอีกนัยหนึ่งการทดสอบจะทำแบบสุ่มทุกครั้งที่ทำมากกว่า 1,000 ครั้ง

  • ชุดการทดสอบทั้งหมดจะถูกส่งต่อไปข้างหน้าและข้างหลังเพื่อแสดงผลของการอุ่นเครื่อง JVM ในการเพิ่มประสิทธิภาพและเวลา

  • ทั้งชุดจะทำสองครั้งหนึ่งครั้งใน-clientโหมดและอื่น ๆ ใน-serverโหมด

สรุป

- โหมดไคลเอนต์ (32 บิต)

สำหรับความยาวสตริงที่1 ถึง 256 อักขระการโทรstring.charAt(i)ชนะด้วยการประมวลผลเฉลี่ย 13.4 ล้านถึง 588 ล้านตัวอักษรต่อวินาที

นอกจากนี้มันยังเร็วกว่า 5.5% โดยรวม (ลูกค้า) และ 13.9% (เซิร์ฟเวอร์) ดังนี้:

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

มากกว่านี้ด้วยตัวแปรความยาวสุดท้าย:

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

สำหรับสตริงที่ยาวความยาวอักขระ512 ถึง 256K การใช้การสะท้อนเพื่อเข้าถึงอาเรย์สำรองของสตริงนั้นเร็วที่สุด เทคนิคนี้เร็วเกือบสองเท่าของ String.charAt (i) (เร็วขึ้น 178%) ความเร็วเฉลี่ยในช่วงนี้คือ 1.111 พันล้านตัวอักษรต่อวินาที

ต้องได้รับฟิลด์ล่วงหน้าก่อนจากนั้นจะสามารถใช้ซ้ำในไลบรารีบนสตริงที่แตกต่างกัน น่าสนใจไม่เหมือนกับรหัสด้านบนที่มีการเข้าถึงฟิลด์จะเร็วกว่า 9% ที่จะมีตัวแปรความยาวสุดท้ายในพื้นที่กว่าการใช้ 'chars.length' ในการตรวจสอบลูป นี่คือวิธีที่การตั้งค่าการเข้าถึงฟิลด์สามารถทำได้เร็วที่สุด:

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

ความคิดเห็นพิเศษเกี่ยวกับโหมดเซิร์ฟเวอร์

การเข้าถึงภาคสนามเริ่มต้นที่ชนะหลังจากสายอักขระความยาว 32 ตัวในโหมดเซิร์ฟเวอร์บนเครื่อง Java 64 บิตบนเครื่อง AMD 64 ของฉัน ไม่ปรากฏจนกว่าจะมีความยาว 512 อักขระในโหมดไคลเอนต์

ก็น่าสังเกตว่าเมื่อฉันใช้ JDK 8 (รุ่น 32 บิต) ในโหมดเซิร์ฟเวอร์ประสิทธิภาพโดยรวมจะช้าลง 7% สำหรับทั้งสตริงขนาดใหญ่และขนาดเล็ก นี่คือด้วยการสร้าง 121 ธันวาคม 2013 จาก JDK 8 รุ่นแรก ดังนั้นสำหรับตอนนี้ดูเหมือนว่าโหมดเซิร์ฟเวอร์ 32 บิตช้ากว่าโหมดไคลเอ็นต์ 32 บิต

ที่ถูกกล่าวว่า ... ดูเหมือนว่าโหมดเซิร์ฟเวอร์เดียวที่คุ้มค่าการเรียกใช้อยู่ในเครื่อง 64 บิต มิฉะนั้นมันจะลดประสิทธิภาพลง

สำหรับการสร้าง 32 บิตที่ทำงาน-server modeบน AMD64 ฉันสามารถพูดได้ว่า:

  1. String.charAt (i) เป็นผู้ชนะที่ชัดเจนโดยรวม แม้ว่าระหว่าง 8 ถึง 512 ตัวอักษรมีผู้ชนะระหว่าง 'ใหม่' 'นำมาใช้ใหม่' และ 'ฟิลด์'
  2. String.charAt (i) เร็วขึ้น 45% ในโหมดไคลเอนต์
  3. การเข้าถึงฟิลด์เร็วกว่าสองเท่าสำหรับ Strings ขนาดใหญ่ในโหมดไคลเอนต์

นอกจากนี้ยังมีมูลค่าการพูด String.chars () (สตรีมและรุ่นขนาน) เป็นรูปปั้นครึ่งตัว ช้ากว่าวิธีอื่นใด StreamsAPI เป็นวิธีที่ค่อนข้างช้าในการดำเนินการสตริงทั่วไป

รายการที่ต้องการ

Java String อาจมีเพรดิเคตที่ยอมรับวิธีการที่ปรับให้เหมาะสมเช่นมี (เพรดิเคต), forEach (consumer), forEachWithIndex (consumer) ดังนั้นโดยไม่จำเป็นต้องให้ผู้ใช้ทราบความยาวหรือการเรียกซ้ำไปยังเมธอด String สิ่งเหล่านี้สามารถช่วยในการแยกวิเคราะห์การbeep-beep beepเพิ่มความเร็วของไลบรารี

ฝันต่อไป :)

ขอให้มีความสุข!

~ SH

การทดสอบใช้วิธีการ 9 ข้อต่อไปนี้ในการทดสอบสตริงสำหรับการมีอยู่ของช่องว่าง:

"charAt1" - ตรวจสอบเนื้อหาการใช้งานปกติ:

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" - เหมือนกัน แต่ใช้ String.length () แทนการสร้าง int สุดท้ายสำหรับความยาว

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

"สตรีม" - ใช้สตรีมมิ่งใหม่ของ JAVA-8 String และส่งผ่านการคาดการณ์เพื่อทำการตรวจสอบ

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

"streamPara" - เหมือนกันเหนือกว่า แต่ OH-LA-LA - GO Parallel !!!

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

"นำมาใช้ใหม่" - เติมถ่านที่นำกลับมาใช้ใหม่ได้ [] ด้วยเนื้อหาของสตริง

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" - ขอรับสำเนาใหม่ของอักขระ [] จากสตริง

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" - เหมือนกัน แต่ใช้ "ไปข้างหน้า"

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

"field1" - FANCY !! ได้รับสนามสำหรับการเข้าถึงถ่านภายในสตริง []

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" - เหมือนกัน แต่ใช้ "ไปข้างหน้า"

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

ผลการคอมโพสิตสำหรับ-clientโหมดไคลเอนต์(รวมการทดสอบไปข้างหน้าและข้างหลังรวมกัน)

หมายเหตุ: โหมด -client พร้อม Java 32 บิตและโหมด -server พร้อม Java 64 บิตจะเหมือนกันกับด้านล่างในเครื่อง AMD64 ของฉัน

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

ผลการคอมโพสิตสำหรับ-serverโหมดเซิร์ฟเวอร์(รวมการทดสอบไปข้างหน้าและถอยหลัง)

หมายเหตุ: นี่เป็นการทดสอบสำหรับ Java 32 บิตที่ทำงานในโหมดเซิร์ฟเวอร์บน AMD64 โหมดเซิร์ฟเวอร์สำหรับ Java 64 บิตเป็นเช่นเดียวกับ Java 32 บิตในโหมดไคลเอนต์ยกเว้นว่าการเข้าถึงฟิลด์เริ่มต้นที่ชนะหลังจากขนาดอักขระ 32

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

รหัสโปรแกรมวิ่งแบบเต็ม

(เพื่อทดสอบบน Java 7 และรุ่นก่อนหน้าให้ลบการทดสอบสตรีมสองรายการ)

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
การทดสอบนี้รันในเซิร์ฟเวอร์ JVM หรือไคลเอ็นต์ JVM หรือไม่ การปรับให้เหมาะสมที่ดีที่สุดนั้นทำได้ในเซิร์ฟเวอร์ JVM เท่านั้น หากคุณรันโดยใช้ JVM 32 บิตดีฟอลต์และไม่มีอาร์กิวเมนต์ดังนั้นคุณรันในโหมดไคลเอ็นต์
ceklock

2
การรับบัฟเฟอร์สำรองเป็นปัญหาในกรณีของสตริงย่อยหรือสตริงที่สร้างขึ้นโดยใช้ String (char [], int, int) ตามที่คุณได้รับบัฟเฟอร์ทั้งหมด (อย่างน้อยใน Android) แต่การทำดัชนีของคุณจะเป็นศูนย์ อย่างไรก็ตามหากคุณรู้ว่าคุณไม่มีซับสตริงมันจะทำงานได้ดี
prewett

5
มีความคิดว่าทำไม "for (int i = 0; i <data.length (); i ++)" เร็วกว่าการกำหนด data.length () เป็นตัวแปรท้องถิ่นขั้นสุดท้าย?
skyin

2
การกำหนดตัวแปรทั้งหมดจำเป็นต้องมีการดำเนินการสแต็กในโค้ดไบต์เมธอด แต่การปรับให้เหมาะสมจากการจดจำอัลกอริทึมของคุณสามารถติดตามได้อย่างรวดเร็วว่าการดำเนินการซ้ำในรหัสเครื่องจริงโดยไม่มีค่าใช้จ่ายการจัดสรรตัวแปร การเพิ่มประสิทธิภาพดังกล่าวบางครั้งมีอยู่ในคอมไพเลอร์ bytecode บางครั้งไม่ ทุกอย่างขึ้นอยู่กับว่า jvm นั้นฉลาดพอ :-)
ผู้ประสานงาน

2
@DavidS ตัวเลขคืออัตรา (เป็นนาโนวินาที) ต่อการตรวจสอบอักขระ เล็กกว่าดีกว่า
ผู้ประสานงาน

14

นี่เป็นเพียงการเพิ่มประสิทธิภาพขนาดเล็กที่คุณไม่ควรกังวล

char[] chars = str.toCharArray();

ส่งคืนสำเนาของstrอักขระอาร์เรย์ (ใน JDK จะส่งคืนสำเนาอักขระโดยการโทรSystem.arrayCopy)

นอกเหนือจากนั้นstr.charAt()ตรวจสอบว่าดัชนีอยู่ในขอบเขตและส่งกลับอักขระภายในดัชนีอาร์เรย์หรือไม่

คนแรกไม่ได้สร้างหน่วยความจำเพิ่มเติมใน JVM


ไม่ตอบคำถาม คำถามนี้เกี่ยวกับประสิทธิภาพ สำหรับทุกสิ่งที่คุณรู้ OP อาจค้นพบว่าการวนซ้ำสตริงเป็นค่าใช้จ่ายที่สำคัญในการสมัคร
rghome

9

เพียงแค่อยากรู้อยากเห็นและเปรียบเทียบกับคำตอบของเซนต์ฮิลล์

หากคุณต้องการประมวลผลข้อมูลจำนวนมากคุณไม่ควรใช้ JVM ในโหมดไคลเอนต์ โหมดไคลเอนต์ไม่ได้ถูกสร้างขึ้นเพื่อปรับให้เหมาะสม

ลองเปรียบเทียบผลลัพธ์ของการวัด @Saint Hill โดยใช้ JVM ในโหมดไคลเอนต์และโหมดเซิร์ฟเวอร์

Core2Quad Q6600 G0 @ 2.4GHz
JavaSE 1.7.0_40

ดูเพิ่มเติม: ความแตกต่างที่แท้จริงระหว่าง "java -server" และ "java -client"?


โหมดลูกค้า:

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) 

โหมดเซิร์ฟเวอร์:

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) 

สรุป:

อย่างที่คุณเห็นโหมดเซิร์ฟเวอร์นั้นเร็วกว่ามาก


2
ขอบคุณสำหรับการโพสต์ ดังนั้นสำหรับสตริงขนาดใหญ่การเข้าถึงฟิลด์ยังเร็วกว่า charAt () 2 เท่า ในความเป็นจริงการเข้าถึงฟิลด์นั้นเร็วขึ้นโดยรวมเมื่อเทียบกับการนำหลังความยาว 28 สตริง (บ้า !!) ดังนั้น ... โหมดเซิร์ฟเวอร์ทำให้ทุกอย่างเร็วขึ้น น่าสนใจมาก!
ผู้ประสานงาน

1
ใช่วิธีการไตร่ตรองเร็วกว่าจริงๆ น่าสนใจ
ceklock

2
btw: JVM ที่ใหม่กว่าจะหาตัวเลขของ -server หรือ -client ที่ทำงานได้ดีที่สุด (ปกติ): docs.oracle.com/javase/7/docs/technotes/guides/vm/…
jontejj

2
@jontejj ในทางปฏิบัติมันไม่ง่ายเลย หากคุณใช้ JVM แบบ 32 บิตบน Windows ดังนั้น JVM จะเป็นค่าเริ่มต้นสำหรับไคลเอ็นต์เสมอ
ceklock

7

การใช้ครั้งแรกstr.charAtควรเร็วขึ้น

หากคุณขุดภายในซอร์สโค้ดของStringคลาสเราจะเห็นว่าcharAtมีการใช้งานดังนี้:

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

ที่นี่ทั้งหมดจะทำดัชนีอาร์เรย์และส่งกลับค่า

ตอนนี้ถ้าเราเห็นการใช้งานtoCharArrayเราจะพบด้านล่าง:

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

อย่างที่คุณเห็นมันกำลังทำอยู่System.arraycopyซึ่งแน่นอนว่าจะช้ากว่าที่จะไม่ทำ


2
เป็นเรื่องโง่ที่ String # charAt ควรทำการตรวจสอบดัชนีเพิ่มเติมเมื่อมีการตรวจสอบดัชนีในการเข้าถึงอาร์เรย์
Ingo

1
ความเสี่ยงในการฟื้นเธรดอายุ 8 ปี ... อาร์เรย์ถ่านที่อยู่ด้านหลังสตริงอาจใหญ่กว่าตัวสตริงเอง นั่นคือถ้าคุณมีสตริง "abcde" และจากนั้นคุณใช้สตริงย่อยเพื่อแยก "bcd" ลงในสตริงใหม่สตริงใหม่จะได้รับการสำรองข้อมูลโดยอาร์เรย์ char เดียวกันกับสตริงแรก นั่นเป็นสาเหตุที่คลาสสตริงรักษาออฟเซ็ตและการนับดังนั้นจึงรู้ว่าตัวอักษรใดในอาเรย์คือตัวที่แสดงถึงสตริงนี้ ดังนั้นการตรวจสอบระยะจึงมีความสำคัญไม่เช่นนั้นมันจะเป็นไปได้ที่จะเข้าถึงตัวละครที่อยู่ด้านท้ายของสตริงนี้
dty

3

แม้จะมีคำตอบ @Saint ฮิลล์ถ้าคุณพิจารณาความซับซ้อนเวลาของการstr.toCharArray () ,

อันแรกเร็วกว่าแม้สำหรับสตริงที่มีขนาดใหญ่มาก คุณสามารถเรียกใช้รหัสด้านล่างเพื่อดูด้วยตัวคุณเอง

        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)");

เอาท์พุท:

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

2

ดูเหมือนว่า niether เร็วขึ้นหรือช้าลง

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

สำหรับสายยาวฉันจะเลือกอันแรก เหตุใดจึงต้องคัดลอกสตริงที่มีความยาว เอกสารพูดว่า:

ถ่านสาธารณะ [] toCharArray () แปลงสายนี้เป็นอาร์เรย์ตัวละครใหม่

คืนค่า: อาร์เรย์อักขระที่จัดสรรใหม่ซึ่งความยาวคือความยาวของสตริงนี้และมีเนื้อหาถูกเตรียมใช้งานเพื่อให้มีลำดับอักขระที่แสดงโดยสตริงนี้

// แก้ไข 1

ฉันเปลี่ยนการทดสอบเพื่อหลอกลวงการเพิ่มประสิทธิภาพ JIT

// แก้ไข 2

ทำซ้ำการทดสอบ 10 ครั้งเพื่อให้ JVM อุ่นเครื่อง

// แก้ไข 3

สรุป:

ก่อนอื่นstr.toCharArray();คัดลอกสตริงทั้งหมดในหน่วยความจำ สามารถใช้หน่วยความจำสำหรับสตริงที่ยาว วิธีการString.charAt( )ค้นหาถ่านในอาร์เรย์ถ่านภายในดัชนีการตรวจสอบคลาส String ก่อน ดูเหมือนว่าวิธี Strings แรกที่สั้นพอ (ie chatAtmethod) จะช้าลงเล็กน้อยเนื่องจากการตรวจสอบดัชนีนี้ แต่ถ้า String ยาวพอการคัดลอก char char ทั้งหมดจะช้าลงและวิธีแรกจะเร็วกว่า อีกต่อไปสตริงช้าtoCharArrayดำเนินการ ลองเปลี่ยนการ จำกัดfor(int j = 0; j < 10000; j++)วงเพื่อดู หากเราปล่อยให้ JVM warm up code ทำงานเร็วขึ้น แต่สัดส่วนจะเท่ากัน

หลังจากทั้งหมดมันเป็นเพียงการเพิ่มประสิทธิภาพขนาดเล็ก


คุณลองfor:inตัวเลือกเพื่อความสนุกของมันได้หรือไม่
dasblinkenlight

2
มาตรฐานของคุณมีข้อบกพร่อง: มันไม่ปล่อยให้ JIT ทำการปรับปรุงให้เหมาะสม JIT สามารถลบลูปได้อย่างสมบูรณ์เนื่องจากพวกเขาไม่ทำอะไรเลย
JB Nizet

สตริงไม่ได้เป็นIterableอาเรย์หรือ
Piotr Gwiazda

2
นี่ไม่ใช่การทดสอบที่ถูกต้องคุณได้ 'อุ่น' JVM ของคุณด้วยการทดสอบ 1 ซึ่งสามารถบิดเบือนผลลัพธ์ไปสู่ความโปรดปรานของการทดสอบ 2 คำถามทั้งหมดของ OP คือการเพิ่มประสิทธิภาพขนาดเล็ก
การรับรู้

1
จริง หลังจากอุ่นขึ้น (ดูแก้ไข 2) ทั้งสองครั้งจะมีขนาดเล็กกว่าและยังคงอยู่ใกล้กัน ในตัวอย่างของฉันการทดสอบที่สองนั้นเร็วขึ้นเล็กน้อย แต่ถ้าฉันทำให้ String ยาวขึ้นอันแรกก็จะเร็วขึ้น สตริงที่ยาวกว่าการทดสอบที่ช้ากว่าคือเนื่องจากการคัดลอกอาเรย์ถ่าน เพียงทำมันเป็นวิธีแรก
Piotr Gwiazda

2

String.toCharArray()สร้างอาร์เรย์ถ่านใหม่หมายถึงการจัดสรรหน่วยความจำของความยาวสตริงแล้วคัดลอกอาร์เรย์ถ่านดั้งเดิมของสตริงโดยใช้System.arraycopy()แล้วส่งคืนสำเนานี้ไปยังผู้โทร String.charAt () ผลตอบแทนของตัวละครที่ตำแหน่งiจากต้นฉบับที่ว่าทำไมจะเร็วกว่าString.charAt() String.toCharArray()แม้ว่าจะString.toCharArray()ส่งคืนการคัดลอกและไม่ถ่านจากอาร์เรย์สตริงเดิมซึ่งString.charAt()ส่งกลับอักขระจากอาร์เรย์ถ่านดั้งเดิม โค้ดด้านล่างส่งคืนค่าที่ดัชนีที่ระบุของสตริงนี้

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

โค้ดด้านล่างจะส่งกลับอาร์เรย์อักขระที่จัดสรรใหม่ซึ่งความยาวคือความยาวของสตริงนี้

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

อันที่สองเป็นสาเหตุให้สร้างอาร์เรย์ถ่านใหม่และตัวอักษรทั้งหมดจากสตริงจะถูกคัดลอกไปยังอาร์เรย์ถ่านใหม่นี้ดังนั้นฉันเดาว่าอันแรกจะเร็วกว่า (และหิวน้อยกว่าหน่วยความจำ)

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.