www.fgks.org   »   [go: up one dir, main page]

Как стать автором
Обновить

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

И это вы ещё Span не использовали.

Использовали. ? Про Span как-нибудь отдельно опубликую статью.

Не хватило ещё SearchValues в статье

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

В чем странность заключается?

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

Статья про одно, а тестовый код про другое.

Так если выделять подстроку с помощью split, то замер от момента вызова до получения результата в виде подстроки как раз и будет включать в том числе и разбиение строки по разделителю

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

Также непонятно почему они поддерживают только абсолютные пути и не поддерживают относительные, на которых код будет неверен `c:file.ext`. В общем много странного. Почему не используют стандартный для этого класс Path (https://learn.microsoft.com/ru-ru/dotnet/api/system.io.path) и т.д.

Ах, если бы люди умели читать, то в мире бы давно наступил коммунизм. )) Статья о том, что рассмотренные выше способы я встречал на практике. Это не значит, что это правильно или так надо делать. Это значит, что такой код пишут люди. Люди, например, берут split, разбивают по символу и берут последнее/первое/любое другое вхождение. С точки зрения результата выходит то же самое - извлечена нужна подстрока. Поэтому и сравниваю.

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

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

Во первых коммунизм это конец эволюции и следовательно это конец всему живому. Это камни. Они бывают красивыми, но они не бывают живыми. Так что с точки зрения науки коммунизм это утопия.

Во вторых, я вам пишу, что надо уметь ПИСАТЬ, а читать тут люди умеют и я в том числе.

В третьих я вам также стал отвечать с конца, как у вас статья и написана. Сначала что ваш код странный, потом когда люди не поняли о чем я, добавил что статья написана неправильно.

Но вы похоже все ещё похоже не поняли почему... А все потому что я отвечаю вам ровно также как у вас написана статья. Она написана задом наперёд. И начинается с одного, а потом перескакивает на другое.

Только со слов ' Наша задача – извлечь имя файла с расширением стандартным инструментарием C#.' становится понятно что вам собственно надо. И почему вы сравниваете какие то странные методы.

А надо вам не извлечь подстроку (по индексу и длине, длина опциональна), а вам надо найти последнее вхождение символа и вернуть часть строки после этого символа. Тут два действия, а не одно. Так бы и написали в заголовке как быстро получить имя файла из полного пути к файлу.

А слова `стандартным инструментарием C#` вообще не совсем соответствуют действительности, потому что самый стандартный для этого путь это использовать класс Path. И только если НАДО быстрее то использовать что то другое. Обычно этого НЕ НАДО и быстродействия класса Path вполне хватает.

Как я полагаю, чисто в задаче извлечения подстроки Span (AsSpan+ToString) вряд ли будет существенно быстрее, чем стандартный метод: все равно память потребуется выделять один раз, под результат и копировать результат туда. Я прав?

Но вот если потребуется обработка, которую одним стандартным методом не сделаешь, то там разница может быть существенной. Если будете писать статью про Span - мне был бы интересен именно этот аспект. И - сравнение с использованием не String, а StringBuilder как альтернативы.

Был бы рад прочитать.

Да, вы правы. В аналогичном сценарии Span даже работал чуть медленнее. Но если не вызывать ToString, то естественно Span быстрее.

На самом деле про Span статья уже давно есть, правда она на английском. И писалась она мной до того, как прочитал книгу про бенчмарки. Так что возможно там есть какие-то ошибки в замерах и я хотел сделать ревью перед публикацией на Хабр.

По поводу сравнения со StringBuilder. Какие именно сценарии интересуют? Вариантов просто может быть масса :)

По поводу сравнения со StringBuilder. Какие именно сценарии интересуют? В

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

А теперь просьба повторить тоже самое со строками в кодировке utf8. Причем заведомо содержащими символы длиной от одного до четырех байт.

Так не бывает строк таких в дотнете. string всегда в UTF-16. Который вполне себе тоже содержит символы длиной от двух до четырех байт.

Я просил со строками, а не с типом string. Данное выражение в C# совершенно корректно:

var str = "Hello, 世界!"u8;

UTF-16. Который вполне себе тоже содержит символы длиной от двух до четырех байт.

Длиной два или четыре байта. Но уж никак не от двух до четырех.

Выражение-то корректно, только имеет тип ReadOnlySpan<byte>. Его в строковых операциях не использовать нигде, только в стрим писать байтами и остаётся.

