[Перевод] Java Best Practices. Преобразование Char в Byte и обратно

    Сайт Java Code Geeks изредка публикует посты в серии Java Best Practices — проверенные на production решения. Получив разрешение от автора, перевёл один из постов. Дальше — больше.


    Продолжая серию статей о некоторых аспектах программирования на Java, мы коснёмся сегодня производительности String, особенно момент преобразования character в байт-последовательность и обратно в том случае, когда используется кодировка по умолчанию. В заключение мы приложим сравнение производительности между неклассическими и классическими подходами для преобразования символов в байт-последовательность и обратно.

    Все изыскания базируются на проблемах в разработке крайне эффективных систем для задач в области телекоммуникации (ultra high performance production systems for the telecommunication industry).

    Перед каждой из частей статьи очень рекомендуем ознакомиться с Java API для дополнительной информации и примеров кода.

    Эксперименты проводились на Sony Vaio со следующими характеристиками:
    ОС: openSUSE 11.1 (x86_64)
    Процессор (CPU): Intel® Core(TM)2 Duo CPU T6670 @ 2.20GHz
    Частота: 1,200.00 MHz
    ОЗУ (RAM): 2.8 GB
    Java: OpenJDK 1.6.0_0 64-Bit

    Со следующими параметрами:
    Одновременно тредов: 1
    Количество итераций эксперимента: 1000000
    Всего тестов: 100


    Преобразование Char в Byte и обратно:



    Задача преобразования Char в Byte и обратно широко распространена в области коммуникаций, где программист обязан обрабатывать байтовые последовательности, сериализовать String-и, реализовывать протоколы и т.д.
    Для этого в Java существует набор инструментов.

    Метод «getBytes(charsetName)» класса String, наверное, один из популярнейших инструментов для преобразования String в его байтовый эквивалент. Параметр charsetName указывает на кодировку String, в случае отсутствия оного метод кодирует String в последовательность байт используя стоящую в ОС по умолчанию кодировку.

    Ещё одним классическим подходом к преобразованию массива символов в его байтовый эквивалент является использование класса ByteBuffer из пакета NIO (New Input Output).

    Оба подхода популярны и, безусловно, достаточно просты в использовании, однако испытывают серьёзные проблемы с производительностью по сравнению с более специфическими методами. Помните: мы не конвертируем из одной кодировки в другую, для этого вы должны придерживаться «классических» подходов с использованием либо «String.getBytes (charsetName)» либо возможностей пакета NIO.

    В случае ASCII мы имеем следующий код:

    public static byte[] stringToBytesASCII(String str) {
    
     char[] buffer = str.toCharArray();
    
     byte[] b = new byte[buffer.length];
    
     for (int i = 0; i < b.length; i++) {
    
      b[i] = (byte) buffer[i];
    
     }
    
     return b;
    
    }
    


    Массив b создаётся путём кастинга (casting) значения каждого символа в его байтовый эквивалент, при этом учитывая ASCII-диапазон (0-127) символов, каждый из которых занимает один байт.

    Массив b можно преобразовать обратно в строку с помощью конструктора «new String(byte[])»:

    System.out.println(new String(stringToBytesASCII("test")));
    


    Для кодировки по умолчанию мы можем использовать следующий код:

    public static byte[] stringToBytesUTFCustom(String str) {
    
     char[] buffer = str.toCharArray();
    
     byte[] b = new byte[buffer.length << 1];
    
     for(int i = 0; i < buffer.length; i++) {
    
      int bpos = i << 1;
    
      b[bpos] = (byte) ((buffer[i]&0xFF00)>>8);
    
      b[bpos + 1] = (byte) (buffer[i]&0x00FF);
    
     }
    
     return b;
    
    }
    


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

    И обратно в строку:

    public static String bytesToStringUTFCustom(byte[] bytes) {
    
     char[] buffer = new char[bytes.length >> 1];
    
     for(int i = 0; i < buffer.length; i++) {
    
      int bpos = i << 1;
    
      char c = (char)(((bytes[bpos]&0x00FF)<<8) + (bytes[bpos+1]&0x00FF));
    
      buffer[i] = c;
    
     }
    
     return new String(buffer);
    
    }
    
    


    Мы восстанавливаем каждый символ строки из его двухбайтового эквивалента и затем, опять же с помощью конструктора String(char[]), создаём новый объект.

    Примеры использования возможностей пакета NIO для наших задач:

    public static byte[] stringToBytesUTFNIO(String str) {
    
     char[] buffer = str.toCharArray();
    
     byte[] b = new byte[buffer.length << 1];
    
     CharBuffer cBuffer = ByteBuffer.wrap(b).asCharBuffer();
    
     for(int i = 0; i < buffer.length; i++)
    
      cBuffer.put(buffer[i]);
    
     return b;
    
    }
    
    public static String bytesToStringUTFNIO(byte[] bytes) {
    
     CharBuffer cBuffer = ByteBuffer.wrap(bytes).asCharBuffer();
    
     return cBuffer.toString();
    
    }
    


    А теперь, как и обещали, графики.

    String в byte array:



    Ось абсцисс — количество тестов, ординат — количество операций в секунду для каждого теста. Что выше — то быстрее. Как и ожидалось, «String.getBytes()» и «stringToBytesUTFNIO(String)» отработали куда хуже «stringToBytesASCII(String)» и «stringToBytesUTFCustom(String)». Наши реализации, как можно увидеть, добились почти 30% увеличения количества операций в секунду.

    Byte array в String:



    Результаты опять же радуют. Наши собственные методы добились 15% увеличения количества операций в секунду по сравнению с «new String(byte[])» и 30% увеличения количества операций в секунду по сравнению с «bytesToStringUTFNIO(byte[])».

    В качестве вывода: в том случае, если вам необходимо преобразовать байтовую последовательность в строку или обратно, при этом не требуется менять кодировки, вы можете получить замечательный выигрыш в производительности с помощью самописных методов. В итоге, наши методы добились в общем 45% ускорения по сравнению с классическими подходами.

    Счастливого кодинга.

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 35

      +4
      Так и сделали бы топик-перевод.
        +3
        Наши собственные методы добились 15% увеличения количества операций в секунду по сравнению с «new String(byte[])» и 30% увеличения количества операций в секунду по сравнению с «bytesToStringUTFNIO(byte[])».
        В итоге, наши методы добились в общем 45% ускорения
        Откуда здесь 45%?
        Во первых сначала говорится про увеличение на 15% и 30% кол-ва операций в секунду (а это 13% и 23% прироста скорости обработки соответственно), а потом сразу переход на ускорение.
        Во вторых в лучшем случае прирост производительности не может быть больше чем 23%, а если используются оба метода — то и того меньше.

        А то получается изначальный автор таким хитрым софизмом более чем в два раза увеличил процентные показатели эффективности своего метода.
          0
          Справедливости ради надо уточнить, что местами статья действительно вызывает какие-то сомнения. Однако то, что выигрыш есть, несомненно — лично померял.
          • НЛО прилетело и опубликовало эту надпись здесь
            • НЛО прилетело и опубликовало эту надпись здесь
                0
                Неправильно подозреваете. Одинаково работают. Можете проверить.
                Совсем недавно я приводил пример, когда метод в три раза длиннее по байткоду компилируется один-в-один, как и короткий метод.
                • НЛО прилетело и опубликовало эту надпись здесь
                  • НЛО прилетело и опубликовало эту надпись здесь
                +1
                А можно еще ускорить процентов этак на 50, если избавиться от str.toCharArray(); и пользоваться str.charAt().
                • НЛО прилетело и опубликовало эту надпись здесь
                    +1
                    Потому что проверял и знаю.
                    А еще это вполне логично даже из общих соображений: toCharArray() создает новый массив, увеличивая нагрузку на память и копируя символы лишний раз, в то время как charAt() достает символ непосредственно из строки.
                    • НЛО прилетело и опубликовало эту надпись здесь
                        +1
                        Не будет это лучше. От лишнего копирования-то все равно не избавитесь. А вызов метода — это не так страшно. Тем более, что никакого вызова в данном случае в скомпилированном коде нет — метод инлайнится.
                        • НЛО прилетело и опубликовало эту надпись здесь
                          • НЛО прилетело и опубликовало эту надпись здесь
                            • НЛО прилетело и опубликовало эту надпись здесь
                                0
                                C:\Windows\Temp>java Test2 (charAt)
                                240098112
                                210

                                C:\Windows\Temp>java Test3 (getChars)
                                240098112
                                247

                                Меньше — лучше, правильно ведь? charAt выигрывает.

                                C:\Windows\Temp>java -version
                                java version "1.6.0_20"
                                Java(TM) SE Runtime Environment (build 1.6.0_20-b02)
                                Java HotSpot(TM) 64-Bit Server VM (build 16.3-b01, mixed mode)

                                Вы бы еще в чистом интерпретаторе запустили и в нем сравнивали.
                                • НЛО прилетело и опубликовало эту надпись здесь
                                  • НЛО прилетело и опубликовало эту надпись здесь
                                      0
                                      Мне смайлик следовало поставить во фразе про интерпретатор? :)
                                      Тега <irony> явно не хватает.
                                      Давайте перенесем дальнейшую дискуссию в личку, чтоб более не засорять топик.
                                      • НЛО прилетело и опубликовало эту надпись здесь
                        +1
                        А если сравнить эти функции с кодом написанным на c и загруженным в java через JNI. Какова будет разница в производительности? Так, для сравнения…
                          0
                          м'сье знает толк…
                          0
                          «Массив b можно преобразовать обратно в строку с помощью конструктора «new String(byte[])»:»
                          Это вы для ASCII написало, но на самом деле:
                          «String(byte[] bytes) Constructs a new String by decoding the specified array of bytes using the platform's default charset.»
                          Т.е. это ни разу ни ASCII и на каком-нибудь Linux или в Японии вы запросто всё испортите из-за того, что входной пото байт будет рассматриваться, как последовательность UTF8.
                          Хуже того, некоторые символы могут преобразовываться в Char неоднозначно — управляющие символы и т.п.
                          Короче говоря — плохо разобрались в предмете.
                            +2
                            Интересно, были ли тесты написаны с учетом особенностей микробенчмаркинга в Яве или нет.
                              0
                              Чертовски правильный вопрос!
                              Например, в выложенных здесь javatest.zip и javatest2.zip нарушены практически все пункты правил написания микробенчмарков для JVM.
                              • НЛО прилетело и опубликовало эту надпись здесь
                                  0
                                  Извините. Возможно, вы и анализировали output компилятора и ассемблерный код. Тогда я неправ.
                                  Я ж судил только по очевидным признакам:
                                  — Warm-up phase нет.
                                  — Изначально выводы были основаны на измерениях результатов клиентского компилятора.
                                  — Предотвращение OSR не выполнено.
                                  — Печать и инициализация классов внутри timing phase.
                                  А ведь эти самые главные!
                                  • НЛО прилетело и опубликовало эту надпись здесь
                              +5
                              это конечно прекрасно — оптимизировать преобразования из массивов в строки и обратно
                              круто, что 30%
                              но если подоюные вызовы занимали 5% времени выполнения запроса, то ускорение будет 1,66%
                              и ради этого придется писать кривые самописные методы
                              овчинка выделки не стоит, я щетаю
                                0
                                И всё же иногда приходится работать с большими объёмами текстовых данных, которые требуют подобных операций. И в таком случае описанные в статье методы действительно влияют на общую производительность.
                                +3
                                Так как уже второй раз у вас вижу в заголовке топика «[Перевод]», то выступлю в роли КО и скажу, что так делать не надо. Для переводов существует специальный тип топиков.
                                  0
                                  >Каждый символ в Java занимает 2 байта

                                  На собеседованиях очень люблю спрашивать, как же это так, если основная фишка UTF в том, что там переменное количество байт на символ.
                                    0
                                    >The char data type (and therefore the value that a Character object encapsulates) are based on the original Unicode specification, which defined characters as fixed-width 16-bit entities

                                    оно? :)
                                      0
                                      >The Unicode standard has since been changed to allow for characters whose representation requires more than 16 bits.

                                      Скорее об этом и что с этим делать.

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                  Самое читаемое