как избежать потери памяти при сохранении символов UTF-8 (8 бит) в символе Java (16 бит). два в одном?

Боюсь, у меня вопрос по мелочи довольно перенасыщенной темы, я много искал, но так и не нашел четкого ответа на эту конкретную очевидную - imho - важную, проблему:

При преобразовании byte[] в String с использованием UTF-8 каждый байт (8 бит) становится 8-битным символом, закодированным UTF-8, но каждый символ UTF-8 сохраняется как 16-битный символ в java. Это правильно? Если да, это означает, что каждый глупый символ Java использует только первые 8 бит и потребляет вдвое больше памяти? Это тоже правильно? Интересно, как это расточительное поведение приемлемо..

Нет ли какой-нибудь хитрости, чтобы иметь псевдостроку, которая является 8-битной? Действительно ли это приведет к меньшему потреблению памяти? Или, может быть, есть способ сохранить> два 8-битных символа в одном 16-битном символе java, чтобы избежать этой траты памяти?

спасибо за любые разъясняющие ответы...

РЕДАКТИРОВАТЬ: привет, спасибо всем за ответ. Я знал о свойстве переменной длины UTF-8. Однако, поскольку мой источник - это 8-битный байт, я понял (очевидно, неправильно), что ему нужны только 8-битные слова UTF-8. Действительно ли преобразование UTF-8 сохраняет странные символы, которые вы видите, когда в CLI делаете «cat somebinary»? Я думал, что UTF-8 просто каким-то образом используется для сопоставления каждого из возможных 8-битных слов байта с одним конкретным 8-битным словом UTF-8. Неправильный? Я думал об использовании Base64, но это плохо, потому что он использует только 7 бит.

вопросы переформулированы: есть ли более разумный способ преобразовать байт во что-то String? Возможно, самым любимым было просто преобразовать byte[] в char[], но тогда у меня все еще есть 16-битные слова.

дополнительная информация о прецедентах:

Я адаптирую Jedis (java-клиент для NoSQL Redis) в качестве "примитивного уровня хранения" для hypergraphDB. . Итак, jedis — это база данных для другой «базы данных». Моя проблема в том, что мне приходится все время кормить джедаев данными byte[], но внутренне >Redis‹ (фактический сервер) имеет дело только с «двоично-безопасными» строками. Поскольку Redis написан на C, char имеет длину 8 бит, AFAIK, а не ASCIII, который составляет 7 бит. Однако в джедаях, мире java, каждый символ имеет внутреннюю длину 16 бит. Я не понимаю этот код (пока), но я полагаю, что jedis затем преобразуют эти 16-битные строки Java в 8-битную строку, соответствующую Redis (([здесь] [3]). Он говорит, что расширяет FilterOutputStream. Я надеюсь обойти преобразование строки byte[] ‹-> в целом и использование этого Filteroutputstream...? )

теперь я задаюсь вопросом: если бы мне приходилось все время преобразовывать byte[] и String с размерами данных от очень маленьких до потенциально очень больших, не будет ли огромной тратой памяти, чтобы каждый 8-битный символ передавался как 16-битный в java ?