Это Вы зря. Под Linux в контейнерах только в UTF-8 и живем. А куда деваться, если и БД в UTF-8, и клиенты? Например, npgsql живет исключительно в UTF8. А UTF-16 почти ничем не поддерживается.

Внутри программ вы же надеюсь используете стандартные строки?

Так в стандартные строки и UTF-8 спокойно влетает. Просто надо учитывать, что большинство родных методов корректно работать не будет.

Я вас не понимаю. Какая разница какая кодировка в БД, или у ваших клиентах и т.д. Для вас это все прозрачно перекодируется. Или вы о чем то другом?

Прозрачно - это не значит что без потребления ресурсов.

Если сервис, в основном, занят просто перекладыванием из рекордсета полученного из БД в ProtoBuf для gRPC или Confluent, то две дополнительные перекодировки у Вас составят добрую половину всех потребленных ресурсов CPU.

Если же для Ваших задач производительность JSON или даже XML устраивает - можете забыть о таких проблемах.

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

специфичные задачи, и не типичные для шарпа

Будь моя воля, я бы писал это на C++ или Rust. Но, до недавнего времени, вся инфраструктура клиента была построена на технологиях MS. Поэтому почти все разработчики знают только C#. По общеизвестным причинам, надо перенести монолит с MS SQL и SSAS на микросервисную архитектуру с k8s, PostgreSQL, Confluent, Redis, ClickHouse и т.п.

Вот и хреначим на C# эти микросервисы сотнями.

В сторону модульной архитектуры не смотрели? Это намного проще чем микросервисы и производительнее и достаточно гибко.

Не вариант. Там гетерогенная среда. Несколько петабайт суммарно во всех БД. И монументальная Hana с которой приходится интегрироваться в обе стороны.

производительнее

Сомневаюсь. С горизонтальным масштабированием в микросервисной архитектуре все же получше. А тут только в логистике почти десяток кластеров MS SQL остался, БД с которых постепенно распиливаются и переносятся на PostgreSQL. Я когда увидел впервые эти списки из десятков MS SQL Linked Server, на которых строилась интеграция - мне аж дурно стало.

Модульная архитектура отлично горизонтально масштабируется. Не хуже микросервисов. Одновременно с этим она позволяет объединять в одном процессе плотно взаимодействующие между собой сервисы, что позволяет не гонять данные через сеть и протобуфер. В общем правильно написанная имеет преимущества и монолита и микросервисов одновременно. При этом любой из сервисов легко выделить в отдельный процесс или наоборот объединить нужные сервисы в одном процессе. Отдельные процессы могут содержать как один сервис (модуль) так и содержать несколько плотно взаимодействующих сервисов (модулей) и избежать лишней нагрузки на сеть. Позволяет тасовать сервисы по процессам как удобно и производительнее в данный момент.

Модульная архитектура отлично горизонтально масштабируется. Не хуже микросервисов. Одновременно с этим она позволяет объединять в одном процессе плотно взаимодействующие между собой сервисы, что позволяет не гонять данные через сеть.

Вам не кажется, что выделенное противоречит друг другу. Горизонтальное масштабирование по определению требует "гонять данные через сеть".

и протобуфер

А это вообще не понятно. gRPC/ProtoBuf один из самых эффективных RPC, существующий на данный момент. А если у система должна работать на сотнях физических серверов, так как даже крупнейший мейнфрейм её не потянет, то без RPC эффективную связь между модулями не построить.

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

А вот про это попрошу подробней. Как динамически выполняется такая балансировка для микросервисов в кластере серверов k8s я понимаю. Там в любом случае коммуникация между ними производится через сокеты и сервису безразлично, это локальная коммуникация средствами KVM или сетевая. Но как реализовать это при модульной архитектуре, тем более в гетерогенной среде (Windows и Linux)?

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

Вы хоть понимаете, что речь идет автоматизации холдинга из десятка компаний, две из которых в ТОП-20 в РФ по версии Forbes?

Сначала производятся исследования которые показывают какие сервисы выгоднее иметь внутри одного процесса.

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

Если вам нужен сервис внутри процесса, то грузится сам сервис, и взаимодействие происходит без участия сети и буферов. Если распределённый сервис, то его прокладка, которая и общается с ним через сеть и т.д. В общем конфигурация более сложная чем когда везде микросервисы через сеть.

