Найшвидший спосіб вилучити всі недруковані символи з Java String


82

Який найшвидший спосіб позбавити всіх символів, що не друкуються, з StringJava?

Поки що я пробував і вимірював 138-байтовий, 131-символьний рядок:

  • String's replaceAll()- найповільніший метод
    • 517009 результати / сек
  • Попередньо скомпілюйте шаблон, а потім використовуйте Matcher's replaceAll()
    • 637836 результатів / сек
  • Використовуйте StringBuffer, отримуйте кодові точки, використовуючи codepointAt()один за одним, та додайте до StringBuffer
    • 711946 результатів / сек
  • Використовуйте StringBuffer, отримуйте символи, використовуючи charAt()один за одним, та додайте до StringBuffer
    • 1052964 результати / сек
  • Попередньо розподіліть char[]буфер, отримайте символи, використовуючи charAt()один за одним, і заповніть цей буфер, а потім перетворіть назад у рядок
    • 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" безпосередньо дає кращу продуктивність, ніж використання попередньо виділеного статичного const Charset.forName("utf-8")?

Оновлення

  • Пропозиція від храпового чудака дає вражаючу продуктивність 3105590 результатів / сек, покращення + 24%!
  • Пропозиція Еда Стауба дає ще одне поліпшення - 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 (збірка 1.6.0_24-b07)
    • 64-розрядна віртуальна машина Java HotSpot (TM) (збірка 19.1-b02, змішаний режим)

Різні алгоритми показують в кінцевому підсумку різні результати з огляду на різний набір вихідних даних. Я провів орієнтир у 3 режимах:

Той самий рядок

Цей режим працює на тому самому рядку, який StringSourceклас надає як константу. Розбірка:

 Ops / s │ Алгоритм
──────────┼──────────────────────────────
6 535 947 │ Voo1
──────────┼──────────────────────────────
5350454 │ RatchetFreak2EdStaub1GreyCat1
5 249 343 │ EdStaub1
5002501 │ EdStaub1GreyCat1
4 859 086 │ ArrayOfCharFromStringCharAt
4 295 532 │ RatchetFreak1
4 045 307 │ ArrayOfCharFromArrayOfChar
2790178 │ RatchetFreak2EdStaub1GreyCat2
2 583 311 │ RatchetFreak2
1 274 859 │ StringBuilderChar
1 138 174 │ StringBuilderCodePoint
  994 727 │ ArrayOfByteUTF8String
  918 611 │ ArrayOfByteUTF8Const
  756 086 │ MatcherReplace
  598945 │ StringReplaceAll
  460 045 │ ArrayOfByteWindows1251

У графічній формі: (джерело: greycat.ru )Та сама одиночна діаграма

Кілька рядків, 100% рядків містять контрольні символи

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

 Ops / s │ Алгоритм
──────────┼──────────────────────────────
2 123 142 │ Voo1
──────────┼──────────────────────────────
1 782 214 │ EdStaub1
1 776 199 │ EdStaub1GreyCat1
1669628 │ ArrayOfCharFromStringCharAt
1 481481 │ ArrayOfCharFromArrayOfChar
1 460 067 │ RatchetFreak2EdStaub1GreyCat1
1 438 435 │ RatchetFreak2EdStaub1GreyCat2
1 366 494 │ RatchetFreak2
1 349 710 │ RatchetFreak1
  893 176 │ ArrayOfByteUTF8String
  817 127 │ ArrayOfByteUTF8Const
  778899 │ StringBuilderChar
  734 754, StringBuilderCodePoint
  377 829 │ ArrayOfByteWindows1251
  224 140 │ MatcherReplace
  211 104 │ StringReplaceAll

У графічній формі: (джерело: greycat.ru )Кілька струн, концентрація 100%

Кілька рядків, 1% рядків містять контрольні символи

Так само, як і попередні, але лише 1% рядків було сформовано з керуючими символами - інші 99% було створено за допомогою набору символів [32..127], тому вони взагалі не могли містити контрольні символи. Це синтетичне навантаження найближче до реального застосування цього алгоритму у мене.

 Ops / s │ Алгоритм
──────────┼──────────────────────────────
3 711 952 │ Voo1
──────────┼──────────────────────────────
2 851440 │ EdStaub1GreyCat1
2 455 796 │ EdStaub1
2 426 007 │ ArrayOfCharFromStringCharAt
2 347969 │ RatchetFreak2EdStaub1GreyCat2
2224152 │ 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
  501555 │ StringReplaceAll
  381 185 │ ArrayOfByteWindows1251

У графічній формі: (джерело: greycat.ru )Кілька струн, концентрація 1%

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

Список літератури


21
"Це питання показує наукові зусилля" - хм ... так, пройдіть. +1
Густав Баркефорс

7
StringBuilderбуде незначно швидшим, ніж StringBufferнесинхронізованим, я лише згадую це, тому що ви позначили це тегомmicro-optimization

2
@Jarrod Roberson: гаразд, тож давайте зробимо всі поля лише для читання остаточними і також витягніть s.length()з forциклу :-)
додому

3
Деякі символи в космосі можна надрукувати, наприклад \tта \n. Багато символів старше 127 не можна друкувати у вашому наборі символів.
Пітер Лорі

1
Ви ініціювали буфер рядків ємністю s.length()?
фрік-фрік

Відповіді:


11

Якщо доцільно вбудувати цей метод у клас, який не є спільним для потоків, тоді ви можете використати буфер повторно:

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% чи близько того, наскільки я розумію найкращий на даний момент випадок.

Якщо це буде використано на потенційно великих рядках, і "витік" пам'яті викликає занепокоєння, можна використовувати слабке посилання.


