Pull to refresh

Comments 53

В Rust не всегда удаётся писать высокоэффективные обобщённые структуры данных

Будем честны, в расте невозможно писать структуры данных, особенно эффективные. Там даже строку не смогли реализовать хорошо из-за языка

А на С++ описанное в статье пишется и довольно просто
https://github.com/kelbon/AnyAny/blob/main/include/anyany/variant_swarm.hpp
Причем расширяемее чем в zig

А можете пояснить что не так со строками в раст? Хотелось бы для себя узнать больше

Вообще строки в Rust появились вопреки, а не почему. Тип String под капотом Vec<u8>, то есть вектор байт. Есть ещё &str - строковый слайс. Обычно, когда говорят про строки в Rust, имеют в виду эти 2 типа.
Если String может быть мутабельным,то &str изменить напрямую никак нельзя.
Есть ещё такая особенность: вы не можете индексировать строку.
fn main() {
let my_string1 = "45456456";
let my_string2 = String::from("45456456");
println!("{}", my_string1[0]);
println!("{}", my_string2[0]);
}
Этот код не может быть скомпилирован, будет ошибка error[E0277]: the type str cannot be indexed by {integer}

Всё это следствие безопасности языка.
P.S.: поправьте меня, если я не прав.

Понял спасибо. Но в целом непонятна претензия к индексации строки. Ведь то что вы показали это валидный UTF-8 и к нему индексация символов неприменима так как это не константная операция по массиву. И это не следствие безопасности языка, а следствие того что это UTF-8 и один символ может кодироваться несколькими байтами. Взять определенный символ по индексу возможно, но для этого надо будет пройтись по массиву символов my_string.chars().nth(0)

Но и не понимаю претензию к тому что это вектор байт...ведь...строуки это и есть массив байт...разве нет?

Я не фанатик раста, я вообще его плоховато знаю. Так что не примите пожалуйста за попытку защиты языка. Я как раз наоборот пытаюсь для себя лучше его понять

они не "индексируются" потому что они юникодные, а там длина символа переменная

В QString как-то работает. Только там как бы QChar

там подозреваю просто перегрузка оператора идет и внутри такой же перебор идет

У QString индексирование легко может вернуть половину суррогата, так что именно что "как-то". QString — это тяжелое наследие времен UCS-2, которое с горем пополам расширили до UTF-16. Давно уже пора бы его закопать и переходить на UTF-8. Даже в WinAPI, спустя всего 20 лет, добавили поддержку!

Внутри всё равно UTF-16. Поэтому вызов UTF-8 будет не таким уж эффективным.

В тему разных реализаций. К примеру Go в string хранит в UTF-8, при этом можно, если зачем-то хочется, взять индекс строки, он выдаст тип byte, т.е. для ограниченного подмножества ASCII работать будет. Однако, в цикле for idx,val := range str (более частый кейс использования) перебор пройдет уже по символам, а не байтам.

Зато my_string1[0..1] уже скомпилируется. Безопасность тут ни при чём, это всего лишь следствие использования кодировки UTF-8

На самом деле строковых типов сильно больше и многим это даже не нравится. Это не проблема безопасности, но вполне себе показывает, что строки не настолько просты насколько хотелось бы - там utf, сям cp866, трям какой-нибудь legacy CJK-JIS. В этом смысле стандартная библиотека явным образом делает основной тип строк валидным utf-8 как один из хороших дефолтов.

Ну и если подумать, то выяснится, что буквально всё в структурах это вектор байт, просто в С/С++ отродясь не было честного байтового типа и был только char*. Так что очень странная претензия.

и чем же вас не устраивает char как байт? В С++17 есть даже std::byte, который в общем то только именем от char отличается

Например, наличием signed/unsigned char. std::byte относительно недавно появился, а в Си в честном виде его так и не завезли. std::byte не символьный и не арифметичесский тип, что как минимум на уровне шаблонов имеет значение. Но, да, кому нужны все эти модные штуки, когда есть (unsigned char*)(void*)

Это не следствие безопасности, а следствие того что UTF-8.

Очень хочешь получить бессмысленный кусок code point? Да пожалуйста - можешь бесплатно преобразовать String обратно в Vec или &str в &[u8]

Хочешь бесплатно индексироваться по символам - переходи на Ascii / UTF-32 не удивлюсь, если уже кто-то типы для таких строк создал (ну для ascii точно есть в std) - в общем в любую кодировку с фиксированным размером байтов.

Спасибо, вот это интересное чтиво было) Но наверное это следствие его низкоуровневости и попытки беопасно работать везде?)