Все это основано на DI и конфигурациях.

Какое отношение вышеизложенное имеет к заданным мной вопросам?

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

У меня нет времени подробно ответить вам. Да и более профессиональные ответы легко найти в интернете.

Ищите в сторону modular monolith, modular monolith vs microservices и т.д.

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

Чисто микросервисная архитектура по моему мнению уступает модульной.

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

Вот недавно рисовал архитектуру растаскивания кластера из трех серверов СУБД у которых по 2 ТБ RAM в каждом. Третий сервер в кластере дал в районе 20% прироста производительности. Четвертый подключать смысла нет вообще. Сами сервера наращивать уже тупо некуда. Остается только функционально пилить. Чем тут может помочь модульная архитектура?

Чисто микросервисная архитектура по моему мнению уступает модульной.

В том то и дело, что почти везде утверждается обратное:

Модульная архитектура хороша для относительно небольших систем, а для крупных систем она неприемлема.

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

У вас на таких данных и чистые микросервисы (без прочих изменений) так же не справятся.

Почему это вдруг? Это уже не первый распиленный монолит на проекте. И даже не самый крупный. Ведь в горизонтальном масштабировании никто не ограничивает.

Самое сложное в таком распиливании - это MDM. А когда топики разрисованы и кеширование спроектировано, остальное уже не так сложно.

сколько ресурсов будет впустую потеряно

Но зато будет работать. А иначе - просто сдохнет. Понятно что издержки есть.

Плюс микросервисной архитектуры в том, что есть ограничение по ресурсам только физического сервера. А ограничений по суммарным ресурсам нет.

Следует отметить, что ProtoBuf реально даёт большой выигрыш по загрузке сети. Мы опасались, что старые сервера с 80 гигабит (2x40GBASE) могут упираться в производительность сети. Но этого не происходит и переход на 200 гигабит происходит самотеком.

терабайтную базу

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

В модульной архитектуре каждый модуль примерно соответствует одному микросервису по функционалу.

Модули взаимодействует с другим модулем через API, предпочтительно через слабосвязанную асинхронную связь.

Каждый такой модуль можно запустить как отдельный процесс — микросервис. Которые можно горизонтально масштабировать.

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

Несколько модулей в одной сборке часто позволяет уменьшить нагрузку на инфраструктуру. Данные могут передаваться в одном процессе, а не гоняются по сети или сетевым протоколам. Конечно же такие сборки хорошо горизонтально масштабируются, одна сборка может быть запущена на многих узлах (виртуальных или реальных)

Самая большая разница между модулями и микросервисами заключается в том, как они развёртываются. Каждый микросервис развёртывается как отдельная сборка, в то время как модуль может быть развернут и как отдельная сборка и в составе более крупной сборки с несколькими модулями.

Модули взаимодействует с другим модулем через API, предпочтительно через слабосвязанную асинхронную связь.

То есть, тот же самый gRPC? Но без возможности отказоустойчивой связи через Кафку или Кролик? Так это получается только менее устойчиво, чем в микросервисной архитектуре и требует существенно больше забот при восстановлении после сбоев. Тупо топик не перечитаешь и пропущенное сообщение никто тебе не пришлет.

В тоже самое время можно объединять несколько модулей в одну сборку, например на машинах разработчиков.

А вот в этом смысла не вижу. Пусть уж лучше падает один сервис, а не сразу их группа. А разработчику никто не запрещает локальный k8s запустить, если вдруг ему это захотелось.

Несколько модулей в одной сборке часто позволяет уменьшить нагрузку на инфраструктуру.

Каким образом? Ну если, конечно, руки не кривые и so не были выложены на одном и том же слое в образе.

К тому же лишает k8s возможности поднимать дополнительный экземпляр только одного микросервиса, не справляющегося с нагрузкой.

Вы, получается, строки совсем не используете в программе, только байтовые массивы? Это же очень сильно неудобно? И выгоды никакой, потому что перевод кодировки должен или автоматически происходить, через объект Connection к базе, или вручную в момент отправки наружу через StreamWriter, настроенный на нужную кодировку?

Вы, получается, строки совсем не используете в программе, только байтовые массивы?

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

И выгоды никакой

Ничего себе никакой! Мало того, что минимум, минус две перекодировки совсем не маленьких массивов информации, так еще и заметная экономия памяти.

объект Connection

