it-swarm.com.ru

Самый быстрый способ убрать все непечатаемые символы из строки Java

Какой самый быстрый способ убрать все непечатаемые символы из String в Java?

До сих пор я пытался измерить 138-байтовую строку из 131 символа:

  • Строка replaceAll() - самый медленный метод
    • 517009 результатов/сек
  • Прекомпилируйте шаблон, затем используйте функцию replaceAll().__ из Matcher.
    • 637836 результатов/сек
  • Используйте StringBuffer, получайте кодовые точки, используя codepointAt() один за другим, и добавляйте StringBuffer
    • 711946 результатов/сек
  • Используйте StringBuffer, получайте символы, используя charAt() один за другим, и добавляйте StringBuffer
    • 1052964 результатов/сек
  • Предварительно выделите буфер char[], поочередно получите символы с помощью charAt() и заполните этот буфер, а затем преобразуйте обратно в String
    • 2022653 результатов/сек
  • Предварительно выделите 2 буфера char[] - старые и новые, получите все символы для существующей строки, используя getChars(), переберите старый буфер один за другим и заполните новый буфер, затем преобразуйте новый буфер в строку - моя собственная самая быстрая версия
    • 2502502 результатов/сек
  • То же самое с 2 буферами - только с использованием byte[], getBytes() и указанием кодировки как "utf-8"
    • 857485 результатов/сек
  • То же самое с 2 буферами byte[], но с указанием кодировки как константы Charset.forName("utf-8")
    • 791076 результатов/сек
  • То же самое с 2 буферами byte[], но с указанием кодировки в качестве 1-байтовой локальной кодировки (едва ли это нормальное занятие)
    • 370164 результатов/сек

Моя лучшая попытка была следующей:

    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" напрямую дает лучшую производительность, чем использование предварительно выделенной статической константы Charset.forName("utf-8")?

Обновление

  • Предложение от ratchet freak дает впечатляющую производительность 3105590 в секунду, улучшение + 24%!
  • Предложение от Ed Staub дает еще одно улучшение - 3471017 результатов/сек, + 12% по сравнению с предыдущим лучшим.

Обновление 2

Я старался изо всех сил собрать все предложенные решения и их кросс-мутации и опубликовал их как небольшую платформу для тестирования производительности на github . В настоящее время в нем 17 алгоритмов. Одним из них является «специальный» алгоритм - Voo1 ( предоставленный SO пользователем 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) VM (сборка 19.1-b02, смешанный режим)

Разные алгоритмы показывают совершенно разные результаты при разном наборе входных данных. Я провел тест в 3 режимах:

Одна и та же строка

Этот режим работает с одной строкой, предоставленной классом StringSource как константа. Вскрытие является:

 Ops/s │ Алгоритм 
 ──────────┼───────────────────────────────────
 6 535 947 │ Voo1 
 ──────────┼────────────────────────────────── ─ 
 5 350 454 │ RatchetFreak2EdStaub1GreyCat1 
 5 249 343 │ EdStaub1 
 5 002 501 │ EdStaub1GreyCat1 
 4 859 086 │ ArrayOfCharFromStringCharAhar
 2 790 178 │ RatchetFreak2EdStaub1GreyCat2 
 2 583 311 │ RatchetFreak2 
 1 274 859 │ StringBuilderChar 
 1 138 174 │ StringBuilderCodePoint 
 994 727 │ ArrayOfByteUTF8String 
 918 611 ray ArrayOfByteUTF8Const 
 756 086 │ MatcherReplace 
 598 945 │ StringReplaceAll 
 460 045 │ ArrayOfByteWindows1251 

В виде карты: Же однострочный график http://www.greycat.ru/img/os-chart-single.png

Несколько строк, 100% строк содержат управляющие символы

