понедельник, 27 июня 2022 г.

Реализация мьютекса в libstdc++

При общении с коллегами возник вопрос, на сколько затратна операция использования std::mutex в libstdc++.


В Linux mutex работает через системный вызов futex, который освобождает процессор и добавляет поток в очередь, если мьютекс занят.

Futex - это сокращение над Fast userspace mutex. Смысл в том, что перед тем как выполнять дорогостоящее обращение к ядру, выполняется проверка флага с использованием атомарных операций, и только если мьютекс действительно занят, то текущий вызов будет поставлен в очередь, которую предоставляет и обслуживает ядро.

Всё что сказано выше - пока просто справочная нформация, которую можно легко загуглить. Интересней другое: если флаг мьютекса занят, то выполняется ли при этом так называемый SPIN WAIT wait перед обращением к ядру.

Такой подход используется, например, в библиотеке .Net framework.

Под Windows std::mutex реализован с использованием spin wait, при этом блокировка крутится заданное число циклов (~4000), и уже по истечении этого времени поток засыпает.

Однако, как выяснилось, в libstdc++ механизм spin wait не реализован. В коде разбросаны комментарии вида:

// TODO Spin-wait first.

В мануале также нет информации по поводу spin wait.

Таким образом, до тех пор пока потоки не конлфиктуют за право владения объектом std::mutex, этот механизм работает крайне быстро и только в user space. Но вот когда начинают возникать блокировки, то сразу появляется необходимость обращения к ядру. Такие обращения сами по себе уже вызывают 10 тыс. циклов CPU. Поэтому не безосновательно принято считать использование мьютексов дорогостоящей операций.

При работе с мьютексами не стоит забывать два основных правила:

- Код под блокировкой мьютекса лучше всего свести к минимуму.

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

воскресенье, 30 января 2022 г.

Проверка длины строки в GRPC запросах

Введение

Многие наверное сталкивались со следующей проблемой: веб присылает строку, состоящую из русских символов длиной 1000 символов, однако у нас на сервере при валидации grpc запроса строка не проходит проверку по той причине, что ее длина превышает ограничение в 1024 символа.

Если в GRPC сервисе необходимо убедиться, что количество символов в переданной строке не превышает установленного лимита, то следует принять во внимание тот факт, что стандартный метод std::string::size возвращает количество байт, а не число символов в строке. Дело в том, что русские символы будут представлены в юникоде, и каждый символ строки будет кодироваться двумя байтами. таким образом в примере выше метод std::string::size вернет значение 2000.

Работа с UTF-8 в стандартной библиотеке

Для выполнения преобразования из одной кодировки в другую с последующим подсчет количества символов можно использовать std::codecvt. 

int main()
{
    vector<string> data = {
        "proverka",
        "проверка",
        "проverка",
        "勇气忠诚命运福福",
        "𤭢𤭢𤭢𤭢𤭢𤭢𤭢𤭢"
    };
    std::wstring_convert<std::codecvt_utf8<char32_t>, char32_t> cvt;
    for (const auto& str : data)
    {
        cout << "String = " << str
             << ", bytes count = " << str.size()
             << ", chars count = " << cvt.from_bytes(str).size()
             << endl;
    }
    return 0;
}




Однако, в стандарте классы wstring_convert и codecvt_utf8 помечены как deprected in C++17.

В стандарте приводят следующие причины деприкации:

  • As the Unicode standard and security experts point out, ill-formed UTF can be and has been used as an attack vector. Yet the <codecvt> facets don't provide the safe forms of error handling as their defaults, and it is far to hard to use them in a safe way.
  • I've wanted to use <codecvt> in production code three or four times where turned out to not be suitable, but because the spec is so obscure, it took far too much time to realize that.
  • In the couple of cases where <codecvt> would have been suitable, I couldn't use it because the code had to be portable, yet libstdc++ didn't support until very recently. That, coupled with the fact that is a new header, means there just aren't a lot of users.
  • In published usage surveys like BuiltWith, use of formerly popular non-UTF encodings like Shift-JIS and Big5 is plummeting compared to UTF encodings. The standard library badly needs modern C++ UTF conversion facilities, yet <codecvt> is an embarrassment. Let's deprecate it to clear the path for the future.