Я уже указал выше, что, например, PostgreSQL поддерживает только UTF-8. Confluent, Redis, ClickHouse - тоже. Ну тупо нет смысла хоть где-то UTF-16 использовать, так это потребует, как минимум, два лишних перекодирования.

А статья и сравнение есть, сколько экономится памяти и времени? Посчитано? А то вдруг нам всем тоже надо так делать.

А статья и сравнение есть, сколько экономится памяти и времени?

Если будет время, могу попробовать посчитать.

А то вдруг нам всем тоже надо так делать.

Странный вопрос. Если из сотни полей в UTF-8 рекордсете из десятков миллионов строк парсятся только одно-два, а остальные передаются по gRPC или в Кафку тоже в UTF-8 - то точно надо так делать, так как перекодировки займут добрую половину времени CPU.

А если еще что-то кешируется в Redis, в котором все ключи по жизни в UTF-8 - то тем более.

так как перекодировки займут добрую половину времени CPU.

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

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

Зря думали. Рекордную скорость перекодировки UTF-8 в UTF-16, которую я нашел - 4 гигабайта в секунду. Но это средствами AVX-512 на ассемблере. Там же сравнивают с ICU - всего 0.5 ГБ в секунду. Поэтому двойная перекодировка 0.5 ГБ займет в C# больше секунды. Сравнивать со скоростью локальных сокетов KVM между контейнерами тут даже смысла нет, так как разница в два порядка. Что же касается физической сети, то в худшем случае, если сервера в стойке не на 100GBASE, а на 40GBASE, то все равно передача по сети в ~5 раз быстрее, чем двойная перекодировка.

мне кажется сложнее парсить

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

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

Неправильно помните. Сравнивать/сопоставлять он умеет не только бинарные величины, но, кроме бинарного, поддерживая исключительно UTF-8 COLLATION.

Очень странная статья, в духе:

Давайте сравним что лучше подходит для хирургической операции: скальпель, швейцарский нож, кухонный комбайн или газонокосилка.

Удивительный результат - побеждает скальпель. Вот только в чем тут полезность статьи? Не использовать для задачи то, что для нее не подходит? Это было понятно и без бэнчмарков.
Не понимаю, откуда плюсы у этой статьи.

Мир был бы прекрасен, если бы все разработчики были такими как вы и знали как пользоваться инструментами. :) Но, к сожалению, приходится видеть код, который я и разбирал в статье. А значит понятно не всем. Хотелось наглядно показать, особенно начинающим специалистам, почему не надо использовать газонокосилку, где нужен скальпель.

Чутка пооптиомизировал Linq

Изначальный код

 _result[i] = new string(s.TakeLast(s.Length - start).ToArray());

Результат:

| Method             | Mean      | Error    | StdDev   | Ratio    | RatioSD | Gen0       | Gen1      | Gen2      | Allocated | Alloc Ratio |
|------------------- |----------:|---------:|---------:|---------:|--------:|-----------:|----------:|----------:|----------:|------------:|
| Substring          |  19.36 ms | 0.386 ms | 0.565 ms | baseline |         |  1718.7500 | 1687.5000 |  531.2500 |   7.16 MB |             |
| RangeOperator      |  18.58 ms | 0.345 ms | 0.398 ms |      -5% |    3.7% |  1718.7500 | 1687.5000 |  531.2500 |   7.16 MB |         +0% |
| Linq               | 156.06 ms | 3.082 ms | 8.120 ms |    +707% |    6.3% | 14000.0000 | 4500.0000 | 2000.0000 |  71.94 MB |       +904% |
| Split              |  53.87 ms | 1.072 ms | 2.629 ms |    +179% |    5.6% |  3400.0000 | 1500.0000 |  500.0000 |  17.34 MB |       +142% |
| RegularExpressions | 108.32 ms | 2.542 ms | 7.455 ms |    +443% |    6.5% |  5800.0000 | 1800.0000 |  800.0000 |  30.83 MB |       +331% |

Выкидываем ToList