Поставщик исходной строки предварительно сгенерировал множество случайных строк, используя набор символов (0..127) - таким образом, почти все строки содержали хотя бы один управляющий символ. Алгоритмы получали строки из этого предварительно сгенерированного массива циклически.

 Ops/s │ Алгоритм 
 ──────────┼───────────────────────────────────
 2 123 142 │ Voo1 
 ──────────┼────────────────────────────────── ─ 
 1 782 214 │ EdStaub1 
 1 776 199 │ EdStaub1GreyCat1 
 1 694 628 │ ArrayOfCharFromStringCharAt 
 1 481 481 │ ArrayOfCharFromArrayOfCharChat1
 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 │ Алгоритм ──────────┼─────────────────────────────────── 3 711 952 │ Voo1 ──────────┼────────────────────────────────── ─ 2 851 440 │ EdStaub1GreyCat1 2 455 796 │ EdStaub1 2 426 007 │ ArrayOfCharFromStringCharAt 2 347 969 │ RatchetFreak2EdStaub1GreyCat2 .2F2 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 ray ArrayOfByteWindows1251

В виде карты: 
 Несколько строк, 1% концентрация http://www.greycat.ru/img/os-chart-multi1.png

Мне очень трудно определиться с тем, кто дал лучший ответ, но, учитывая, что лучшее решение для реального приложения было дано/вдохновлено Эдом Стаубом, я думаю, было бы справедливо отметить его ответ. Спасибо всем, кто принял участие в этом, ваш вклад был очень полезным и неоценимым. Не стесняйтесь запускать набор тестов на своем компьютере и предлагать еще лучшие решения (рабочий JNI-решение, любой?).

Рекомендации.

76
GreyCat

Если разумно встроить этот метод в класс, который не является общим для всех потоков, вы можете повторно использовать буфер: 

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%, как я понимаю, в лучшем случае.

Если это должно использоваться на потенциально больших строках, и «утечка» памяти является проблемой, можно использовать слабую ссылку.

9
Ed Staub

использование массива 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);
24
ratchet freak

Ну, я побил текущий лучший метод (решение freak с предварительно выделенным массивом) примерно на 30% по моим меркам. Как? Продав свою душу.

Я уверен, что все, кто следил за обсуждением до сих пор, знают, что это в значительной степени нарушает любой базовый принцип программирования, ну да ладно. В любом случае следующее работает только в том случае, если используемый массив символов строки не используется совместно для других строк - если он это сделает, тот, кто должен отладить, будет иметь полное право принять решение убить вас (без вызовов substring () и использовать это для литеральных строк это должно работать, так как я не понимаю, почему 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 против 2616120.89ops/s для старого варианта. Я вполне уверен, что единственный способ победить это написать на C (возможно, нет) или совершенно другой подход, о котором никто до сих пор не думал. Хотя я абсолютно не уверен, стабильно ли время на разных платформах - по крайней мере, дает надежные результаты на моем компьютере (Java7, Win7 x64).

9
Voo

Вы можете разбить задачу на несколько параллельных подзадач, в зависимости от количества процессоров.

2
umbr

IANA, низкоуровневый фанат производительности Java, но вы пытались развернуть свой основной цикл ? Похоже, что это может позволить некоторым процессорам выполнять проверки параллельно.

Кроме того, это есть несколько забавных идей для оптимизации.

1
Ryan Ransford

Я был настолько свободен и написал небольшой тест для разных алгоритмов. Это не идеально, но я беру не менее 1000 запусков данного алгоритма 10000 раз по случайной строке (по умолчанию около 32/200% не для печати). Это должно позаботиться о таких вещах, как GC, инициализация и т.д. - не так много служебной информации, что ни у одного алгоритма не должно быть хотя бы одного запуска без особых помех.

Не особенно хорошо задокументировано, ну да ладно. Здесь мы идем - Я включил как алгоритмы трещотки уродов и базовую версию. В данный момент я случайным образом инициализирую строку длиной 200 символов с равномерно распределенными символами в диапазоне [0, 200).

1
Voo

почему использование имени кодировки "utf-8" напрямую дает лучшую производительность, чем использование предварительно выделенного статического константы Charset.forName ("utf-8")?

Если вы имеете в виду String#getBytes("utf-8") и т.д. Это не должно быть быстрее - за исключением некоторого лучшего кэширования - поскольку Charset.forName("utf-8") используется внутри, если кодировка не кэшируется.

Одна вещь может заключаться в том, что вы используете разные кодировки (или, возможно, некоторые из ваших кодов делают это прозрачно), но кодировка, кэшированная в StringCoding, не меняется.

0
Thomas