Альтернативное решение: ICU

Среди различных third-party библиотек аналогичный функционал есть в icu.

#include <unicode/unistr.h>
 
namespace utils
{
size_t GetUnicodeStringSize(const std::string& str)
{
    icu_69::UnicodeString uni(str.c_str());
    return static_cast<size_t>(uni.countChar32());
}
// namespace utils

В тестах получаем такие же результаты, как и при использовании codecvt.

Здесь главное не перепутать UnicodeString::length и UnicodeString::countChar32.

Функция UnicodeString::length считает количество code units.

Функция UnicodeString::countChar32 - считает количество code points.

В UTF-8 code units - это единица кодировки. Для русских символов code unit составляет два байта, соответственно число code units показывает количество символов. Однако для экзотических языков, возможна своя специфика. Так для такого азиатского иероглифа '𤭢' один символ занимает 4 байта и два code units, т.е. количество code units не будет равно количеству символов в строке.

Для подсчета количества символов нам правильней считать именно code points. Количество code points - это как раз число символов в строке в представлении юникод.

Альтернативное решение boost::locale

Также упомяну вариант работы с UTF-8 через библиотеку boost::locale. 

boost::locale::conv::to_utf<wchar_t>(str.c_str(), "UTF-8").size();

Однако на сайте boost.org пишут, что эта библиотека использует под капотом codecvt (который собственно depricated). 
Вроде можно также пересобрать boost::locale с использованием ICU.

воскресенье, 22 сентября 2019 г.

C++ Russia 2019 (Москва)

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

Nicolai Josuttis - C++17 — the biggest traps

Nicolai Josuttis рассказал про:
1. Различные способы инициализации переменных в С++.
2. Показал примеры использования filesystem в С++ 17.
3. Различные варианты использования constexpr, включая if constexpr
4. Fold expressions - полезнейшая фича, которая появляется в С++ 17.
5. Особенности использования string_view.
6. Использование параллельных алгоритмов с std::execution::par
7. std::optional и std::variant.
8. CTAD (Class template argument deduction) и deductions guides
Ждем с нетерпением книгу C++17 - The Complete Guide by Nicolai M. Josuttis (http://www.cppstd17.com/).

Олег Фатхиев - Эволюция метапрограммирования: как правильно работать со списками типов

Автор в своем докладе начал с простых примеров использования метапрограммирования и закончил метафункциями, которые по заданной лямбде фильтруют или выполняют другие операции над списками типов.
Понравилось то, что он в каждом примере приводил три варианта решения:
1. Простой с использованием классических средств метапрограммирования, которые были доступны еще до С++ 11. Например, рекурсия для прохода по списку типов.
2. Улучшенный с использование variadic templates и sizeof...
3. Самый лучший с использованием fold expressions и возможностей constexpr, который появились в С++ 17. В этом варианте получался наиболее красивый и лаконичный код.
Также докладчик продемонстрировал интересные приемы с использованием std::index_sequence.

Know your hardware: CPU memory hierarchy - Alexander Titov

Интересный доклад, в этот раз он был на английском, но при желание можно посмотреть его видеозапись на русском с конфы corehard (https://www.youtube.com/watch?v=rBMN_yyzi18).
Из интересных моментов можно выделить следующие:
- Не забывать про принцип локальности данных.
- Принудительная упаковка - это зло. Например, если в структуре, для которой включена принудительная упаковка (pragma pack), присутствует atomic<uint64_t>, и так совпало, что это поле оказалось на границе сразу двух 64-байтовых линий кеша процессора, то это может вызвать замедление в 300 раз. Если принудительная упаковка не включена, то такое никогда не произойдет, так как компилятор позаботится о том, чтобы атомик целиком поместился на одну кеш линию.
- Не допускайте, чтобы данные с которыми работают разные потоки оказывались в одной линии. Это также приведет к замедлению, а точнее к отсутствию ускорения, на которое расчитывал автор, добавляя потоки. Например, есть массив int thread_results[4], в который периодически пишут 4 потока, каждый в свою ячейку. Так как все 4 элемента массива скорее всего попадут в одну кеш линию, то это будет проблемой.
- По возможности следует отделять горячие данные, с которыми вы работаете часто, от холодных данных, которые используются редко. Например, есть структура Mixed { int warm; int cold[15]; }, и есть какой-то большой массив таких структур, с которым работает ваш алгоритм. При этом алгоритм постоянно обращается к полю warm и лишь изредка к полю cold. В этом случае кеши процессора используются крайне не эффективно, так как в кеш то и дело попадают данные cold, которые не используются. Как вариант, эти данные можно было бы разделить, т.е. сделать отдельный массив, в которым лежали бы только данные warm, и отдельный массив для данных cold.
- Докладчик порекомендовал почитать по этой теме: intel architectures optimization reference manual
- А также посмотреть курс: high-performance computing and concurrency

Шаблоны C++ и базы данных - Сергей Федоров

Доклад получился на удивление годным. Докладчик на реальном примере из практики показал, как if constexpr (C++ 17) помогает упростить код и сделать его более лаконичным, избавившись от разных устаревших практик, таких как, например, Tag Dispatching.
Также продемонстрировал много интересных примеров создания метафункций для своей системы трейтов.
На следующем примере трейта показал, как определять, есть ли у типа T реализация в точке использования трейта:
namespace detail{
template<typename T, std::size_t sizeof(T)>
std::true_type IsCompleteImpl(T*);
std::false_type IsCompleteImpl(...);
}
template<typename T>
using IsDeclComplete = decltype(detail::IsCompleteImpl(std::declval<T*>()));

Здесь фокус в том, что для типа T можно работать с его указателем и даже получать от него declval, если у типа нет реализации. Однако без реализации нельзя получить sizeof.
Также докладчик показал хорошие примеры использования std::index_sequence и fold expressions.

воскресенье, 30 декабря 2018 г.

Отчет о CPPCON 2018 (часть 2)

1. Fast Conversion From UTF-8 with C++, DFAs, and SSE Intrinsics


Полезно посмотреть, чтобы вспомнить, что из себя представляет кодировка UTF-8.

2. Spectre: Secrets, Side-Channels, Sandboxes, and Security


Интересно для общего развития.

3. A Little Order: Delving into the STL sorting algorithms


Короткий 20-минутный доклад. Можно посмотреть, чтобы освежить в памяти использование таких алгоритмов STL как std::sort, std::partial_sort, std::stable_sort, std::nth_element.

4. State Machines Battlefield - Naive vs STL vs Boost


Выступающий пытался реализовать простую стейт машину с использованием таких средств как std::variant, boost::statechart, boost::msm, coroutines (co_await, co_return), которые будут доступны в С++ 20. В рассказе были обозначены преимущества и недостатки того или иного подхода. В целом доклад получился познавательным, хотя конечно не стоит от него ждать просветления.

5. 105 STL Algorithms in Less Than an Hour


Джонатан Бокара уже не первый год ездит по миру, выступая в различных конференциях со своим докладом про STL алгоритмы. В этом году он уже выступал с этим докладам у нас на конференции (ссылка https://www.youtube.com/watch?v=tO_drboJfxs) CPP Russia, которая проходила в Питере. Кроме того Джонатан ведет довольно занятный блог (https://www.fluentcpp.com) с большим количеством интересных статей по С++, обобщенному программированию, и, разумеется, STL. Его доклады имеет смысл смотреть преимущественно для фана при наличии свободного времени, в основном с целью освежить в голове какие-то знания, которые и так в принципе все знали, но возможно забыли из-за того, что не применяли на практике в последнее время.

воскресенье, 25 ноября 2018 г.

Конференция dotnext 2018 - отчет

В этом году мне удалось побывать на конференции Dotnext 2018 для .Net разработчиков, которая проводилась в Москве 22-23 ноября. Мероприятие проходило на территории гостиницы Украина на Кутузовском проспекте и была организована по высшему разряду с питанием, кофебрейками, ништяками и прочим.



На конференцию были приглашены такие известные личности как Джефри Рихтер, Павел Йосифович и др.



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

Доклад Джефри Рихтера - Generics

Маэстро рассказывал про дженерики в .Net. Уровень материала - разжевывание дженериков студентам второго курса для подготовки к сдаче лабы.
Однако, одна прозвучавшая мысль меня порадовала. Рихтер рассказал, чем отличаются шаблоны в .Net framework от шаблонов в C++. Я и раньше мог на пальцах объяснить, в чем отличие, но не хватало общего понимания картины. Теперь же пазл сложился :) Основная и главная особенность дженериков в .Net состоит в том, что в .Net дженерики остаются дженериками после компиляции в байт код :) Всё, точка. Этого ответа достаточно на вопрос, в чем различия между дженериками в плюсах и в дотнете. Всё остальное просто следствия это факта.

Доклад Джефри Рихтера - Building responsive and scalable applications

Джефри, как и на первом своем докладе про дженерики, просто перечитывал на память главы из своей книги. Уровень сложности материала можно оценить как для начинающих.
При каждом переключении контекстов потоков Windows выполняет следующие операции:
1. Сохранение регистров текущего потока в оперативной памяти
2. Вычисление того, какой поток должен выполняться следующим.
3. Загрузка данных нового потока в регистры процессора.
4. Переключение контекстов может приводить к промахам при поиске по кешам процессора.
Всё это приводит к деградации производительности.
Вывод: используйте потоки, как можно реже. Для работы с диском, сетью и др. следует использовать асинхронное API.

Системные метрики: собираем подводные камни

Доклад про то, как программно получать и логировать какие-нибудь интересные метрики. Для этого есть следующие доступные варианты:
1. Получение performance counters через стандартные .Net методы из пространства имен System.Diagnostics. Надо иметь в виду, что категория Process весьма медленная.
2. Еще один способ сбора метрик - это использование функции RegQueryValueEx(HKEY_PERFORMANCE_DATA, "230 784", ...). Всегда лучше сравнивать разные способы сбора метрик.
3. Performance Data Helpers (pdh.dll). Имеет смысл использовать PDH, если уперлись в ограничения .Net обертки.
4. ETW - event tracing for Windows. Записывает евенты от различных провайдеров: .Net CLR provier, Kernel-Network provider, custom provider.
Также докладчик приводил интересные примеры из собственной практики, с какими проблемами ему приходилось сталкиваться при сборке метрик, и как он их решал.
Хороший блог Bruce Dawson, в котором много интересных статей на тему вопросов performance в Windows.
Рекомендую этот доклад к просмотру!

Создание окружения для интеграционных тестов на основе Docker-контейнеров

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

Windows 10 internals for .NET developers Pavel Yosifovich

1. Для лучшего понимания положения .Net в операционной системе Windows показал всю цепочку вызовов функций при обращении к методу File.Read(), начиная с библиотеки типов .Net и заканчивая HAL.
2. Рассказывал про jobs: как их создавать, как использовать с примерами.
3. Приоритеты потоков
4. Processor Affinity. Это про то, на каком процессоре должен выполняться тот или иной поток, и как этим можно управлять.
5. Waitable kernel objects: семафоры, мьютексы, евенты.
К сожалению, ничего нового не узнал.

Yield и async-await: как оно все устроено внутри и как этим воспользоваться

Также хочется отметить этот доклад. В этом докладе докладчик рассказал про ICFP Contest - соревнование для программистов. И показал на примере решения одной из задач с этого соревнования, каким образом можно применять конструкции yield return и async await. В ходе рассказал было подобно описано, как эти конструкции устроены внутри, как выглядит byte код для них, и как они работают.
Из этого доклада вряд ли можно будет получить много полезной информации с точки зрения практического применения. Однако, содержимое доклада и подача материала докладчиком показались мне весьма увлекательными. В итоге, посмотрел доклад с удовольствием.
Рекомендую этот доклад к просмотру!

От монолита к микросервисам: история и практика

1. С преимуществами микросервисной архитектуры всё понятно:
- Модульность. Все компоненты системы разделены на модули, которые можно разрабатывать, поддерживать и деплоить независимо от остальных.
- Проще разделить зону ответственности между разработчиками.
- Высокая отказоустойчивость.
- Разнообразие технологий.
2. Все микросервисы должны быть логически независимыми функциональными единицами, которые общаются через какую-то общую шину данных. Если у вас один микросервис обращается к другому, а то к третьему, то это антипаттерн.
3. Для управление контейнерами хорошо бы использовать какую-нибудь систему оркестрации, например, Kubrnetes.
4. Грамотно настроенная система мониторинга микросервисов может сильно облегчить жизнь.
5. При переходе от монолита к микросервисам не обязательно сразу начинать всё ломать и крушить. Можно постепенно по мере готовности микросервисов отрезать те или иные куски монолита, переключая систему на использование публичного API микросервисов.
6. Сложнее всего разделять базу данных.
Полезные ссылки:
Мартин Фаулер про микросервисы.
Хорошая статья про микросервисы на хабре.

Domain-driven design: рецепт для прагматика

Основные принципы DDD:
1. Общий язык, используемый в терминологии, между всеми участниками проекта: начиная с разработчиков, закачивая бизнесом.
2. Контекст доменной модели. Все бизнес сущности, используемые при проектировании системы, должны не выходить за рамки принятого контест доменной модели. Модели должны быть ограничены своими контекстами.
3. Больше общения между девеломентом и бизнесом.
4. Максимально выразительный код интерфейсов бизнес логики.
Хорошая статья на хабре про проектирование и архитектуру - Как мы попробовали DDD, CQRS и Event Sourcing и какие выводы сделали.

понедельник, 15 октября 2018 г.

Отчет о CPPCON 2018 (часть 1)

В сентябре завершилась одна из самых крупных конференций для C++ разработчиков cppcon 2018. На этот раз она проходила в Бельвью. В ней приняли учасие такие гуру своего дела как Андрей Александреску, Скот Мейерс, Бьёрн Страуструп, Герб Саттер и другие, чьи имена у всех на слуху, и книги которых многие конечно читали.
Из того, что показалось интересным мне, я бы выделил следующие выступления:

1. Louis Dionne "Compile-time programming and reflection in C++20 and beyond" 


Доклад получился познавательным и интересным. Louis Dionne периодически участвует в разного рода конференциях, выступая преимущественно с докладами по шаблонам, метапрограммированию в С++, Boost.Hana и другим связанным темам, делая при этом всегда упор на новые стандарты С++.
Автор много рассказывал про constexpr, про развитие возможностей, который предоставляет этот инструмент в разных редакциях стандарта. Всё идет к тому, чтобы постепенно увеличивать и наращивать доступные программисту средства языка, которые можно использовать в купе с constexpr. Так, например, в C++ 17 возможность использования if constexpr позволяет целиком заменить некоторые сложные блоки SFINAE. Таким образом в связи с перспективами дальнейшего развития constexpr, его следует активно использовать там, где это необходимо.
Также автор рассказывал про рефлексию типов, которая будет доступна в будущих стандартах языка. Концепция рефлексии в С++ очень напоминает рефлексию в .Net, которая доступна всем C# девелоперам с ранних версий .Net. К примеру, можно будет написать универсальный метод вывода в поток названия элемента любого енума.

2. Kate Gregory "Simplicity: Not Just For Beginners"


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

3. Bjarne Stroustrup "Concepts: The Future of Generic Programming (the future is here)"


Здесь Страуструп больше часа рассказывал про концепты, которые нас ожидают в С++ 20. На мой взгляд доклад можно было бы сделать хотя бы раза в полтора короче. Но в целом посмотреть однозначно стоит. Концепты уже точно появятся в С++ 20, и судя по тому, как видит развитие языка его создатель, у концептов большое будущее. Поэтому вполне логично инвестировать свое время в изучении данного средства языка уже сейчас с тем, чтобы применять его по возможности на практике в дальнейшем.
Из примера на вики концепты могут быть объявлены как-то так:
template <class T>
concept bool EqualityComparable() {
    return requires(T a, T b) {
        {a == b} -> Boolean; // Boolean is the concept defining a type usable in boolean context
        {a != b} -> Boolean;
    };
}
С одной стороны это просто дополнительное ограничение для шаблонного параметра. То же самое можно было бы получить с использованием std::enable_if и SFINAE. Но с другой использование концептов делает код гораздо более читаемым и лаконичным.
Кроме того, как обещает Страуструп, использование концептов значительно сократит время компиляции по сравнению с использованием стандартных средств обобщенного программирования.

Читайте продолжение отчета во второй части.