Re[50]: MS забило на дотнет. Питону - да, сишарпу - нет?
От: Sinclair Россия https://github.com/evilguest/
Дата: 03.09.21 18:01
Оценка: +2
Здравствуйте, vdimas, Вы писали:

V>Здравствуйте, Sinclair, Вы писали:


V>>>Я привожу что есть по факту.

V>>>А то, что у тебя — это надо самим делать обертку поверх ODBC и там малость не так тривиально, но это уже подробности.
S>>Я тоже привожу то, что есть по факту.

V>Так я попросил показать, что это за код, где живет, который

V>
V>GetFieldType(i) == typeof(int)
V>      ? MemoryMarshal.GetReference(MemoryMarshal.Cast<byte, int>(_rawData.Slice(GetFieldOffset(i)));
V>      : Convert.ToInt32(GetValue(i));
V>

Примерно такой код и живёт
V>И да, если читать из поля Int16, например, Int32, то опять пляшем через боксинг, верно?
Смотря как делать. Но это — не очень важный аспект: поля int16 в базах встречаются нечасто. А там, где встречаются, разработчики знают, зачем их применяют, и не будут делать конверсию на том конце, который сделает её медленнее.

V>Ну так из приемного буфера (который уже managed) — в MemoryRecordBuffer, а оттуда в датасет или в поля объектов какого-нить ORM.

Вот не вижу этого момента "из приёмного буфера".
У меня такое ощущение, что показано не всё. По крайней мере, в документации указано, что SmiRecordBuffer подразумевает эффективные реализации для in-proc сценариев, но в референс сорцах виден только один наследник, который помечен как "используем для out-of-proc".

V>>>И в любом случае твой пример только для MSSQL, а для любых других баз на основе OLEDB или ODBC будет как я дал ссылку на исходники дотнета.

Для любых других баз будет их собственные реализации IDataRecord.

V>И все пишут в своих проектах собственные дрова к БД?

V>Или пользуются имеющимся в дотнете?
Зачем собственные? Зачем имеющиеся в дотнете? Если есть такая потребность — пишется один раз эффективная реализация для конкретной СУБД.
Так-то я могу и в нативе взять первое попавшееся чопопало и потом расстраиваться, что у меня двойные-тройные конверсии внутри.

V>Куда я тебе показал в первый раз — это реализация ODBC драйвера.

V>Все базы имеют ODBC-дрова, но далеко не все имеют OLEDB.
V>Например, нет OLEDB драйвера к самой популярной в вебе базе MySQL.
Эмм, причём тут OLEDB? Мы же вроде бы про дотнет говорим. И кто вам сказал, что драйвера OLEDB внутри устроены как-то сильно эффективнее, чем MemoryRecordBuffer? Это же COM — значит, никакого инлайнинга вызовов GetData; да и сигнатура у его методов подразумевает копирование в caller-allocated storage. Ну, и где тут хвалёный С++ с его возможностью "вернуть сразу данные из буфера без промежуточных копирований"?
Так что давайте мы не будем рассматривать OLE DB в качестве какой-то особенно быстрой альтернативы дотнету.
А если мы посмотрим на дотнет, то есть, к примеру, System.Data.Sqlite.org. Он работает поверх SQLite, безо всякого ODBC или OLEDB. Или, скажем, вряд ли же какой-нибудь ДевАртовский дотконнект SQLite написан как-то сильно плохо. А если разработчика устраивает дефолтный System.Data.Sqlite, у которого в аннотации написано "простенький провайдер поверх ADO с изрядными ограничениями", то кто ему виноват?
V>Следующая по популярности идёт PostgreSQL, к ней тоже живые/актуальные только ODBC-дрова.
Для PostgreSQL живы/актуальны нативные дотнетные дрова, 100% написанные на C#. Судя по всему — тоже не очень плохо написанные: https://www.npgsql.org/

V>Я говорил как оно есть по-факту, а ты рассуждаешь как оно могло бы быть, если бы все в мире действовали самым разумным способом.

Как видим, все действуют именно так, как я говорю.
V>"В нейтиве оптимизированные" — это ты только что придумал.
V>Дрова там обычные, бо парсить разметку любого протокола много ума не надо.
V>Оптимизированным получается сам сценарий зачитки данных, потому что максимально непосредственный.
Непонятно. Вы в качестве оптимизированного сценария приводите почему-то OLEDB, который by design страдает от тех же (и худших) ограничений, что и дотнет.

V>Там должно было быть до 80% общего кода, если не больше.

Со временем, может быть, и будет. А может быть — нет. Потому что какой-нибудь SQLite вообще буфера клиенту не отдаёт — зачем ему? У него все манипуляции делаются прямыми вызовами в inproc DLL.
А устройство протоколов обмена данными для каждого вендора, мягко говоря, очень своё.
Если мы захотим сделать что-то более оптимальное, чем дефолтный драйвер поверх универсального коннектора типа OLE DB или ODBC, то придётся погрузится вот в эти подробности.

V>Ну вот я тебе ссылку дал на кишки драйвера к MSSQL.

V>Это я уже молчу о том, что половину типов по той ссылке можно было сделать value-type.
В теории — да.
V>По-факту там ад и ужас, нагрузка на GC на ровном месте.
По факту там внутри — массив структур.

S>>поверх которого генерируется специфический для возвращаемого типа код.


V>Не особо специфический — просто набор сгенерённых акцессоров к полям объекта (это "статические" объекты, т.е. для одного целевого entity создаются лишь однажды для каждого его поля), замапленный на колонки конкретного рекордсета. Если маппер достаточно умный, то при надобности вставит свои конвертеры, например где надо из Int16 в Int32, чтобы чтение уже скопированных и закешированных данных обходилось без боксинга/анбоксинга, как оно есть сейчас.

Имеется в виду, что для каждого пользовательского типа QueryResult и набора колонок исходного запроса должен порождаться примерно вот такой вот код:
public record QueryResult(string, decimal);
public IEnumerable<QueryResult> GetQueryResult<R>(R r) where R: IDataReader
{
  while(r.Read())
    yield return new QueryResult(r.GetString(1), r.GetDecimal(0));
}

Примерно это делает Linq2db. Если IDataReader реализован так, как вы хотите (путём парсинга протокола), то всё будет близко к максимально оптимальному. В суровом настоящем для того, чтобы заинлайнить тут вызовы, нужно делать IDataReader структ-типом; в неопределённом светлом будущем (которое в джаве уже наступило) инлайнинг выполняется спекулятивно благодаря тому, что реальный тип R виден среде исполнения.

V>Т.е., в общем случае 3 вложенных виртуальных ф-ии на одно чтение уже закешированных данных.

V>А нет, это еще не чтение, это всё еще продолжается заполнение "модели" данными.
V>Чтение клиентским кодом будет когда-нибудь потом.
Ну так в OLE DB — то же самое.

V>Это для динамических противопоказано, а для снапшотных до фени.

Не шибко дружественно к кэшу, да и потребности такой нет.

V>Что уже странно, кстате, бо обычно данные в современном мире передаются вменяемыми порциями — чтобы на экран вместилось.

Ну, так на современный экран вмещаются мегабайты — мы ж не только текст показываем

V>По-идее, дефолтная реализация должна была давать пример остальным, то бишь, представлять из себя самое лучшее референсное воплощение...

По идее — да. Но не получится, т.к. в OLE DB и ODBC нет подобной возможности. Поверх чего будем строить "референсное воплощение"?

S>>Для специфичной — делаем враппер, который достаёт данные согласно разметке.


V>Не так быстро.

V>Не "враппер", а полноценный драйвер.
V>Например, полностью своя подмена драйвера ODBC.

V>То бишь, самим реализовать всё семейство с 0-ля: IDbConnection, IDbTransaction, IDbCommand и еще пару десятков (если не больше) сущностей из модели ADO.Net.

Ну, это мы как раз говорим об обобщённом доступе — когда в компайл-тайм мы как бы не знаем настоящих типов данных и их разметки.

V>Я тебя уже 3-й раз прошу дать координаты этого "нового стиля".

V>Исходники же дотнета открыты, какие проблемы?
Пока что во взаимодействии с базами ничего не произошло. В основном оптимизируют HTTP стек.
V>Вот тебе их ODBC "враппер":
V>https://github.com/dotnet/runtime/blob/main/src/libraries/System.Data.Odbc/src/System/Data/Odbc/DbDataRecord.cs#L43
V>На Линухах у разработчика будет задействован он с вероятностью 99%.
V>И мне даже несколько странно, что ты не поддерживаешь меня в моём стремлении расстрелять без объяснения причин авторов этого кода. ))
Вот в данном месте я не понимаю, что именно там можно улучшить.
С учётом того, что ODBC не даёт читать данные дважды, а IDataReader — даёт.
То есть если бы мы писали ORM прямо поверх ODBC, а не поверх ADO.NET поверх ODBC, то можно было бы попытаться взять конкретный пользовательский тип, unsafe способом раскопать его layout, прибиндить к его экземпляру колонки фиксированных размеров и делать SQLFetch на каждый next. Да и то, я сходу не вижу способа сделать это всё достаточно надёжным способом — так, чтобы это работало с приемлемой скоростью и не взрывалось от незначительного обновления в используемом у клиента драйвере.

