Comments 16
Одно неконтролируемое чтение данных без полной проверки каждого символа на диапазон unicode (а это просадка производительности) - и такой "оптимальный" код сломается. По сути, это не оптимизация, а вектор атаки. Rust классная технология, но её авторам бы поменьше религиозного фанатизма и побольше бы погружения в реальную практику системного программирования.
Что-то мне подсказывает, что читать без unsafe кусок памяти в структуру/енам у вас не выйдет, это не си. А unsafe подразумевает, что вы знаете, что вы делаете.
Допускаю такое. Но встречный вопрос: проверяется ли в самом деле диапазон значений при чтении char из файла или потока стандартной библиотекой? А как при этом решается вопрос с обратной совместимостью при расширении стандарта unicode, что происходит регулярно?
Это же получается, что используя такой трюк мы автоматически предполагаем, что обновление стандарта поломает наш код - или на этапе проверки, или уже логику, если проверки нет. Как ни крути, не очень хорошее решение.
Я не знаю способа прочитать один символ именно как символ. Возможно он существует, но я не в курсе. Я знаю только про str::from _utf8, который все это проверяет и про str::from_utf8_unchecked который не проверяет и он, соответственно, unsafe.
А так юникод вообще вещь странная и я не очень понимаю, что считать символом.
Одну и ту же Ë можно записать и как один символ и как два (диакритический знак и Е). В общем если читать вручную посимвольно, то это верный способ по итогу рано или поздно себе ногу отстрелить.
При чтении из UTF-8 проблемы может и нет, но ведь существует UTF-32, которым тип Char и представлен. И что делать с обновлением стандарта - всё равно не ясно :) Отдельный прикол в том, что даже UTF-32 поддерживает составные символы, ну и про диакритические знаки вы верно заметили.
В общем, конечно не большой спец в Rust, в основном пишу всякое системное на C/C++ в т.ч. в ядро, но на первый взгляд кажется, что им бы эту оптимизацию прагмой сделать и тогда ей бы цены не было. А так это весьма спорно.
Де-факто стандарт в современном мире это UTF-8. Если про UTF-16 я еще могу сказать, что он там где-то в кишках винды до сих пор популярен, то про использование UTF-32 я вообще ничего не знаю, но возможно потому что у меня область немного другая и это конкретно я не сталкиваюсь.
В теории наверное что-то есть для работы как с UTF-16, так и с UTF-32, но я практически уверен, что там будет либо с проверками, либо unsafe, либо оба варианта рядом.
И в любом случае это не имеет отношения к теме поста, потому что сериализация/десериализация прямого представления в памяти делается только через unsafe. Что подразумевает, что человек в теме происходящего разбирается достаточно хорошо, чтобы сделать это корректно.
существует UTF-32, которым тип Char и представлен.
Где? Если вы имели в виду Rust, то там char
- это не UTF-32.
Но встречный вопрос: проверяется ли в самом деле диапазон значений при чтении char из файла или потока стандартной библиотекой?
Не может не проверятся. Char хранит номер кодовой точки, а в файлах записаны дынные в какой-то кодировке (например, utf-8). Это не одно и тоже и библиотека должна сделать перевод.
А как при этом решается вопрос с обратной совместимостью при расширении стандарта unicode, что происходит регулярно?
1114112 (0х110000) — это все кодовые точки, которые когда либо будут в стандарте. Их было столько что 5-ой версии 15 лет назад, что в текущей, 16-ой. Количество точек соответствующих каким-то символам на текущий момент — 150к. Каждый год по паре тысяч добавляют. То есть, при сохранении текущих темпов, пространства хватит лет на 100.
Впрочем, я бы выбрал что-то ближе к концу (0xF0000000), чтобы через 50 лет не переписывать.
Согласен, что странно было выбрать нишу впритык. Но существующие Rust программы это не сломает: все программы собираются всегда статически, если появится стандарт, расширяющий диапазон и что-то использующий там, то ваша программа все равно не сможет это корректно прочитать еще до того, как вы к enum-у перейдете.
Наверно, любое некрайнее (в свободном диапазоне) значение дискриминанта не очень удобно для компилятора, ведь кроме enum Option
c одним None
, можно объявить сколько угодно полей без данных. Например: None, None2, ... None845
и так далее.
Если выбирать ближе к концу, тогда придется для разных типов заводить разные определения близости к концу. Возможно, в компиляторе выбрали универсальный путь.
Можно начать нумеровать от первого числа вне диапазона типа (в примере от 0x110000
) и вплоть до сколько влезет в 4 байта. Если не влезет, то придется живой дискриминант ввести.
Наверное, можно нумеровать и в обратнуую сторону: от 0xFFFFFFFF
до 0x110000
). Не знаю, есть ли хорошая причина выбрать прямой или обратный вариант. Может, тип хранящий enum
тоже может поменяться (то есть точка обратного отсчета 0xFFFFFFFF
поменяется. А может они просто выбрали один из двух подходящих и им оказался прямой.
P.S. Удивительно, но в типе char есть дырка! :) то есть там может быть не 1_114_111
вариантов, а 1_112_064
(на 2047
меньше). И компилятор ее понимает и принимает во внимание. Моя жизнь не будет прежней :)
Строки в Rust всегда корректные UTF8 последовательности в байтовом представлении именно потому что даныые пролностью проверяются https://github.com/rust-lang/rust/blob/master/library/core/src/str/validations.rs#L126
По сути, это не оптимизация, а вектор атаки
Покажите proof of concept
А как при этом решается вопрос с обратной совместимостью при расширении стандарта unicode, что происходит регулярно?
Подозреваю вы тут что-то путаете или не понимаете как юникод работает.
Диапазоны значений (0 to D7FF16 and E00016 to 10FFFF16) никогда не менялись и не будут меняться как раз для обратной совместимости с UTF-16
Это же получается, что используя такой трюк мы автоматически предполагаем, что обновление стандарта поломает наш код
А как скомпилированный код из стандартной библиотеки узнает об изменении стандарта юникода? Юникод - это не просто стандарт и кодировка, а еще и очень много кода, реализующего вышеперечисленное.
При чтении из UTF-8 проблемы может и нет, но ведь существует UTF-32, которым тип Char и представлен.
Зачем вы галлюцинируете вместо того, чтобы просто открыть доки: https://doc.rust-lang.org/std/primitive.char.html
В расте неконтролируемое чтение возможно только если вы делаете unsafe вызов и явно выполняете функцию неконтролируемого чтения, беря на себя ответственность за результат выполнения, так что нет, авторы раста прекрасно понимают практику системного программирования.
Так и вижу эту картину: конец месяца, подбой итогов, пиво, пицца…
И тут кто-то говорит: ну, коллеги, поскольку буквально всё остальное у нас уже безукоризненно, давайте, наконец, откусим последнюю занозу, отделяющую проект от финального релиза под кодовым названием «The Best Ever». Давайте сэкономим четыре байта. Эти четыре байта — последнее, что стои́т между нами и идеалом.
Вышеописанная особенность Rust это скорее даже не оптимизация, а суровая необходимость. В Rust достаточно часто используется Option<&T> для представления опциональных ссылок, вместо какого-то отдельного типа. Так чтобы он весил не больше указателя, Option использует тот факт, что ссылки не могут быть нулевыми и поэтому хранится он в памяти как сырой указатель с интерпретацией нулевого значения как None и остальных значений как Some(&T).
Этим на самом деле вышеописанная оптимизация по большей части и ограничивается, в остальных случаях она даёт мизерный эффект, т. к. такие случаи достаточно редки.
Удивительная оптимизация размера enum в компиляторе Rust