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

Как продеть слона через игольное ушко. Обработка максимальных объемов данных за минимальное время

Время на прочтение12 мин
Количество просмотров27K
Всего голосов 26: ↑19 и ↓7+12
Комментарии27

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

Совсем пропущены языки нового поколения (Go/Rust), от которых есть ожидание революции и чуда по смещению Си с на«Си»женных позиций.
--Совсем пропущены языки нового поколения (Go/Rust), от которых есть ожидание революции и чуда по смещению Си с на«Си»женных позиций.

Что-то может и изменится если на этих языках напишут полноценную ОС с драйверами, блекжеком и шлюхами.
А зачем переписывать то, что уже и так отлично работает? Просто чтобы доказать «можем»?

Я думаю, появление быстрого браузера будет достаточным аргументом. Пока что ни один интерпретируемо-опекаемый-gc-в-уютной-песочнице язык приличного браузера не породил, а задачи производительности, которые стоят перед создателями браузеров весьма и весьма близки к задачам, которые решают программисты в районе «ой, как бы мне MSI-X по ядрам распихать поудобнее».
А как насчет оптимизации по попаданию в кэш?
… А так же per-process queue, потому что в NUMA доступ к памяти соседнего сокета очень болезенный. И?

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

Если же кто-то хочет указать на тесную спайку всяких gcc-шных расширений для полу-ассемблерного написания, а то и самого что ни на есть ассембера в виде вставок, так их в любой компилируемый (в нативный код) язык можно делать с одинаковым успехом. Некоторые умники мне даже показывали «ассемблер в хаскеле», что уж про Rust/Go говорить.
Я говорил про NUMA?

Как оптимизировать программы на Си вполне известно. Для Си есть качественные компиляторы с различными оптимизациями. Модель памяти в Си проще, в ней нет GC/подсчета ссылок за которые надо платить.
В Rust'е подсчёта ссылок тоже нет, кстати. И GC нет.

А «есть качественный компилятор» — какое это отношение к языку-то имеет? Кстати, предполагается, что компилятор rust'а некачественный?
НЛО прилетело и опубликовало эту надпись здесь
Я тоже так думал, пока не столкнулся с embedded разработкой. Там, на одном очень редком, маломощном процессоре приходилось держать несколько tcp соединений, обрабатывать данные по последовательному порту, и предоставить api для управления модулем по сети. Вот тут то я и начал оптимизировать свой код, и, как оказалось, к очень многим выводам пришел как и автор. Только я в итоге понял их методом проб и ошибок, больше года, а здесь всего за семь статей можно было это узнать.
И, мое имхо, на читаемости кода это никак не сказалось в худшую сторону, а во многим местах код стал прозрачнее. Особенно про сереализацию и десериализацию это актуально. Когда вы передает json и храните его представление, и чтобы что -то извлечь читаете его — это выглядит значительно сложнее, чем получить массив данных, единожды засунуть его в константный shared_pointer, а нужные данные извлекать просто по индексу в массиве, разименовывая указатель.
НЛО прилетело и опубликовало эту надпись здесь
Ну так я заметил, что код в читаемости и гибкости не утратил, но стал быстрее. Поэтому если знаешь, то лучше писать так сразу.
Замечательно, действительно, надо запретить себе даже думать об указателях и представить что их нет. Правда что ли? 14-й стандарт отменяет использование указателей? Наиболее эффективное и оптимизированное построение алгоритма происходит именно на основе умелого управления памятью напрямую из кода C/C++. Можно сколько угодно отговаривать новичков от использования указателей в коде, но в этом случае из них не вырастет специалистов, умело управляющихся с памятью вручную. Именно такая магия и нужна как крупным компаниям, так и маленьким стартапам, и именно от получения этих бесценных навыков ты отговариваешь новичков? Не скажу, что очень уж ценный совет для начинающих.
В 17ом стандарте обещают класс string_view для работы над подстроками, но всегда можно его самому навелосипедить или взять StringRef из llvm, но зачем копирования то заведомо лишние плодить?
НЛО прилетело и опубликовало эту надпись здесь
Если большинство слов не помещается в изначально зарезервированный в `std::string` буфер размером 16 char

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

char const *finish = text + std::strlen(text);

Зачем пробегаться по тексту лишний раз, ели всё равно strlen расчитан на null-terminated строку.

Когда нужен именно массив с поэлементным доступом и недорогим увеличением размера, смотри лучше в сторону `std::deque`

Ну вот смотрю я push_back(), а сложность для deque — константная, а для vector — амортизированная константная. Что это значит? Утверждение, без каких либо объяснений.

Для класса field, как по мне нужно хотя бы упомянуть о конструкторе копирования/операторе присваивания и, вообще, m_buffer это целая эпопея для тех, кто знаком с memory alignment.
Это всё здорово, но вычисление длины null-terminated строки фактически не влияет на замеры, по сравнению с заполнение N-го числа std::string это как раз копейки. Можно нивилировать стандартной передачей указателя на конец, аналогичного итератору end(). push_back() у std::vector запросто может пойти за новой памятью, и поэлементно будет перемещать все элементы из старого блока в новый, std::deque'у это не грозит. С alignment знакомы, пожалуй, все, m_buffer на это никак не влияет, разница между выделением памяти внутри самого объекта или в куче, да или на стеке, где угодно, для alignment нулевая.
Вот только когда требуется создать no latency или low latency сервис или нужно сэкономить память и время выполнения узкого места обработки больших объемов данных, то тут же прибегают за помощью к «архаичным» разработчикам на C/C++. Просто потому, что эти ребята умеют вручную управлять памятью и прекрасно представляют, что за начинка у той или иной высокоуровневой операции. Сегодня наша задача — стать на шаг ближе к этим ребятам.