person ib84    schedule 12.04.2011    source источник
comment
Вы знаете, что некоторые символы UTF-8 имеют размер 2, 3 или 4 байта, верно? Весь мир не использует ASCII.   -  person Ernest Friedman-Hill    schedule 12.04.2011
comment
привет, спасибо всем за ответ. Я знал о свойстве переменной длины UTF-8. Однако, поскольку мой источник - это 8-битный байт, я понял, что ему нужны только 8-битные слова UTF-8. Разве это не так? Действительно ли преобразование UTF-8 сохраняет странные символы, которые вы видите, когда в CLI выполняете команду cat somebinary? Я думал, что UTF-8 просто каким-то образом используется для сопоставления каждого из возможных 8-битных слов байта с одним 8-битным словом UTF-8. Неправильный? Я думал об использовании Base64, но это плохо, потому что он использует только 7 бит.   -  person ib84    schedule 12.04.2011
comment
UTF-16 также является кодировкой переменной ширины, как и UTF-8. Он просто использует более крупные единицы кода.   -  person tchrist    schedule 12.04.2011
comment
Что плохого в преобразовании двух байтов в один символ?   -  person ib84    schedule 12.04.2011
comment
Что было бы плохо, так это то, что символ состоит из 21 бита.   -  person tchrist    schedule 12.04.2011
comment
Вы можете закодировать его по алгоритму Хаффмана, чтобы самые распространенные кодовые точки занимали наименьшее количество битов, и наоборот. Таким образом, для каждого документа потребуется отдельный преамбулаторный словарь, отображающий биты в кодовые точки. Это было бы ложной эффективностью.   -  person tchrist    schedule 12.04.2011
comment
Хм? почему 21-битный символ не 16-битный/кратный 8-битному? помните, я хочу эффективно хранить только байты, меня не волнуют специальные символы.   -  person ib84    schedule 12.04.2011
comment
Ваше утверждение содержит ложную предпосылку: не существует такого понятия, как «специальный символ»; кодовые точки либо допустимы, либо нет. При условии, что обе являются допустимыми кодовыми точками, кодовые точки U+XXXXX и U+YYYYYY в равной степени не являются специальными. Единственная специальность будет выведена из обратного частотного анализа, и это будет игра Хаффмана чрезвычайно ограниченной применимости. Unicode включает порядковые номера от 0 до 0x1FFFFF, хотя не все они допустимы для обмена. Именно по этой мере символ состоит из 21 бита; все, что меньше 21 бита, ipso facto меньше символа.   -  person tchrist    schedule 12.04.2011


Ответы (7)


Нет ли какой-нибудь хитрости, чтобы иметь псевдостроку, которая является 8-битной?

да, убедитесь, что у вас установлена ​​последняя версия Java. ;)

http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html

-XX:+UseCompressedStrings Использовать byte[] для строк, которые могут быть представлены как чистый ASCII. (Представлено в Java 6 Update 21 Performance Release)

РЕДАКТИРОВАТЬ: этот параметр не работает в обновлении Java 6 22 и не включен по умолчанию в обновлении Java 6 24. Примечание. Похоже, что этот параметр может снизить производительность примерно на 10%.

Следующая программа

public static void main(String... args) throws IOException {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 10000; i++)
        sb.append(i);

    for (int j = 0; j < 10; j++)
        test(sb, j >= 2);
}

private static void test(StringBuilder sb, boolean print) {
    List<String> strings = new ArrayList<String>();
    forceGC();
    long free = Runtime.getRuntime().freeMemory();

    long size = 0;
    for (int i = 0; i < 100; i++) {
        final String s = "" + sb + i;
        strings.add(s);
        size += s.length();
    }
    forceGC();
    long used = free - Runtime.getRuntime().freeMemory();
    if (print)
        System.out.println("Bytes per character is " + (double) used / size);
}

private static void forceGC() {
    try {
        System.gc();
        Thread.sleep(250);
        System.gc();
        Thread.sleep(250);
    } catch (InterruptedException e) {
        throw new AssertionError(e);
    }
}

Печатает это по умолчанию

Bytes per character is 2.0013668655941212
Bytes per character is 2.0013668655941212
Bytes per character is 2.0013606946433575
Bytes per character is 2.0013668655941212

с опцией -XX:+UseCompressedStrings