V>>>Сорри, но ты сейчас малость из пальца насасываешь.

V>>>Оно примерно так и было, как есть сейчас.
S>>Я говорю про Span<T>, Memory<T>, MemoryMarshal, ref struct, unmanaged constraint.
S>>Это — революционные изменения в платформе, без которых бессмысленно говорить о каких-либо оптимизациях. Т.к. например банальный парсинг даты из хидера HTTP требовал сначала скопировать байты, затем превратить их в UTF16, и потом мучительно парсить — то есть речь о двойном копировании с двукратным раздуванием объёма.

V>Зачем "скопировать"?

V>StreamReader поверх сокета был всегда.
V>А потом еще и асинхронный давно.
И? Ну вот где-то "в сокете" лежит (предположительно) дата в формате RFC2616. Надо получить DateTime. У вас — фреймворк версии 4.5. Что именно вы собрались делать со StreamReader-ом?

V>Не, сортировка в магазинах по ограниченному кол-ву критериев: по цене, популярности, рейтингу.

V>И почти никогда нет последовательной сортировки, т.е. сортировка практически во всех популярных (проходных) онлайн-магазинах по одному из предоставленных критериев в одну сторону, на этом с сортировкой всё.
Ну так и ветер дует вовсе не потому, что деревья качаются. Просто разработчики магазинов велосипедят именно такой вот код, в котором невозможно сделать последовательную сортировку.
А не потому, что их пользователи попросили "нельзя ли сделать вот так, через жопу, а?"

