PIERWSZA AKTUALIZACJA: Zanim wypróbujesz to kiedykolwiek w środowisku produkcyjnym (niezalecane), przeczytaj najpierw: http://www.javaspecialists.eu/archive/Issue237.html
Począwszy od Java 9, opisane rozwiązanie nie będzie już działać , ponieważ teraz Java będzie domyślnie przechowywać łańcuchy jako bajt [].
DRUGA AKTUALIZACJA: od 25.10.2016 r. Na moim AMDx64 8core i source 1.8 nie ma różnicy między używaniem „charAt” a dostępem do pola. Wygląda na to, że jvm jest wystarczająco zoptymalizowany, aby wbudowywać i usprawniać wszelkie wywołania „string.charAt (n)”.
Wszystko zależy od długości String
badanego. Jeśli, jak mówi pytanie, dotyczy to długich ciągów, najszybszym sposobem sprawdzenia łańcucha jest użycie odbicia w celu uzyskania dostępu do kopii char[]
łańcucha.
W pełni losowy test porównawczy z JDK 8 (win32 i win64) na 64 AMD Phenom II 4 core 955 @ 3,2 GHZ (zarówno w trybie klienta, jak iw trybie serwera) z 9 różnymi technikami (patrz poniżej!) Pokazuje, że używanie String.charAt(n)
jest najszybsze w przypadku małych stringi, a użycie reflection
do uzyskania dostępu do tablicy zaplecza String jest prawie dwukrotnie szybsze w przypadku dużych ciągów.
EKSPERYMENT
Wypróbowano 9 różnych technik optymalizacji.
Cała zawartość ciągu jest losowa
Test jest wykonywany dla rozmiarów strun w wielokrotności dwóch, zaczynając od 0,1,2,4,8,16 itd.
Testy są wykonywane 1000 razy na rozmiar łańcucha
Testy są za każdym razem tasowane w losowej kolejności. Innymi słowy, testy są wykonywane w losowej kolejności za każdym razem, gdy są wykonywane, ponad 1000 razy.
Cały zestaw testów jest wykonywany do przodu i do tyłu, aby pokazać wpływ rozgrzewania maszyny JVM na optymalizację i czasy.
Cały zestaw odbywa się dwukrotnie, raz w -client
trybie, a drugi w -server
trybie.
WNIOSKI
tryb klienta (32 bity)
W przypadku ciągów o długości od 1 do 256 znaków wywołanie string.charAt(i)
wygrywa ze średnim przetwarzaniem od 13,4 do 588 milionów znaków na sekundę.
Ponadto jest ogólnie 5,5% szybszy (klient) i 13,9% (serwer) w następujący sposób:
for (int i = 0; i < data.length(); i++) {
if (data.charAt(i) <= ' ') {
doThrow();
}
}
niż w ten sposób z lokalną zmienną końcową długości:
final int len = data.length();
for (int i = 0; i < len; i++) {
if (data.charAt(i) <= ' ') {
doThrow();
}
}
W przypadku długich ciągów znaków o długości od 512 do 256 K , użycie odbicia w celu uzyskania dostępu do tablicy zaplecza String jest najszybsze. Ta technika jest prawie dwa razy szybsza niż String.charAt (i) (o 178% szybciej). Średnia prędkość w tym zakresie wynosiła 1,111 miliarda znaków na sekundę.
Pole należy uzyskać z wyprzedzeniem, a następnie można je ponownie wykorzystać w bibliotece na różnych ciągach. Co ciekawe, w przeciwieństwie do powyższego kodu, w przypadku dostępu do pola, posiadanie lokalnej zmiennej końcowej długości jest o 9% szybsze niż użycie „chars.length” w sprawdzaniu pętli. Oto jak najszybciej można skonfigurować dostęp do pola:
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);
}
Specjalne komentarze na temat trybu -server
Dostęp do pola zaczyna się wygrywać po 32-bitowych ciągach znaków w trybie serwera na 64-bitowej maszynie Java na moim komputerze AMD 64. Nie było to widoczne do długości 512 znaków w trybie klienta.
Warto również zauważyć, że myślę, że kiedy uruchamiałem JDK 8 (kompilacja 32-bitowa) w trybie serwera, ogólna wydajność była o 7% wolniejsza zarówno dla dużych, jak i małych ciągów. Było to w przypadku wczesnego wydania JDK 8 z kompilacją 121 grudnia 2013 r. Na razie wydaje się, że tryb serwera 32-bitowego jest wolniejszy niż tryb klienta 32-bitowego.
Biorąc to pod uwagę ... wydaje się, że jedynym trybem serwera, który warto wywołać, jest maszyna 64-bitowa. W przeciwnym razie faktycznie ogranicza wydajność.
W przypadku wersji 32-bitowej działającej -server mode
na AMD64 mogę powiedzieć:
- String.charAt (i) jest wyraźnym zwycięzcą. Chociaż między rozmiarami od 8 do 512 znaków byli zwycięzcy w kategorii „nowe” „ponowne wykorzystanie” i „pole”.
- String.charAt (i) działa o 45% szybciej w trybie klienta
- Dostęp do pola jest dwukrotnie szybszy w przypadku dużych ciągów znaków w trybie klienta.
Warto również powiedzieć, że String.chars () (Stream i wersja równoległa) to bust. O wiele wolniej niż w jakikolwiek inny sposób. Streams
API jest raczej powolny sposób, aby wykonywać operacje ogólnie ciągów.
Lista życzeń
Java String może mieć predykat akceptujący zoptymalizowane metody, takie jak include (predykat), forEach (konsument), forEachWithIndex (konsument). Tak więc, bez konieczności znajomości przez użytkownika długości lub powtarzania wywołań metod String, może to beep-beep beep
przyspieszyć analizowanie bibliotek .
Marz dalej :)
Happy Strings!
~ SH
W teście wykorzystano 9 następujących metod testowania ciągu znaków na obecność białych znaków:
„charAt1” - SPRAWDŹ ZAWARTOŚĆ STRINGU W ZWYKŁY SPOSÓB:
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” - TAKIE SAME, JAK POWYŻEJ, ALE UŻYWAJ String.length () ZAMIAST TWORZENIA OSTATECZNEJ LOKALNEJ int NA DŁUGOŚĆ
int charAtMethod2(final String data) {
for (int i = 0; i < data.length(); i++) {
if (data.charAt(i) <= ' ') {
doThrow();
}
}
return data.length();
}
„stream” - UŻYJ NOWEJ JAVA-8 String's IntStream I PRZEKAŻ PROGNOZĘ DO SPRAWDZENIA
int streamMethod(final String data, final IntPredicate predicate) {
if (data.chars().anyMatch(predicate)) {
doThrow();
}
return data.length();
}
"streamPara" - TAKIE SAMO JAK POWYŻEJ, ALE OH-LA-LA - JEDŹ RÓWNOLEGLE !!!
// avoid this at all costs
int streamParallelMethod(final String data, IntPredicate predicate) {
if (data.chars().parallel().anyMatch(predicate)) {
doThrow();
}
return data.length();
}
„reuse” - WYPEŁNIJ WIELOKROTNEGO UŻYTKU [] ZAWARTOŚCIĄ STRINGS
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" - UZYSKAJ NOWĄ KOPIĘ ZNAKU [] Z STRUNU
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;
}
„nowy2” - TAKIE SAME JAK POWYŻEJ, ALE UŻYWAJ „DLA KAŻDEGO”
int newMethod2(final String data) {
for (final char c : data.toCharArray()) {
if (c <= ' ') {
doThrow();
}
}
return data.length();
}
"field1" - FANCY !! UZYSKAJ POLE DOSTĘPU DO WEWNĘTRZNEGO ZNAKU STRUNY []
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” - TAKIE SAME JAK POWYŻEJ, ALE UŻYJ „DLA KAŻDEGO”
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;
}
WYNIKI KOMPOZYTOWE DLA -client
TRYBU KLIENTA (łącznie testy w przód i wstecz)
Uwaga: tryb -client z 32-bitową Javą i -server z 64-bitową Javą jest taki sam, jak poniżej na moim komputerze 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
WYNIKI KOMPOZYTOWE DLA -server
TRYBU SERWEROWEGO (łącznie testy w przód i wstecz)
Uwaga: to jest test dla 32-bitowej wersji Java działającej w trybie serwera na AMD64. Tryb serwera dla 64-bitowej Javy był taki sam, jak dla 32-bitowej Javy w trybie klienta, z wyjątkiem tego, że dostęp do pola zaczyna wygrywać po rozmiarze 32 znaków.
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
PEŁNY URUCHOMIONY KOD PROGRAMU
(aby przetestować na Javie 7 i wcześniejszych, usuń testy dwóch strumieni)
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)
?