Самый быстрый способ удалить все непечатаемые символы из строки Java
Каков самый быстрый способ удалить все непечатаемые символы из String
в Java?
До сих пор я пробовал и измерял 138-байтовую строку с 131 символом:
- String
replaceAll()
- самый медленный метод
- Предварительно скопируйте шаблон, затем используйте Matcher
replaceAll()
- Используйте StringBuffer, получайте кодовые страницы с помощью
codepointAt()
один за другим и добавьте в StringBuffer
- Используйте StringBuffer, получайте символы, используя
charAt()
один за другим и добавляйте к StringBuffer
- Предопределить буфер
char[]
, получить символы, используя charAt()
один за другим и заполнить этот буфер, а затем преобразовать обратно в String
- Предопределить 2
char[]
буфера - старые и новые, получить все символы для существующей строки сразу с помощью getChars()
, поочередно перебирать старый буфер и заполнять новый буфер, а затем конвертировать новый буфер в String - моя самая быстрая версия
- Тот же материал с двумя буферами - только с использованием
byte[]
, getBytes()
и с указанием кодировки как "utf-8"
- Тот же материал с 2 буферами
byte[]
, но определяющий кодировку как константу Charset.forName("utf-8")
- Тот же материал с 2 буферами
byte[]
, но определяющий кодировку как 1-байтовую локальную кодировку (едва ли разумную вещь)
Моя лучшая попытка заключалась в следующем:
char[] oldChars = new char[s.length()];
s.getChars(0, s.length(), oldChars, 0);
char[] newChars = new char[s.length()];
int newLen = 0;
for (int j = 0; j < s.length(); j++) {
char ch = oldChars[j];
if (ch >= ' ') {
newChars[newLen] = ch;
newLen++;
}
}
s = new String(newChars, 0, newLen);
Любые мысли о том, как сделать это еще быстрее?
Бонусные баллы для ответа на очень странный вопрос: почему использование имени набора символов "utf-8" напрямую дает лучшую производительность, чем использование предварительно назначенного static const Charset.forName("utf-8")
?
Update
- Предложение от уловов с храповым механизмом дает впечатляющие результаты 3105590/сек, улучшение на +24%!
- Предложение от Ed Staub дает еще одно улучшение - 3471017 результатов/сек, + 12% по сравнению с предыдущим лучшим.
Обновление 2
Я изо всех сил старался собрать все предлагаемые решения и перекрестные мутации и опубликовал их как небольшую платформу для сравнения производительности в github . В настоящее время он поддерживает 17 алгоритмов. Один из них - "специальный" - алгоритм Voo1 (, предоставленный SO user Voo) использует сложные уловки отражения, тем самым достигая звездных скоростей, но он испортил состояние строк JVM, он сравнивается отдельно.
Вы можете проверить его и запустить, чтобы определить результаты на вашем поле. Здесь резюме результатов, которые я получил на моем. Он специфицирует:
- Debian sid
- Linux 2.6.39-2-amd64 (x86_64)
- Java, установленная из пакета
sun-java6-jdk-6.24-1
, JVM идентифицирует себя как
- Java (TM) SE Runtime Environment (сборка 1.6.0_24-b07)
- 64-разрядная серверная виртуальная машина Java HotSpot TM (сборка 19.1-b02, смешанный режим)
Различные алгоритмы показывают в конечном счете разные результаты, учитывая другой набор входных данных. Я провел тест в трех режимах:
Одинаковая строка
Этот режим работает в одной и той же строке, предоставляемой классом StringSource
как константа. Разборки:
Ops / s │ Algorithm
──────────┼──────────────────────────────
6 535 947 │ Voo1
──────────┼──────────────────────────────
5 350 454 │ RatchetFreak2EdStaub1GreyCat1
5 249 343 │ EdStaub1
5 002 501 │ EdStaub1GreyCat1
4 859 086 │ ArrayOfCharFromStringCharAt
4 295 532 │ RatchetFreak1
4 045 307 │ ArrayOfCharFromArrayOfChar
2 790 178 │ RatchetFreak2EdStaub1GreyCat2
2 583 311 │ RatchetFreak2
1 274 859 │ StringBuilderChar
1 138 174 │ StringBuilderCodePoint
994 727 │ ArrayOfByteUTF8String
918 611 │ ArrayOfByteUTF8Const
756 086 │ MatcherReplace
598 945 │ StringReplaceAll
460 045 │ ArrayOfByteWindows1251
В набросках:
Такая же строчная диаграмма http://www.greycat.ru/img/os-chart-single.png
Несколько строк, 100% строк содержат управляющие символы
Поставщик исходной строки предварительно сгенерировал множество случайных строк с использованием набора символов (0..127) - таким образом, почти все строки содержали хотя бы один управляющий символ. Алгоритмы получили строки из этого предварительно сгенерированного массива в циклическом режиме.
Ops / s │ Algorithm
──────────┼──────────────────────────────
2 123 142 │ Voo1
──────────┼──────────────────────────────
1 782 214 │ EdStaub1
1 776 199 │ EdStaub1GreyCat1
1 694 628 │ ArrayOfCharFromStringCharAt
1 481 481 │ ArrayOfCharFromArrayOfChar
1 460 067 │ RatchetFreak2EdStaub1GreyCat1
1 438 435 │ RatchetFreak2EdStaub1GreyCat2
1 366 494 │ RatchetFreak2
1 349 710 │ RatchetFreak1
893 176 │ ArrayOfByteUTF8String
817 127 │ ArrayOfByteUTF8Const
778 089 │ StringBuilderChar
734 754 │ StringBuilderCodePoint
377 829 │ ArrayOfByteWindows1251
224 140 │ MatcherReplace
211 104 │ StringReplaceAll
В набросках:
Несколько строк, концентрация 100% http://www.greycat.ru/img/os-chart-multi100.png
Несколько строк, 1% строк содержат управляющие символы
То же, что и предыдущее, но только 1% строк было сгенерировано с помощью управляющих символов - остальные 99% были сгенерированы при использовании набора символов [32..127], поэтому они не могли содержать управляющие символы вообще. Эта синтетическая нагрузка ближе всего подходит для реального применения этого алгоритма у меня.
Ops / s │ Algorithm
──────────┼──────────────────────────────
3 711 952 │ Voo1
──────────┼──────────────────────────────
2 851 440 │ EdStaub1GreyCat1
2 455 796 │ EdStaub1
2 426 007 │ ArrayOfCharFromStringCharAt
2 347 969 │ RatchetFreak2EdStaub1GreyCat2
2 242 152 │ RatchetFreak1
2 171 553 │ ArrayOfCharFromArrayOfChar
1 922 707 │ RatchetFreak2EdStaub1GreyCat1
1 857 010 │ RatchetFreak2
1 023 751 │ ArrayOfByteUTF8String
939 055 │ StringBuilderChar
907 194 │ ArrayOfByteUTF8Const
841 963 │ StringBuilderCodePoint
606 465 │ MatcherReplace
501 555 │ StringReplaceAll
381 185 │ ArrayOfByteWindows1251
В набросках:
Несколько строк, концентрация 1% http://www.greycat.ru/img/os-chart-multi1.png
Мне очень трудно решить, кто дал лучший ответ, но, учитывая реальное приложение, лучшее решение было дано/вдохновлено Эд Штаубом, я думаю, было бы справедливо отметить его ответ. Спасибо всем, кто принял участие в этом, ваш вклад был очень полезным и бесценным. Не стесняйтесь запускать тестовый пакет на вашем ящике и предлагать еще лучшие решения (работая над решением JNI, кто-нибудь?).
Ссылки
Ответы
Ответ 1
Если разумно внедрить этот метод в класс, который не разделяется нитями, тогда вы можете повторно использовать буфер:
char [] oldChars = new char[5];
String stripControlChars(String s)
{
final int inputLen = s.length();
if ( oldChars.length < inputLen )
{
oldChars = new char[inputLen];
}
s.getChars(0, inputLen, oldChars, 0);
и т.д...
Это большой выигрыш - 20% или около того, так как я понимаю текущий лучший случай.
Если это нужно использовать для потенциально больших строк, и проблема с утечкой памяти вызывает беспокойство, может быть использована слабая ссылка.
Ответ 2
с использованием массива 1 char может работать немного лучше
int length = s.length();
char[] oldChars = new char[length];
s.getChars(0, length, oldChars, 0);
int newLen = 0;
for (int j = 0; j < length; j++) {
char ch = oldChars[j];
if (ch >= ' ') {
oldChars[newLen] = ch;
newLen++;
}
}
s = new String(oldChars, 0, newLen);
и я избегал повторных вызовов s.length();
другая микро-оптимизация, которая может работать,
int length = s.length();
char[] oldChars = new char[length+1];
s.getChars(0, length, oldChars, 0);
oldChars[length]='\0';//avoiding explicit bound check in while
int newLen=-1;
while(oldChars[++newLen]>=' ');//find first non-printable,
// if there are none it ends on the null char I appended
for (int j = newLen; j < length; j++) {
char ch = oldChars[j];
if (ch >= ' ') {
oldChars[newLen] = ch;//the while avoids repeated overwriting here when newLen==j
newLen++;
}
}
s = new String(oldChars, 0, newLen);
Ответ 3
Хорошо, я избил текущий лучший метод (freak solution with preallocated array) примерно на 30% в соответствии с моими мерами. Как? Продавая мою душу.
Как я уверен, все, кто следил за обсуждением до сих пор, знают, что это почти полностью нарушает принцип базового программирования, но хорошо. В любом случае, следующее работает только в том случае, если используемый массив символов строки не используется совместно с другими строками - если он делает, тот, кто должен отлаживать это, будет иметь все права, чтобы убить вас (без вызовов подстроки() и использования этого в литеральных строках это должно работать, поскольку я не понимаю, почему JVM будет ставить уникальные строки, прочитанные из внешнего источника). Хотя не забудьте удостовериться, что эталонный код не делает этого - это очень вероятно и поможет решению отражения, очевидно.
В любом случае мы идем:
// Has to be done only once - so cache those! Prohibitively expensive otherwise
private Field value;
private Field offset;
private Field count;
private Field hash;
{
try {
value = String.class.getDeclaredField("value");
value.setAccessible(true);
offset = String.class.getDeclaredField("offset");
offset.setAccessible(true);
count = String.class.getDeclaredField("count");
count.setAccessible(true);
hash = String.class.getDeclaredField("hash");
hash.setAccessible(true);
}
catch (NoSuchFieldException e) {
throw new RuntimeException();
}
}
@Override
public String strip(final String old) {
final int length = old.length();
char[] chars = null;
int off = 0;
try {
chars = (char[]) value.get(old);
off = offset.getInt(old);
}
catch(IllegalArgumentException e) {
throw new RuntimeException(e);
}
catch(IllegalAccessException e) {
throw new RuntimeException(e);
}
int newLen = off;
for(int j = off; j < off + length; j++) {
final char ch = chars[j];
if (ch >= ' ') {
chars[newLen] = ch;
newLen++;
}
}
if (newLen - off != length) {
// We changed the internal state of the string, so at least
// be friendly enough to correct it.
try {
count.setInt(old, newLen - off);
// Have to recompute hash later on
hash.setInt(old, 0);
}
catch(IllegalArgumentException e) {
e.printStackTrace();
}
catch(IllegalAccessException e) {
e.printStackTrace();
}
}
// Well we have to return something
return old;
}
Для моей тестовой строки, которая получает 3477148.18ops/s
vs. 2616120.89ops/s
для старого варианта. Я вполне уверен, что единственный способ победить это может заключаться в том, чтобы написать его на C (возможно, не так) или какой-то совершенно другой подход, о котором никто не думал. Хотя я абсолютно не уверен, что время стабильное на разных платформах - по крайней мере, дает надежные результаты на моем ящике (Java7, Win7 x64).
Ответ 4
Вы можете разделить задачу на несколько параллельных подзадач, в зависимости от количества процессоров.
Ответ 5
IANA низкоуровневый java-уровень junkie, но вы пробовали разворачивать свой основной цикл? Похоже, что он может позволить некоторым процессорам выполнять проверки параллельно.
Кроме того, этот содержит интересные идеи для оптимизации.
Ответ 6
Я был настолько свободен и написал небольшой ориентир для разных алгоритмов. Это не идеально, но я беру минимум 1000 прогонов заданного алгоритма 10000 раз по случайной строке (по умолчанию 32/200% непечатаемых). Это должно заботиться о таких вещах, как GC, инициализация и т.д. - не так много накладных расходов, что любой алгоритм не должен иметь хотя бы один прогон без особых помех.
Не особенно хорошо документировано, но хорошо. Здесь мы идем - я включил оба алгоритма с храповым уродством и базовую версию. В настоящий момент я произвольно инициализирую длинную строку длиной 200 символов с равномерно распределенными символами в диапазоне [0, 200].
Ответ 7
почему использование имени набора символов "utf-8" напрямую дает лучшую производительность, чем использование предварительно заданного статического const Charset.forName( "utf-8" )?
Если вы имеете в виду String#getBytes("utf-8")
и т.д.: Это не должно быть быстрее - за исключением некоторого лучшего кэширования - поскольку Charset.forName("utf-8")
используется внутри, если кодировка не кэшируется.
Можно сказать, что вы используете разные кодировки (или, может быть, некоторые из ваших кодов прозрачны), но кодировка в кеше в StringCoding
не изменяется.