V>ЧТД.

V>Почему база передаёт NULL-поля в виде битового вектора, и почему я не могу поступать так же?
Потому что надо смотреть в query plan, который будет получаться.

V>Без джоинов.

V>Онлайн-магазины — это почти всегда розница.
Я что-то не понял, каким это способом мы от всех UI всех приложений вообще внезапно перешли к единственному гую "список товаров".
V>Речь может идти только о проекциях, т.к., например, подробные технические характеристики в обзорный список товаров обычно не тащат, это уже когда открывают карточку выбранного товара на экране.
Ну так их зачастую и в основную таблицу не тащат, потому что нет желания её раздувать.

S>>а потом половина вытащенных данных будет дропнута перед показом.


V>Я бы хотел посмотреть на живой пример, где это так.



S>>И вы ещё спрашиваете меня, почему я считаю белых неэффективными.


V>Я считаю, что ты надумываешь условия под ответ.

V>Предлагаю поступать наоборот.


V>Если бы.


V>Когда партиции булевские, то на десятках-сотнях этих индексов от булевых полей ничего не выигрывается.

Смелая идея. Во-первых, далеко не все партиции — булевские.
Во-вторых, когда их много, и они рассеянные — как раз таки выигрывается дофига. Потому что каждая отдельная редкая фича встречается у малой доли товаров.
Следите за руками: я беру список без фильтра, https://www.wildberries.ru/catalog/elektronika/smartfony-i-telefony/vse-smartfony. 1401 товар.
Включаю, скажем, объём памяти 8GB: https://www.wildberries.ru/catalog/elektronika/smartfony-i-telefony/vse-smartfony?page=1&amp;f4424=12864 — уже 8 товаров.
Селективность — 150x! Как бы есть за что побороться, не так ли?