Это не столько с Windows, сколько с winapi-rs. Но там и без этого уже всё в unsafe завёрнуто.

Ну, во первых они юникодные по умолчанию и других нет, что зачастую приводит к потере перфа на пустом месте, а во вторых для хорошей реализации обычно используют self -reference типы, а в расте такое невозможно, потому что все типы "муваются" только через memcpy

Ну и они нерасширяемые никак - только такая String и ничего больше. Для сравнения можно глянуть на С++ строку, где и аллокатор и трейты (поведение строки, типа кейс интенсив и тд) кастомизируются, как и тип символа

А почему это плохо что они по умолчанию юникод? По моему in general как раз в большинстве случаев и используется везде юникод, а там где нужен не юникод ситуаций сильно меньше. Так что тогда можно и сделать не юникод строку отдельно.

А можете пояснить на счет self reference типов пожалуйста?

А почему это плохо что они по умолчанию юникод

потому что других нет. Возьмёт новичок и пойдет делать задачу на литкоде, строку перевернуть. А ему бах и все юникодные "прелести" в лицо, наверное на этом он язык и забросит

А можете пояснить на счет self reference типов пожалуйста?

в С++ внутри строки есть указатель на массив символов, в зависимости от длины строки он либо указывает в динамическую память, либо внутрь самой строки, поэтому для маленьких строк(которых большинство) динамическую память выделять не нужно(и эффективность не теряется т.к. для доступа всегда достаточно перейти по указателю). А в расте
* непонятно как через лайфтаймы выразить что объект сам на себя ссылается
* если даже получится, то мувать объект будет нельзя, потому что в расте не предусмотрено ничего кроме копирования байт в новое место(а это сломает self-reference объект)

На счет литкода аргумента не понял. Тот же го тоже имеет юникод строки по умолчанию. И не вижу в этом проблемы и также легко юникод реверсил когда был джуном. Не понимаю что должно оттолкнуть и в чем проблемес.

А на счет self reference спасибо!

Тот же го тоже имеет юникод строки по умолчанию.

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

Ну, когда понадобится «исчезнуть» — выпустят новый edition, в котором изменят поведение строковых литералов и подсунут в prelude новую реализацию String, и в целом ничего страшного не произойдёт (хотя экосистеме понадобится какое-то время для переката, но нам не впервой — вон призрак UCS-2 нас уже лет тридцать преследует)

для маленьких строк(которых большинство) динамическую память выделять не нужно

https://lib.rs/crates/smol_str, https://lib.rs/crates/smartstring - навскидку.

(и эффективность не теряется т.к. для доступа всегда достаточно перейти по указателю).

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

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

ну очень маленькие строки (например четыре буквы на английском в utf-8) можно передать как одно машинное слово вообще без indirection.

Четыре или три буквы - вполне реальные в мире строки. Например это могут быть тикеры для биржи.

мы говорим не про 4 байтика, а про строки. Для них лучше чем переход по указателю без ветвления придумать ничего нельзя(компилятор может оптимизировать этот переход по указателю, если на компиляции доказал, что строка < SSO size)

А ему бах и все юникодные "прелести" в лицо

Та ну я вас умоляю. s.chars().rev().collect::<String> не невесть какая магия.

и это в тысячу раз неэффективнее чем переворот ASCI строки

Так вы за юникод говорили, причём тут ASCII? Разверни какой-нибудь "ふすむすぁしぺど" как ASCII по байтам и получишь грязи, а не "どぺしぁすむすふ", так что в плюсах точно кому-то прилетят юникодные прелести.

Если вас интересует только ASCII диапазон, то часто проще сразу в байты всё это дело перевести и работать с байтами.

Возьмёт новичок и пойдет делать задачу на литкоде, строку перевернуть. А ему бах и все юникодные "прелести" в лицо, наверное на этом он язык и забросит

О да, способность решать задачи на Leetcode (с, кстати, крайне кривыми сигнатурами на большинстве языков) — это действительно очень репрезентативная метрика юзабельности языка.


C++ тут, кстати, не лучше — как на нём обратить строку "Привет, мир", например?


И, кстати, я всё ещё не видел случая, когда переворот строки является реальной задачей.

Придираться к примерам, игнорируя проблемы языка - как это по растовски

Но для этого и существуют два основных типа строк. Это String и &str. &str можно получить из String или из другого &str, и это указатель+длина в чистом виде

казалось бы, причём здесь это. И как же раст пудрит мозги, что это называют "другим видом строк", в С++ оно называется string_view и полностью отражает реальность