Bytes per character is 1.0014671435440285
Bytes per character is 1.0014671435440285
Bytes per character is 1.0014609725932648
Bytes per character is 1.0014671435440285
person Peter Lawrey    schedule 12.04.2011
comment
Питер, это очень интересно, спасибо! Я говорю это, потому что меня уже давно не устраивает окончательность класса String в Java в сочетании с тем, как он связан с литералами String в языке. Это делает невозможным использование надлежащих методов объектно-ориентированного программирования для любого вида улучшенных или измененных строк. Вы только что показали одно такое использование; некоторые из моих включают строки posit, состоящие либо из последовательностей кодовых точек, либо из последовательностей графем. Я не знал о -XX:+UseCompressedStrings; его существование предполагает, что можно было бы сделать и другие подобные, хотя, возможно, и не переносимые. - person tchrist; 12.04.2011
comment
очень хорошо! если я сэкономлю 50% памяти, наличие большего объема памяти должно компенсировать падение производительности на 10%. Благодарность - person ib84; 12.04.2011
comment
Это показывает еще одно потенциальное использование определяемого пользователем типа данных String в языке Java. Можно изменить базовое распределение памяти с UTF-16 переменной ширины на UTF-8 переменной ширины или на UTF-32 фиксированной ширины в зависимости от цели и распределения кодовых точек. Они могут быть дополнительно сжаты как детали реализации — при компромиссе между временем и пространством. Все, что требуется, — это улучшить определение Java String, чтобы допускать все, что соответствует «унифицированному интерфейсу кодовой точки». Строковые литералы могут управляться директивой компилятора с лексической областью видимости (pragma< /i>) на исходную единицу. - person tchrist; 12.04.2011
comment
Мой длинный комментарий показывает, почему ASCII недостаточно даже для чисто английского текста: я использовал не только 3 различных типа кавычек (\p{Quotation_Mark}), но и 4 различных типа дефисов и тире (\p{Dash}). Вы, вероятно, даже не замечаете, что есть что, но это нормально: они по-прежнему необходимы для правильного отображения повседневного английского языка. - person tchrist; 12.04.2011
comment
@tchrist, в Англии в повседневном английском используют £. ;) - person Peter Lawrey; 12.04.2011

На самом деле у вас неправильная часть UTF-8: UTF-8 - это многобайтовая кодировка переменной длины, поэтому допустимые символы имеют длину 1-4 байта (другими словами, некоторые символы UTF-8 являются 8-битными, некоторые - 16-битные, некоторые 24-битные, а некоторые 32-битные). Хотя 1-байтовые символы занимают 8 бит, многобайтовых символов гораздо больше. Если бы у вас были только 1-байтовые символы, это позволило бы вам иметь всего 256 различных символов (также известных как «расширенный ASCII»); этого может быть достаточно для 90% использования английского языка (моя наивная догадка), но он укусит вас за задницу, как только вы подумаете о чем-то помимо этого подмножества ( см. слово naïve - английское, но не может быть записано только с помощью ASCII).

Итак, хотя кодировка UTF-16 (которую использует Java) выглядит расточительной, на самом деле это не так. В любом случае, если вы не работаете с очень ограниченной встроенной системой (в таком случае, что вы там делаете с Java?), попытка урезать строки является бессмысленной микрооптимизацией.

Более подробное введение в кодировку символов см., например, в это: http://www.joelonsoftware.com/articles/Unicode.html

person Piskvor left the building    schedule 12.04.2011
comment
@Martijn Courteaux: [отвисает рот, ошеломленная тишина] Итак, вы показали ему что-то еще, что неправильно, но похоже работает, и что очень< /i> трудно разучиться? Я в шоке, в шоке! - person Piskvor left the building; 12.04.2011
comment
спасибо piskvor за ответ. Я читаю вашу ссылку сейчас .. Я 256 возможных слов байта просто сопоставляются с 256 символов 8 бит только UTF-8. Итак, теперь я еще больше убежден, что это действительно глупая вещь, это преобразование между byte[] ‹--› string... нет лучшего способа?? - person ib84; 12.04.2011
comment
Java не использует UCS-2. Он использует UTF-16. Это легко продемонстрировать: механизм регулярных выражений обрабатывает любой символ Unicode как ., независимо от того, сколько единиц кода он занимает. - person tchrist; 12.04.2011
comment
@ user703862: Не совсем так. В UTF-8 только символы 0-127 являются однобайтовыми; все, что выше этого, является многобайтовым. Хотя все кодировки Unicode (UTF-8, UCS-2 и другие) имеют свои недостатки, да, это лучший из всех поддерживаемых способов. - person Piskvor left the building; 12.04.2011
comment
UCS-2 не является кодировкой Unicode, поскольку она не может представлять все кодовые точки Unicode (за исключением тех, которые гарантированно недопустимы для обмена). Кодировки UTF-*, напротив, делают это. Я предпочитаю UTF-8 и UTF-32, в зависимости от того, что я делаю, а не UTF-16, но это слишком сложно, чтобы вдаваться в подробности. - person tchrist; 12.04.2011
comment
@tchrist: извините за дезинформацию. Для полноты: UCS-2 был заменен UTF-16 в версии 2.0 стандарта Unicode в июле 1996 г. en.wikipedia.org/wiki/UCS_2 - person Piskvor left the building; 12.04.2011
comment
@Piskvor, без проблем. Часть моей де-факто работы включает в себя информирование моих коллег, говорящих на языке Java, о различиях между UCS-2 и UTF-16. Большая часть проблемы связана со старой документацией и «упрощенными версиями реальности». Java v1 дебютировала в 95-м, Unicode v2 — в 96-м, но к тому времени было уже сочтено политически слишком поздно исправлять ошибку Java char. Другие факторы вступают в сговор, чтобы продолжить путаницу (например, интерфейсы, неспособные обрабатывать кодовые точки), но ни один из них не хуже, чем когнитивный диссонанс, возникающий в результате того, что Java char или Character слишком малы для хранения кодовой точки Unicode. - person tchrist; 12.04.2011