V>Индексам для пользы от них требуется хоть какая-то "ширина" и самих индексов желательно немного.

V>Т.е., если простая сумма всех уникальных значений всех индексов сравнимо с кол-вом данных — выигрыша от индекса не будет.
Выигрыш от индекса определяется его селективностью. Количество различных значений — это, конечно, здорово, но слишком грубо. Нужно анализировать распределение.
То есть когда я попросил 8GB памяти, я искал не по булевому полю "Is8Gb", а по полю "RAM Size", у которого значительно больше значений, чем 2, и они распределены по Зипфу.
Даже если мы возьмём какое-нибудь честное булевое поле, типа "Есть беспроводная зарядка", то селективность может запросто оказаться значительно лучше, чем 50%.

V>Тут наоборот, все биты надо собрать в одно-два поля и по ним сделать индекс.

V>И тогда будет столько прыжков по индексу, сколько значащих бит в маске.
Кажется, я догадался, что вы имеете в виду. Вы хотите построить bitmap index? Ну, там "прыжки по индексу" выглядят вовсе не так дёшево, как кажется. Их не особо рекомендуют для случаев, когда битов сильно много.
Я ручаюсь, что у wildberries нет никаких битмап индексов внутри — там явно банальный EAV в сочетании с well-known attributes. Но это всё опять — от бедности, когда у нас не данные, а слабоструктурированная помойка.

V>1. Только надо на не равно нулю проверять.

Ну мы же здоровые люди — у нас битовые маски хранятся в unsigned
V>2. Ставлю на то, что ты заблуждаешься.
V>3. Десятки-сотни индексов по булевым или совсем "узким" данным всё-равно ничего не дадут в плане эффективности.
Конечно.
V>Можно поэкспериментировать с планами и посмотреть.
Естественно. Никто в здравом уме не будет собирать честные таблицы из сотен колонок и заводить тысячи индексов. Но и заводить отдельный блок битмап-индекса размером в мегабайт каждый раз, как криворукий продавец напишет очередной "blueooh" в значение атрибута тоже никто не будет.

Рулит вдумчивый анализ структуры данных и подготовка таблиц. А затем, в процессе эксплуатации — подтюнивание индексов. И вот тут очень важно, чтобы клиентское приложение давало СУБД простор для манёвра.
\
S>>Это будет прекрасно приемлемо работать ровно до тех пор, пока у вас хватает кэша на то, чтобы держать всю базу в памяти.

V>Во-вторых, вытесняет кеш скомпиллированных запросов из базы, т.к. каждая из 2^N комбинация уникальная.

Конечно уникальная. Но они — вовсе не равновероятны. В реальности одномоментно живут не так уж много комбинаций.

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

Это всё в теории. На практике HDD никуда не делись. И чем больше объём ворочаемых данных, тем выгоднее оптимизировать софт, чем заливать его железом.

V>"Бесконечные" движения по ним, но в движениях уже никаких 2^N комбинаций изобретать не надо.

V>Как делать "универсальный движитель" по складу я тебе как-то показывал.
Не, толком не показал. Разговоров было много, но до схемы таблиц дело так и не дошло. Как и до понимания того, что не все задачи сводятся к движению по складу — в типичной enterprise базе данных до сих пор бывает по паре тысяч таблиц.

V>Не у меня, а у всех их немного.

