понедельник, 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.