Чудова ідея! Поки що це дозволило підрахувати до 3471017 рядків в секунду - тобто + 12% покращення порівняно з попередньою найкращою версією.
GreyCat

25

використання 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);

1
Дякую! Ваша версія дає 3105590 рядків / сек - значне покращення!
GreyCat

newLen++;: а як щодо використання попереднього збільшення ++newLen;? - (також ++jу циклі). Подивіться тут: stackoverflow.com/questions/1546981/…
Томас

Додавання finalдо цього алгоритму та використання oldChars[newLen++]( ++newLenє помилкою - весь рядок буде вимкнено на 1!) Не дає значних прирістів продуктивності (тобто я отримую ± 2..3% різниці, які можна порівняти з різницями різних запусків)
GreyCat

@grey Я зробив ще одну версію з деякими іншими оптимізаціями
ratchet freak

2
Хм! Це геніальна ідея! 99,9% рядків у моєму виробничому середовищі насправді не потребуватимуть зачистки - я можу вдосконалити його, щоб усунути навіть перше char[]розподіл і повернути рядок як є, якщо не відбулося зачищення.
GreyCat

11

Ну, я перебив поточний найкращий метод (рішення виродка з попередньо виділеним масивом) приблизно на 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).


Дякуємо за рішення, перевірте оновлення запитань - я опублікував свою структуру тестування та додав 3 результати тестового запуску для 17 алгоритмів. Ваш алгоритм завжди вгорі, але він змінює внутрішній стан Java String, порушуючи тим самим контракт "незмінної рядка" => використовувати його в реальному світі було б досить важко. Тестово, так, це найкращий результат, але, мабуть, я
оголошу

3
@GreyCat Так, у нього, звичайно, є кілька великих рядків, і, чесно кажучи, я майже лише написав це, бо я впевнений, що немає помітного способу вдосконалити ваше поточне найкраще рішення. Бувають ситуації, коли я впевнений, що це буде працювати нормально (без підрядків чи стажувань перед вилученням), але це через знання про одну поточну версію точки доступу (тобто, на жаль, вона не буде інтернувати рядки, прочитані з IO - не буде t бути особливо корисним). Це може бути корисно, якщо комусь дійсно потрібні ці зайві x%, але в іншому випадку більше базового рівня, щоб побачити, наскільки ви все ще можете поліпшитись;)
Voo

1
Хоча я намагаюся спробувати версію JNI, якщо знайду час - ніколи не використовував її досі, тому це було б цікаво. Але я впевнений, що це буде повільніше через додаткові накладні витрати (рядки занадто малі) та той факт, що JIT не повинен мати такий важкий час для оптимізації функцій. Тільки не використовуйте new String()на випадок, якщо рядок не було змінено, але я думаю, ви вже це зрозуміли.
Voo

Я вже намагався зробити абсолютно те саме в чистому C - і, ну, це насправді не значно покращує вашу версію на основі роздумів. Версія C працює приблизно на + 5..10% швидше, не так вже й чудово - я думав, це буде як мінімум 1,5x-1,7x ...
GreyCat

2

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


Так, я теж про це думав, але в моїй ситуації це не призведе до збільшення продуктивності - цей алгоритм видалення буде викликаний у вже масово паралельній системі.
GreyCat

2
І, крім того, я міг би здогадатися, що відгалуження декількох потоків для обробки на кожні 50-100 байтові рядки було б величезним надлишком.
GreyCat

Так, розгалуження ниток для кожного маленького рядка - це не гарна ідея. Але балансир навантаження може покращити продуктивність. До речі, ви перевіряли продуктивність за допомогою StringBuilder замість StringBuffer, у якого не вистачає продуктивності, оскільки він синхронізований.
umbr

Моя виробнича установка запускає кілька окремих процесів і використовує якомога більше паралельних процесорів і ядер, тому я можу вільно користуватися StringBuilderскрізь, не маючи жодних проблем.
GreyCat

2

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

Не особливо добре задокументовано, але ну добре. Ось - ми включили як алгоритми храпового виродка, так і базову версію. На даний момент я випадковим чином ініціалізую рядок довжиною 200 символів з рівномірно розподіленими символами в діапазоні [0, 200).


+1 за зусилля - але ти повинен був запитати мене - я вже маю подібний набір тестів - саме там я тестував свої алгоритми;)
GreyCat

@GreyCat Ну, міг, але просто кинути це разом (у будь-якому випадку з існуючого коду) було, мабуть, швидше;)
Voo

1

Наркоман для низькорівневої продуктивності Java IANA, але чи пробували ви розгортати основний цикл ? Здається, це може дозволити деяким процесорам виконувати перевірки паралельно.

Крім того , це має деякі забавні ідеї для оптимізації.


Я сумніваюся, що тут можна було б здійснити будь-яку розгортку, оскільки існують (а) залежності від наступних кроків алгоритму на попередніх кроках, (б) я навіть не чув, щоб хтось робив ручну розгортку циклу на Java, даючи якісь зоряні результати; JIT зазвичай добре виконує роботу, розгортаючи все, що вважає за потрібне. Дякую за пропозицію та посилання, правда :)
GreyCat

0

чому використання набору символів "utf-8" безпосередньо забезпечує кращу продуктивність, ніж використання попередньо виділеного статичного const Charset.forName ("utf-8")?

Якщо ви маєте на увазі String#getBytes("utf-8")і т. Д.: Це не повинно бути швидшим - за винятком кращого кешування - оскільки Charset.forName("utf-8")воно використовується всередині, якщо набір символів не кешований.

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.