Ну и пафос

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

Термин «векторизация» общепринято означает нечто совершенно другое. Вообще, конечно, «вектор» — дебильное имя для структуры, которая на самом деле называется «динамический массив».

Но если соотношение ключ — значение часто изменяется (удаляются соотношения, добавляются новые, и это делается достаточно интенсивно), то проще смириться с поддержанием `std::map`, чем раз за разом перестраивать внутреннее представление `std::unordered_map`. Ведь внутри `std::unordered_map`, по сути, массив цепочек значений, и чем чаще мы изменяем соотношение ключ — значение, тем менее эффективным становится его использование. Здесь не спасет даже более быстрое извлечение по ключу: перестроение больших массивов — это всегда дорого.

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

Главное, что нужно усвоить, — язык C++, как и си, предоставляет прямой доступ к памяти процесса, причем в первую очередь важна память в стеке, а во вторую — память внутри заранее выделенных и подготовленных к использованию буферов. Никакие Java, C# или Python и близко не подойдут к показателям программ, грамотно написанных на C/C++, именно потому, что защищают программиста от неправильной работы с памятью.

Все можно и в Java, и в C#.
Векторизация памяти — это фактически оптимизация работы с оперативной памятью на уровне инструкций CPU, и std::vector для этого отлично подходит. Имя структуры std::vector совсем никак не связано с понятием «динамический массив», как и std::deque. В Java и C# доступ к памяти напрямую настолько же убог и специфичен, насколько и ограничен, сколько всего нельзя в том же unsafe блоке C#, по факту это аналог ассемблерной вставки в С/C++ зачастую абсолютно ненужная операция для высокоуровневых языков. В С/C++ доступ к памяти осуществляется на уровне понятий языка, оптимизация работает как надо и всё можно, что и обычно, это нормальная практика, в отличие от Java/C#.
Главное, что нужно усвоить, — язык C++, как и си, предоставляет прямой доступ к памяти процесса, причем в первую очередь важна память в стеке, а во вторую — память внутри заранее выделенных и подготовленных к использованию буферов. Никакие Java, C# или Python и близко не подойдут к показателям программ, грамотно написанных на C/C++, именно потому, что защищают программиста от неправильной работы с памятью.

В C# и прямая работа с памятью есть и stackalloc и unsafe блоки. Очередное некачественное сравнение?
Как я уже написал выше, unsafe блок крайне ограниченная по своему применению структура языка C#, что именно там можно, а что нельзя. Можно также вспомнить про Marshal и IntPtr, но это вообще доступ к коду, фактически написанному на C/C++ и больше подходит для обёртки, чем для полноценной работы.
Ни ранее, ни выше, ни между строк я не увидел описания этих ограничений. Marshal и IntPtr вообще не являются средствами языка, а просто обертка для unsafe-операций. Вроде такой.

public static unsafe byte ReadByte(IntPtr ptr, int ofs)
{
	…
	byte *addr = (byte *)ptr + ofs;
	return *addr;
	…
}


К чему Вы их упомянули? В CLR есть много более эффективных способов работы с памятью. В том числе safe, умеющие работать с динамическими структурами, которые не определены на этапе компиляции и т.п. По скорости на порядок опережающие Marshal. Или Вы не знали?
Я знаю C# достаточно хорошо, как и .NET Framework в целом и CLR в частности, я говорю лишь о том, что unsafe конструкции не являются нормальной практикой языка и их использование всегда несёт некоторые ограничения, не говоря уже об обёртках из namespace Marshal. Их применение сродни ассемблерной вставке и в этом случае я предпочитаю написать честный нативный модуль и работать с памятью из C/C++, а уж точно не из C#, который в этом случае использовать неэффективно иначе, кроме как для вызова кода обёрток методов.
Ну раз Вы такой профи, то ситуация еще проще. Давайте расчехлим отладчики и пройдемся по коду. Там и увидим, насколько непопулярен unsafe, как редко используются динамические «ассемблерные вставки». Прям на Вашем же примере JSON'а посмотрим, как NetJSON или Jil «небезопасны», как JSON.NET и почти все другие сериализаторы осуществляют эмиссию кода. Померяемся бенчмарками. А то за все время, что Вы говорите о факте существования ограничений с unsafe их можно было уже десять раз показать. У Вас есть перед глазами результат JITа unsafe кода, что Вы так о нем говорите? Должно же быть какое-то подкрепление всех этих буков.
Ну и чего обижаться-то? Как можно сравнивать листинг IL-кода против нативных инструкций скомпилированного и оптимизированного кода на C/C++? Каким бы шустрым CLR ни был, это всё равно лишняя трансляция в машинные инструкции. Хотите померяться с сишником скоростью выполнения managed кода против нативного. Весьма похвально стремление, с каким отстаивается точка зрения и любимые технологии, но это малость опрометчиво. Сериализацию вообще на C# или Java писать не очень эффективно, но если есть пример, где прямо по бенчмаркам C# библиотека уделывает все сишные, я бы на это посмотрел.
НЛО прилетело и опубликовало эту надпись здесь
Не стоит забывать о том, что мапа всё-таки сортирована по ключам. Иногда это важно.
НЛО прилетело и опубликовало эту надпись здесь
Зарегистрируйтесь на Хабре, чтобы оставить комментарий