_result[i] = string.Concat(s.TakeLast(s.Length - start));
| Method             | Mean      | Error    | StdDev   | Ratio    | RatioSD | Gen0      | Gen1      | Gen2      | Allocated | Alloc Ratio |
|------------------- |----------:|---------:|---------:|---------:|--------:|----------:|----------:|----------:|----------:|------------:|
| Substring          |  19.84 ms | 0.392 ms | 0.736 ms | baseline |         | 1718.7500 | 1687.5000 |  531.2500 |   7.17 MB |             |
| RangeOperator      |  19.66 ms | 0.393 ms | 0.846 ms |      -0% |    4.6% | 1718.7500 | 1687.5000 |  531.2500 |   7.17 MB |         +0% |
| Linq               | 129.69 ms | 2.583 ms | 6.982 ms |    +562% |    6.2% | 8250.0000 | 2500.0000 | 1000.0000 |  44.29 MB |       +518% |
| Split              |  52.92 ms | 1.038 ms | 2.365 ms |    +168% |    5.6% | 3272.7273 | 1363.6364 |  454.5455 |  17.33 MB |       +142% |
| RegularExpressions | 103.68 ms | 2.057 ms | 5.491 ms |    +421% |    6.3% | 5800.0000 | 1800.0000 |  800.0000 |  30.82 MB |       +330% |

а так?

_result[i] = string.Concat(s.Skip(start));
| Method             | Mean      | Error    | StdDev   | Ratio    | RatioSD | Gen0      | Gen1      | Gen2     | Allocated | Alloc Ratio |
|------------------- |----------:|---------:|---------:|---------:|--------:|----------:|----------:|---------:|----------:|------------:|
| Substring          |  19.31 ms | 0.380 ms | 0.614 ms | baseline |         | 1718.7500 | 1687.5000 | 531.2500 |   7.16 MB |             |
| RangeOperator      |  19.07 ms | 0.330 ms | 0.620 ms |      -1% |    4.6% | 1718.7500 | 1687.5000 | 531.2500 |   7.18 MB |         +0% |
| Linq               |  61.22 ms | 1.208 ms | 2.847 ms |    +218% |    5.3% | 2875.0000 | 1250.0000 | 375.0000 |  15.55 MB |       +117% |
| Split              |  55.70 ms | 1.099 ms | 2.343 ms |    +188% |    5.2% | 3400.0000 | 1500.0000 | 500.0000 |  17.34 MB |       +142% |
| RegularExpressions | 106.09 ms | 2.996 ms | 8.834 ms |    +477% |    5.6% | 5200.0000 | 1400.0000 | 200.0000 |  30.81 MB |       +330% |

Отлично. )) Да, с LINQ вариантов масса. Такие способы ещё не доводилось на практике видеть (надеюсь и не придётся).

Мне кажется что для полноценной статьи со сравнением нужно было ещё замерить самодельный код, который проходит строку посимвольно. А то бывает люди не верят что substring оптимальное решение...

Правильно написанный свой код будет по скорости практически равен стандартному методу. Думаю что разница будет в пару процентов.

А вот и победитель

    [Benchmark]
    public string[] Path()
    {
        for (int i = 0; i < _data.Length; i++)
        {
            _result[i] = System.IO.Path.GetFileName(_data[i]);
        }

        return _result;
    }
| Method             | Mean      | Error    | StdDev   | Ratio    | RatioSD | Gen0      | Gen1      | Gen2     | Allocated | Alloc Ratio |
|------------------- |----------:|---------:|---------:|---------:|--------:|----------:|----------:|---------:|----------:|------------:|
| Substring          |  25.52 ms | 0.510 ms | 0.546 ms | baseline |         | 1718.7500 | 1687.5000 | 531.2500 |   7.16 MB |             |
| RangeOperator      |  23.17 ms | 0.462 ms | 1.133 ms |      -5% |    4.7% | 1718.7500 | 1687.5000 | 531.2500 |   7.18 MB |         +0% |
| Linq               |  84.01 ms | 1.665 ms | 2.044 ms |    +228% |    3.7% | 3000.0000 | 1285.7143 | 428.5714 |  15.57 MB |       +117% |
| Path               |  25.13 ms | 0.501 ms | 0.929 ms |      +1% |    3.2% | 1718.7500 | 1687.5000 | 531.2500 |   7.17 MB |         +0% |
| Split              |  69.23 ms | 1.377 ms | 1.414 ms |    +172% |    2.5% | 3375.0000 | 1500.0000 | 500.0000 |  17.35 MB |       +142% |
| RegularExpressions | 156.91 ms | 3.078 ms | 4.109 ms |    +513% |    3.6% | 5666.6667 | 1666.6667 | 666.6667 |  30.82 MB |       +330% |

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории