การอัปเดตครั้งแรก: ก่อนที่คุณจะลองในสภาพแวดล้อมการผลิต (ไม่แนะนำ) ให้อ่านสิ่งนี้ก่อน: 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 ฉันสามารถพูดได้ว่า:
- String.charAt (i) เป็นผู้ชนะที่ชัดเจนโดยรวม แม้ว่าระหว่าง 8 ถึง 512 ตัวอักษรมีผู้ชนะระหว่าง 'ใหม่' 'นำมาใช้ใหม่' และ 'ฟิลด์'
- String.charAt (i) เร็วขึ้น 45% ในโหมดไคลเอนต์
- การเข้าถึงฟิลด์เร็วกว่าสองเท่าสำหรับ Strings ขนาดใหญ่ในโหมดไคลเอนต์
นอกจากนี้ยังมีมูลค่าการพูด String.chars () (สตรีมและรุ่นขนาน) เป็นรูปปั้นครึ่งตัว ช้ากว่าวิธีอื่นใด Streams
API เป็นวิธีที่ค่อนข้างช้าในการดำเนินการสตริงทั่วไป
รายการที่ต้องการ
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("");
}
}
for (char c : chars)
?