V>Я тебе дал ссылку на самый продвинутый магазин именно в плане фильтрации-поиска товаров.
Эта голимая убогость? Вайлдберриз как раз знаменит тем, что в нём адски сложно искать.
V>На wildberries заходят не за чем-то конкретным, а "просто посмотреть, что в мире эдакого есть", он заточен именно под такой сценарий.
Совершенно точно — их убогие поисковые возможности не дают найти что-то конкретное.
V>Т.е. вряд ли ты найдёшь магазин с большим кол-вом управляемых признаков выборки.
Относительно нормальный поиск по товарам был на яндекс.маркете. Я туда давно не ходил, не знаю — может, тоже слили в говно. Но, когда ходил, там порнографии типа "38 вариантов написания слова bluetooth" не было.
И если обувь ищешь по размеру, то нет риска нарваться на то, что поставщик указал "34 " и товар не попал в поисковую выдачу.
V>В общем, этих флагов всегда относительно немного, иначе этим магазином невозможно будет пользоваться.
V>Человеку показывается такое кол-во информации и органов управления, которые он принципиально сможет переварить.
V>И желательно до того как психанёт и закроет сайт. ))
Клёво вы придумываете. А можно же не фантазировать, а пойти и посмотреть: https://market.yandex.ru/catalog--vstraivaemye-posudomoechnye-mashiny-v-novosibirske/58608/filters?cpa=0&amp;hid=90584&amp;onstock=1&amp;local-offers-first=0

V>Предлагаешь положиться в споре на случайность, приводить в пример отдельных двоечников или отличников как док-во чему бы то ни было?

Ну, вы же приводите в пример двоечников типа вайлдберриз.

V>Не-не-не, способ общения с базой ортогонален способу формирования условий на клиенте.

С чего это?
V>Клиент всё-равно пошлёт по своему внутреннему протоколу серверу приложений всё эти флаги-ограничения для поиска товаров, а как сервак эти флаги преобразует в запрос — про это клиент и знать не будет.
V>(например, речь об андроидном приложении вайлдберриз)
Не, это у вас какое-то смещение в сознании произошло. Клиент-то понятное дело на сервер это всё отправляет примерно в том же виде, как и браузер.
Но мы же говорим не про веб-клиента какого-то REST API. Мы говорим про взаимодействие с базой — то место, где сервер приложений читает входящие параметры, формирует SQL запрос, отправляет его в базу, а потом обрабатывает и шлёт обратно какие-то JSONы.

V>Я имел ввиду, что если запрос формируется не через Linq, а непосредственной конкатенацией строк, почему это должно работать медленнее?

Для начала — потому, что вы уже выбрали "prepared statement для вызова хранимки", интуитивно полагая, что это и есть оптимальное решение.
А надо было готовить именно честный select statement, т.к. только он донесёт до сервера ваши предпочтения в нужном виде. И прямо все-все колоночки запихать внутрь bitmap index не получится — было б по-другому, так производители СУБД и без вас бы применили этот трюк. Увы.

V>Даже если упомянули самый эффективный и от того самый популярный способ обращения к БД через синтаксис вызова хранимки в ODBC, это не значит, что других способов нет.

V>Это просто способ, к которому неплохо бы привести всё что приводится.


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

Не, самый жирный пример против хранимки — это какой-нибудь экран "order history". С десятком колонок, из которых половина подтащена через inner join, и ещё парочка — через outer join. А полный список, доступный для выбора — так и вовсе полсотни. Потому как пользовательских ролей — много, сценариев — ещё больше. И там нужны и фильтры по диапазонам, и сортировки произвольные. И данные нужны прямо из OLTP, потому что люди в этом списке, в частности, смотрят, какие заказы сфейлились, а какие — прошли. Так что идея "а давайте мы это будем по ночам складывать в OLAP" не проходит.
V>На других экранах: корзина, доставка/трекинг, история покупок, платежей, общений с персоналом и т.д. и т.п. всё проще, нифига не вызов.
Это с т.з. конечного пользователя — да. У него там возможности урезаны, и контекст очень узки. У него история покупок — от силы пара десятков строк, и всего остального — тоже. Тут даже думать не надо, запросы фиксированные, объёмы маленькие, индексы очевидные.

V>И что характерно, в моей модели битовых флагов функциональность хорошо повторно-используема для большинства групп товаров.