При преобразовании byte[] в String с использованием UTF-8 каждый байт (8 бит) становится 8-битным символом, закодированным в UTF-8.

Нет. При преобразовании byte[] в String с использованием UTF-8 каждый UTF-8 последовательность из 1–6 байтов преобразуется в последовательность UTF-16 из 1–2 16-битных символов.

Почти во всех случаях по всему миру эта последовательность UTF-16 содержит один символ.

В Западной Европе и Северной Америке для большинства текста используются только 8 бит этого 16-битного символа. Однако, если у вас есть знак евро, вам потребуется более 8 бит.

Для получения дополнительной информации см. Юникод. Или статью Джоэла Спольски.

person Anon    schedule 12.04.2011
comment
+1 Действительно. Небольшая придирка: вышеизложенное справедливо только в том случае, если под Западной Европой вы подразумеваете Великобританию, а под Северной Америкой — США. В противном случае вы получите французский é, испанский Ñ, немецкий ß, которые все являются многобайтовыми в UTF-8. - person Piskvor left the building; 12.04.2011
comment
@Piskvor: фигурные кавычки, правильные тире и дефисы — и то, и другое необходимо для правильного написания английского языка — все они занимают несколько кодовых единиц в UTF-8. Что касается Великобритании, в валлийском также используются буквы вне диапазона ASCII. Кроме того, любой, кто пишет резюме в своем резюме, автоматически проваливается. - person tchrist; 12.04.2011
comment
@tchrist: Совершенно верно, но 1) обратите внимание, что @Anon пишет большую часть текста, и 2) они были (несправедливо) отклонены в течение многих лет как ненужные придирки, почему бы вам просто не использовать - вместо вашего причудливого e[mn] тире, йада йада, поэтому я хотел привести примеры, где необходимость ясна. - person Piskvor left the building; 12.04.2011
comment
@Piskvor - я никогда не говорил, что это не так. Я сказал, что большинство западных символов будут занимать младшие 8 битов 16-битного символа Java. И хотя я не проверял, я считаю, что все символы, которые вы упомянули, являются ISO-8859-1, так что это правда. Однако знаки препинания, которые упоминает tchrist, не помещаются в этом пространстве. - person Anon; 12.04.2011
comment
@ Анон: я не пытаюсь тебе противоречить, извини, если я не ясно выразился. Что касается ISO-8859-, то это был временный взлом в духе того, что это будет доступно только людям, говорящим на *моем языке; когда-нибудь пробовал конвертировать между ними? Веселье, веселье, веселье (нет). - person Piskvor left the building; 12.04.2011
comment
ISO 8859-15, иногда называемый Latin-9, несколько лучше работает для некоторых западных языков, чем исходный ISO 8859-1, Latin-1, поскольку он включает œ и Œ необходимы для правильного написания таких слов, как французские œuf (U+153) и Œuvre de secours aux enfants ( U+152) и версию Pierre Louÿs от ᴀʟʟᴄᴀᴘs, поскольку LOUŸS требует Ÿ в U+178. Однако он делает такие вещи по цене, неприемлемой для некоторых других языков. Никакого 8-битного репертуара недостаточно для написания современного текста, особенно английского. Юникод решает все эти проблемы; пожалуйста, не восстанавливайте их. - person tchrist; 12.04.2011
comment
@tchrist - а? Этот последний комментарий имел в виду ссылку на мой пост, один из комментариев или что-то совершенно другое? Возможно, я преувеличиваю, но мой ответ не имеет ничего общего со сравнением наборов символов. Это должно было исправить неправильное представление OP о том, почему Java использует 16 бит для char. Если вы считаете, что я сделал это плохо, вы, конечно, можете отредактировать или проголосовать против. Если вы считаете, что я сказал что-то фактически неверное, пожалуйста, поправьте меня. Но сначала убедитесь, что вы исправляете то, что я действительно сказал. - person Anon; 12.04.2011
comment
@ Анон, о, я просто шутил о том, почему ISO 8859-1 не панацея, вот и все. Не звон предназначен. Любой, кто помогает «дезинформировать» широкую публику о причинах и следствиях, связанных с несколько неочевидной расстановкой символов в Java, заслуживает моей признательности и поддержки. Единственный нюанс, который я мог бы отметить, заключается в том, что любая последовательность из 1 или 2 символов UTF-16, представляющая одну кодовую точку Unicode, всегда сопоставляется с одним символом «по всему миру». — тогда и только тогда, когда вы приравниваете кодовые точки к символам, воспринимаемым пользователем, что является немного хитрым предположением, но лучше, чем многие другие. - person tchrist; 12.04.2011

Java хранит все свои «символы» внутри как двухбайтовые представления значения. Однако они не хранятся так же, как UTF-8. Например, максимальное поддерживаемое значение — «￿» (шестнадцатеричное FFFF, десятичное 65536) или 11111111 11111111 двоичное (два байта), но это будет 3-байтовый символ Unicode на диске.

Единственная возможная потеря - это действительно «однобайтовые» символы в памяти (большинство «языковых» символов ASCII фактически умещаются в 7 бит). Когда символы записываются на диск, они все равно будут в указанной кодировке (поэтому однобайтовые символы UTF-8 будут занимать только один байт).

Единственное место, где это имеет значение, - это куча JVM. Однако вам понадобятся тысячи и тысячи 8-битных символов, чтобы заметить реальную разницу в использовании кучи Java, которая будет намного перевешиваться всей дополнительной (хакерской) обработкой, которую вы сделали.

Миллион с лишним 8-битных символов в ОЗУ в любом случае «тратит впустую» около 1 МБ ...

person Michael    schedule 12.04.2011
comment
+1, небольшая придирка: нет такой вещи, как сохраненный Unicode, однако есть несколько кодировок Unicode (сопоставление между несколько абстрактными символами и их байтовыми представлениями) - person Piskvor left the building; 12.04.2011
comment
Да, это, вероятно, должно читаться не так, как UTF-8. ￿ — это 11101111 10111111 10111111 на диске (UTF-8). - person Michael; 12.04.2011
comment
спасибо микавели. Меня беспокоит только куча памяти JVM. Я имею дело с большим количеством байтов [], которые мне нужно упаковать в строки с эффективным использованием памяти. - person ib84; 12.04.2011
comment
Это понятно, но я бы не хотел отказываться от встроенной в Java поддержки Unicode — существует множество других обходных путей для управления количеством байтов []/символов в памяти (потоки, постоянство и т. д. и т. д.). - person Michael; 12.04.2011
comment
Я бы не хотел «отказываться» от «родной поддержки Unicode» в Java; однако я хотел бы, чтобы это действительно работало. На данный момент это немного завистливо и неуклюже, из-за чего слишком легко сделать что-то неправильное и слишком сложно сделать правильно. Юникод не просто присваивает символы порядковым номерам; это УКС. Unicode также представляет собой богатую коллекцию поведений, почти ни одно из которых не соответствует скорости Java. Он вообще не выполняет полное отображение регистра, и хотя он обеспечивает UAX#15, он игнорирует UTS#10, UAX#14, UTS#18, UAX#11, UAX#29 и, действительно, большую часть важных UAX#. 44. Java плохо поддерживает Unicode. - person tchrist; 12.04.2011

Redis (фактический сервер) работает только с "двоично-безопасными" строками.

