ПЕРВОЕ ОБНОВЛЕНИЕ: прежде чем пытаться делать это когда-либо в производственной среде (не рекомендуется), сначала прочтите это: http://www.javaspecialists.eu/archive/Issue237.html
Начиная с Java 9, описанное решение больше не будет работать потому что теперь Java будет хранить строки как byte [] по умолчанию.
ВТОРОЕ ОБНОВЛЕНИЕ: По состоянию на 2016-10-25, на моем 8xore AMDx64 и источнике 1.8 нет никакой разницы между использованием 'charAt' и доступом к полю. Похоже, что jvm достаточно оптимизирован, чтобы встроить и оптимизировать любые вызовы 'string.charAt (n)'.
Все зависит от длины String
проверяемого. Если, как говорится в вопросе, это для длинных строк, самый быстрый способ проверить строку - это использовать отражение для доступа к поддержке char[]
строки.
Полностью рандомизированный эталонный тест с JDK 8 (win32 и win64) на 64-ядерном ядре Phenom II 4 AMD 955 @ 3,2 ГГц (как в режиме клиента, так и в режиме сервера) с 9 различными методами (см. Ниже!) Показывает, что использование String.charAt(n)
является самым быстрым для малых строк и что использование reflection
для доступа к массиву поддержки строк почти вдвое быстрее для больших строк.
ЭКСПЕРИМЕНТ
Испробованы 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 до 256K символов наиболее быстрым является использование отражения для доступа к массиву поддержки String. Этот метод почти в два раза быстрее 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);
}
Специальные комментарии по режиму -server
Доступ к полю начинает выигрывать после 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 и параллельная версия) - это перебор. Намного медленнее, чем любой другой способ. Streams
API является довольно медленным способом для выполнения операций над строками вообще.
Список желаний
Java String может иметь предикат, принимающий оптимизированные методы, такие как содержит (предикат), forEach (потребитель), forEachWithIndex (потребитель). Таким образом, если пользователю не нужно знать длину или повторять вызовы методов 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" - ЖЕ, КАК ВЫШЕ, НО ИСПОЛЬЗОВАТЬ
int charAtMethod2(final String data) {
for (int i = 0; i < data.length(); i++) {
if (data.charAt(i) <= ' ') {
doThrow();
}
}
return data.length();
}
"stream" - ИСПОЛЬЗУЙТЕ НОВУЮ СТРОКУ JAVA-8 и ПРОЙДИТЕ ЭТО ПРЕДИКАТ ДЛЯ ПРОВЕРКИ
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();
}
"reuse" - ЗАПОЛНИТЬ ОБРАЗУЮЩИЙСЯ ЧАР
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 !! ПОЛУЧИТЕ ПОЛЕ для доступа к внутреннему символу 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" - ТАК ЖЕ ВЫШЕ, НО ИСПОЛЬЗУЙТЕ "ДЛЯ КАЖДОГО"
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 с 32-битным Java и режим -server с 64-битным Java такой же, как показано ниже на моей машине с 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
РЕЖИМА СЕРВЕРА (комбинированные тесты вперед и назад)
Примечание: это тест для 32-битной Java, работающей в режиме сервера на AMD64. Режим сервера для 64-битной Java был таким же, как 32-битная Java в режиме клиента, за исключением того, что доступ к полю начинался с выигрыша после 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)
?