V>Просто "метаинформация" относительно семантики бит для каждой группы своя (но это уже очевидные вещи).
Ну пока что проблемой будет то, что предлагаемый вами способ хранения и обработки данных напрямую ни в одной СУБД не поддерживается. Можно что-то подобное навелосипедить на Redis, но он, как известно, не совсем СУБД. То есть он больше подойдёт в качестве кэша между настоящей базой и сервером приложений.
Изо всех известных мне СУБД битмэпы поддерживает только Оракл, но и он не сможет разложить на биты единственную колонку — он работает не так.
Вам придётся заводить в базе честные колонки под каждый из атрибутов, и уже для них создавть bitmap-индексы. Ну, и да, про where bits & @mask > 0 придётся забыть — вам по-прежнему придётся отправлять в сервер вот этот вот where ram = "256 GB" or ram = "265 ГБ", чтобы уже у себя сервер это превратил в сканирование битмапов.
То есть мы вернулись к тому, с чего начали — как сформировать такой запрос, чтобы сервер понял, как его выполнить.

V>Хреновый драйвер, значит, или специально для тебя в текст отрендерили, чтобы тебе было понятней.

V>ODBC и OLEDB умеют передавать именно команды на вызовы процедур с аргументами по протоколу.
V>Да еще с такими, которые в текст не так-то просто перевести, например, binary.
Ну, вопрос, конечно, интересный. Может, действительно — рендерят для понятности. Надо бы посмотреть через wireshark.

V>Я все-равно не понял, почему у меня не может быть несколько хранимок?

Потому что 2^N — это слишком много для "несколько". Поэтому на практике так никто не делает — эти хранимки же надо маинтейнить потом, при каждом мелком изменении в таблице будь любезен весь набор подпилить.

V>Да, так что помешало?


V>Ага, особенно когда у нас значения комбинируются по OR по десяткам индексов.
V>Ты эта, поиграй с планами на досуге.
Я эту домашнюю работу выполнил ещё примерно во времена моих первых десяти постов на этом сайте.
И не просто преподавателю показал, что я умею реляционное исчисление в реляционную алгебру переводить, а натурально сидел и разбирался с планами. И Гарсиа-Молина зачитал до дыр.
Поэтому я, конечно, обрадуюсь, если вы мне что-то новое сможете про это рассказать, но скорее удивлюсь.

V>В случае OR по M признакам будет O(log(N)*M)

Каким образом вы получаете log(N)?

V>Ты планы-то покрути.



V>И где можно на такую реализацию посмотреть?

Пока — нигде, увы.

V>Ес-но. И код пишется под джит и тщательнейшим образом профилируется, благо в джаве репорты от джита подробные и вменяемые, а в дотнете джит — чёрный ящик.

Вполне у него подробные репорты.
V>И я хорошо понимаю — почему.
V>Стоит отлить из бетона какой-нить публичный АПИ к джиту, и эту гирю на ногах так просто потом не сбросишь.
Для репортов АПИ не нужен.

V>Да хотя бы на этом форуме.

V>Не приветствуется вылизывание, подвергается остракизму.
Смотря что вылизывать.

V>Да можно.

V>Реинтерпретация памяти и до Span-времён работала неплохо.
V>Просто теперь к ней подключили реинтерпретацию управляемой памяти.
V>Раньше в этих сценариях тупо пинили.
V>Но всё-равно в дотнете всегда всё пинится.
V>Создаётся копия строки (именно копия), или строка копируется в буфер — пинятся оба конца.
V>Преобразуется из UTF16 в UTF8 — тоже пинятся источник и приёмник.
V>Под капотом там обычный unsafe и указатели.
V>Именно поэтому теперь моя либа по конверсии строк стала не нужна. ))
Реинтерпретация памяти — бессмысленна, если у вас парсинг даты выполняется через DateTime.TryParseExact(string s, ...). Что вы там будете интерпретировать, если методу нужна полноценная отдельная строка, со своим length и буфером?
Только изобретение Span<T> позволило начать пилить разнообразные API, построенные поверх "фрагмента памяти", а не поверх тяжеловесных конструкций типа той же string или byte[].

V>Не уверен, что стоит публично это делать, в той области know how — понятие очень нервное.

А, вы боитесь, что на форуме поделились слишком многими секретами? И теперь вся общественность узнает, как ваши либы устроены внутри? Ну вы даёте. Я NDA даже во сне не нарушаю.
Не, мне не настолько интересно, чтобы вас под монастырь подводить. Вы лучше продолжайте делиться know how — эдак оно полезнее для общества будет.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.