Я понимаю, что это означает, что вы можете использовать произвольные последовательности октетов для ключей/значений. Если вы можете использовать любую последовательность C char, не задумываясь о кодировке символов, то эквивалентом в Java является тип byte.

Строки в Java неявно UTF-16. Я имею в виду, что вы можете вставлять туда произвольные числа, но целью класса является представление символьных данных Unicode. Методы, выполняющие преобразования byte в char, выполняют операции перекодирования из известной кодировки в UTF-16.

Если Jedis обрабатывает ключи/значения как UTF-8, то он не будет поддерживать каждое значение, поддерживаемое Redis. Не каждая последовательность байтов допустима в UTF-8, поэтому кодировку нельзя использовать для двоичной безопасности. струны.


В зависимости от того, какой тип UTF-8 или UTF-16 потребляет больше памяти, зависит от данных. Например, символ евро (€) занимает три байта в UTF-8 и только два в UTF-16.

person McDowell    schedule 12.04.2011

Просто для справки, я написал свою собственную небольшую реализацию интерконвертера byte[] ‹-> String, который работает путем приведения каждых 2 байтов к 1 символу. Это примерно на 30-40% быстрее и потребляет (возможно, меньше) половину памяти стандартного способа Java: new String(somebyte) и someString.getBytes().

Однако он несовместим с существующими строками, закодированными байтами, или строками, закодированными байтами. Кроме того, небезопасно вызывать метод из разных JVM для общих данных.

https://github.com/ib84/castriba

person ib84    schedule 26.04.2011

Может быть, это то, что вы хотите:

// Store them into the 16 bit datatype.
char c1_8bit = 'a';
char c2_8bit = 'h';
char two_chars = (c1_8bit << 8) + c2_8bit;

// extract them
char c1_8bit = two_chars >> 8;
char c2_8bit = two_chars & 0xFF;

Конечно, этот трюк работает только с символами ASCII (символы в диапазоне [0-255]). Почему? Потому что вы хотите хранить свои символы следующим образом:
xxxx xxxx yyyy yyyy с x — это символ 1, а y — это символ 2. Таким образом, это означает, что у вас есть только 8 бит на символ. И какое самое большое целое число вы можете получить с помощью 8 бит? Ответ: 255

255 = 0000 0000 1111 1111 (8 бит). И когда вы используете char> 255, у вас будет следующее:
256 = 0000 0001 0000 0000 (более 8 бит), что не соответствует 8 битам, которые вы предоставляете для 1 char.

Плюс: имейте в виду, что Java — это язык, разработанный умными людьми. Они знали, что делают. Подключите Java API

person Martijn Courteaux    schedule 12.04.2011
comment
Казалось бы умно, но попробуйте сделать это с этими двумя символами: çé. О, они не 8-битные, не так ли? Поздравляем, теперь у вас странный беспорядок в two_chars, и нет возможности извлечь исходные символы. (Интересно, как все думают, что ASCII должно быть достаточно для всех, даже когда они каждый день встречают символы вне ASCII) - person Piskvor left the building; 12.04.2011
comment
@Piskvor: я сказал, что знаю, что это не будет работать с символами вне диапазона [0-255]. Но если он знает, что делает, и его приложение использует только символы ASCII, это то, что он хочет... - person Martijn Courteaux; 12.04.2011
comment
Если разработчики Java были такими умными, зачем им создавать тип данных char, который недостаточно велик для хранения символа? Помните: «int — это новый char». - person tchrist; 12.04.2011
comment
если его приложение использует только символы ASCII - это возможно, но маловероятно. В таком случае ваш способ был бы допустимым (хотя я все же думаю, что есть лучшие способы экономии места, такие как кодирование Хаффмана). - person Piskvor left the building; 12.04.2011
comment
спасибо за этот ответ. Меня вообще не интересуют персонажи. Я просто хочу эффективно упаковать байт в строку. Так что да, байт должен быть [0-255], возможно, мне придется использовать операторы Shift, как вы. Ваш намек напомнил мне, что некоторые люди просто переводили байты в строки. Итак, теперь я пытаюсь преобразовать два байта в один символ. - person ib84; 12.04.2011