Self-Reference типы это для Cow-строк? Так и в плюсах и ЕМНИП в 11 стандарте ещё из дефолтного поведения убрали.

Казалось бы, причём тут cow

Строки правильны с алгоритмической точки зрения, но это приносит некоторое количество боли. Во-первых многословны. Если код направлен на массовую работу со строками, читать тяжело. Во-вторых, не совпадает опыт с другими языками/реализациями. Ни в одном языке нет такого количества WTF при работе со строками у изучающих язык.

Резоны разработчиков языка о гарантиях производительности при работе со строками понятны. Индексация UTF вытащена в код. Но хотелось бы видеть в дополнение к существующему варианту, реализацию StringScalar (аналог видения строк в Питоне) и StringGrapheme, с которыми можно было работать именно как со строками (размер, срез, итерация и пр.). Где сложность обработки/индексации инкапсулирована, пусть и в ущерб производительности. Точнее в ущерб не производительности (она одинаковая), а в ущерб ожиданиям производительности.

Не понял, как вы так волшебным образом перешли от одного упорядоченного массива к нескольким плотным (не разреженным) массивам.

Это перевод.

Автор исходного текста открыл для себя что вектор структур менее выгоден по памяти чем набор отдельных векторов с полями структуры.

Если бы речь шла про структуры, то вопросов бы не было, но речь про объединения. А их вот так вот разделять совсем не выгодно - остаются огромные дырки.

Так суть та же - енам фактически это тег + структура. Речь в первую очередь идёт про вектора перечислений. И их выгоднее в определённых ситуациях перераскладывать на структуру массивов. Правда кейс получается несколько специфичный в любом случае.

Я в Zig не разбираюсь, но насколько понял по приведенному коду, там нет сквозной индексации: Метод append выплевывает индекс в виде кортежа (тег типа + индекс для конкретного типизированного массива), и только по этому тегированному индексу можно сделать get. Так что это не вектор, а куча enum-ов.

На самом деле вы пользуетесь им как обычным массивом и не думаете про все эти кортежи. Компилятор всё делает за вас.

Я полагаю и на Rust можно провернуть такое через.

По умолчанию текущее поведение вполне ожидаемо учитывая, что так работает во всех других языках.

У компилятора раста пока только автоматический packing структур есть в оптимизациях, когда поля структуры перемешиваются, чтобы занимать как можно меньше места. Механизмов AOS <> SOA в компилятор пока не завезли, хотя были некоторые обсуждения на форуме. Microsoft кстати по этому поводу презентовал свой язык.

По умолчанию текущее поведение вполне ожидаемо учитывая

Тут возникает вопрос что вы зовёте другими языками, ибо большинство языков с алгебраическими типами в виде enum это с десяток модных молодёжных языков, включая тот же Rust и несколько старичков вроде Haskell/OCaml с их GC, где про оптимизации подобного рода стали задумываться относительно недавно. Остальное большинство же имеет обычные целые числа в качестве enum, а всякие data-oriented layout крафтились через такую-то матерь и собственные препроцессоры кода.

Я имел ввиду скажем C/C++ где просто делаем variant , аналогично дела обстоят и в C#/Java когда мы эмулируем алгебраические типы.

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

Можно конечно самому сохранять тегированные индексы в отдельном массиве, но в этом упоощенном коде такого нет.

С точки зрения пользователя языка - ничего не поменялось. Делаешь также `for item in array` и трогаешь поля как тебе хочется. Компилятор сам все индирекции посчитает и подставит. Просто теперь память всё это дело будет занимать поменьше, чем при автоматическом выравнивании памяти.

Я полагаю вопрос был о том как мы в таком же порядке итерируемся как при массиве.

У нас получается либо общего порядка больше нет. Он есть относительный в каждом отдельном массиве, но не общий.

Либо мы как-то храним этот порядок, но тогда это увеличивает расход памяти обратно, а также такой перебор будет совсем не эффективным из-за постоянных прыжков между массивами.

Индирекции, особенно на больших данных, получаются дешевле чем промахи кэша из-за постоянной необходимости выравнивать куски памяти. Плюс SOA часто позволяет срабатывать автовекторизации, за счёт чего влияение скачков нивелируется ещё сильнее. Правда не знаю насколько у Zig этот механизм развит. LLVM такое умел, но с ним вроде развелись.

Но вообще да, в среднем ECS подход имеет некоторые накладные расходы, которые дают выигрыш на бОльших данных и головную боль на небольших.

Sign up to leave a comment.

Articles