Ответ 1
ПЕРВЫЙ ОБНОВЛЕНИЕ: Прежде чем пытаться это сделать в производственной среде (не рекомендуется), прочтите это сначала: http://www.javaspecialists.eu/archive/Issue237.html Начиная с Java 9, решение, как описано, больше не будет работать, потому что теперь Java будет хранить строки как byte [] по умолчанию.
ВТОРОЕ ОБНОВЛЕНИЕ: по состоянию на 2016-10-25, на моем AMDx64 8core и источнике 1.8, нет никакой разницы между использованием "charAt" и доступом к полю. Похоже, что jvm достаточно оптимизирован для встроенного и упорядочения любых вызовов 'string.charAt(n)'.
Все зависит от длины проверяемого String
. Если, как говорится в этом вопросе, для строк long, самый быстрый способ проверки строки - использовать отражение для доступа к поддержке char[]
строки.
Полностью рандомизированный тест с JDK 8 (win32 и win64) на 64-разрядном процессоре AMD Phenom II 4 955 @3,2 ГГц (как в режиме клиента, так и в режиме сервера) с 9 различными технологиями (см. ниже!) показывает, что с использованием String.charAt(n)
является самым быстрым для небольших строк и что использование reflection
для доступа к массиву поддержки String почти в два раза быстрее для больших строк.
ЭКСПЕРИМЕНТ
-
Пробуются 9 различных методов оптимизации.
-
Все содержимое строки рандомизировано
-
Тест выполняется для строковых размеров в двоях, начинающихся с 0,1,2,4,8,16 и т.д.
-
Тесты выполняются 1000 раз за размер строки
-
Тесты перетасовываются в случайный порядок каждый раз. Другими словами, тесты выполняются в случайном порядке каждый раз, когда они выполняются, более чем в 1000 раз.
-
Весь набор тестов выполняется вперед и назад, чтобы показать эффект прогрева 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 до 256 тыс. символов, использование рефлекса для доступа к массиву поддержки строк является самым быстрым. Этот метод почти в два раза быстрее, чем 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 символа в режиме сервера на 64-битной машине Java на моем компьютере 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% быстрее в клиентском режиме
- Доступ к полям в два раза быстрее для больших строк в клиентском режиме.
Также стоит сказать, что String.chars() (Stream и параллельная версия) являются бюстом. Путь медленнее, чем любой другой способ. API Streams
является довольно медленным способом выполнения общих операций с строками.
Список пожеланий
Java String может иметь предикат, принимающий оптимизированные методы, такие как содержит (предикат), forEach (потребитель), forEachWithIndex (потребитель). Таким образом, без необходимости для пользователя знать длину или повторять вызовы для методов String, это может помочь разбору библиотек beep-beep beep
speedup.
Продолжайте мечтать:)
Счастливые строки!
~ 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 ДЛЯ LENGTh
int charAtMethod2(final String data) {
for (int i = 0; i < data.length(); i++) {
if (data.charAt(i) <= ' ') {
doThrow();
}
}
return data.length();
}
"stream" - ИСПОЛЬЗУЙТЕ НОВЫЙ JVA-8 String IntStream И ПРОПУСКАЙТЕ ПРОВЕРКУ, ЧТОБЫ ПРОВЕРИТЬ
int streamMethod(final String data, final IntPredicate predicate) {
if (data.chars().anyMatch(predicate)) {
doThrow();
}
return data.length();
}
"streamPara" - ТАК КАК ВЫШЕ, НО ОН-ЛА-ЛА - ИДЕТ ПАРАЛЛЕЛЬНО!!!
// avoid this at all costs
int streamParallelMethod(final String data, IntPredicate predicate) {
if (data.chars().parallel().anyMatch(predicate)) {
doThrow();
}
return data.length();
}
"повторное использование" - ЗАВЕРШИТЬ ВОЗВРАЩАЕМЫЙ char [] С СОДЕРЖАНИЕМ 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" - ПОЛУЧИТЬ НОВУЮ КОПИЮ char [] ИЗ СТРОКИ
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!! ПОЛУЧИТЬ ПОЛЕ ДЛЯ ДОСТУПА К СТРУНУ ВНУТРЕННИМ 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);
}
}
"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
MODE (комбинированные комбинации вперед и назад)
Примечание: режим -client с 32-разрядным и 32-разрядным режимами Java с 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
MODE (комбинированные комбинации вперед и назад)
Примечание. Это тест для 32-разрядной версии Java в режиме сервера на AMD64. Режим сервера для Java-64-бит был таким же, как Java 32-разрядный в клиентском режиме, за исключением того, что начальный выигрыш в Field получался после 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 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 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("");
}
}