ydb-platform / ydb-cpp-sdk

YDB C++ SDK
Apache License 2.0
11 stars 11 forks source link

Issue #93 remove libc compat from contrib #214

Closed tsayukov closed 5 months ago

tsayukov commented 6 months ago

Issue #93

Задача в том, чтобы отказаться от использования libc_compat.

Части libc_compat, которые больше не используются в ydb-cpp-sdk и которые можно спокойно убрать:

Из остальных хедеров, не считая include/windows/sys/, в ydb-cpp-sdk подключается только contrib/libs/libc_compat/string.h. В нём нас интересуют только функции stricmp, strnicmp, strlcpy, strlcat и strlwr.

include/windows/sys/ содержит два хедера: queue.h и uio.h. Первый, sys/queue.h больше не включается в ydb-cpp-sdk. Второй sys/uio.h (в нём нас интересуют две функции: readv и writev) включается при сборке под Windows, таким образом, для остальных систем будет искаться sys/uio.h из glibc, а для Windows выберется хедер в include/windows/sys/, поскольку он добавляется в инклюды libc_compat таргета в cmake. Соответственно, нужно переписать readv и writev в src/library/coroutine/engine/network.cpp и src/util/network/socket.cpp так, чтобы они работали и для Windows, и для платформ с glibc.

Однако, прежде чем перейти к этой задаче, бросается в глаза следующее (и такое встречается не раз):

ssize_t DoReadVector(SOCKET fd, TContIOVector* vec) noexcept {
  return readv(fd, (const iovec*) vec->Parts(), Min(IOV_MAX, (int) vec->Count()));
}

Проблема в том, что каст (забудем пока, что это C-style) из указателя к указателю разных типов по стандарту может привести к UB, когда второй указатель попытаются разыменовать (см. [[basic.lval] par.11], они же Type aliasing). На практике компиляторы в редких случаях эксплуатируют этот UB и скорее всего ничего не взорвётся, но я всё равно бы хотел избегать подобных кастов. Обычно такие штуки решают с помощью std::memcpy, std::bit_cast, а ещё в будущем, когда доимплементируют 23 стандарт, можно будет на месте использовать std::start_lifetime_as без копирований.

Везде, где вызывался readv/writev/sendmsg, наблюдался следующий паттерн:

У нас уже есть класс-обёртка TContIOVector в util/network/iovec.h. Поэтому я решил добавить в него буфер, но в этом буфере сразу создавать нужные "C-шные" структуры типа iovec или WSABUF, исключая проблему небезопасных кастов. Подсказывает, как копировать (через memcpy или присваиванием), дополнительный трейт TSpanAdapterTraits в качестве шаблонного параметра у TContIOVector, который получился довольно многословным, но в нём я постарался учесть все случаи и сделать максимально обобщённым, а также добавил вспомогательные методы, чтобы можно было бы обращаться к полям "C-шной" структуры в общем случае, заранее не зная, что это за структура. Второй шаблонный параметр у TContIOVector -- это функтор, вызывающий соответствующую C-шную функцию. Причина, по которой я не захотел передавать функтор, например, в метод, -- не хотелось, чтобы один и тот же TContIOVector, изначально предназначенный только для чтения (TPartы, допустим, указывают на константные строки, лежащие в readonly памяти), использовали для записи. Возможно, есть решение получше. У TContIOVector главные методы, которые делают всю работу, -- это TryProcess и TryProcessAllBytes. Соответственно:

Возможно, стоит подумать о том, чтобы заменить TPart (изначально он определён в IOutputStream) на std::span<std::bytes> и std::span<const std::bytes>? TPart плох тем, что он держит в себе const void*, а используют его как для чтения, так и для записи. Моему TSpanAdapterTraits всё равно, с каким span-подобным объектом работать.

Подумал, сильно ли повлияет на производительность сразу выделение буфера (TTempBuf до 64 * 1024 байтов переиспользует массив из готового списка, соответственно он не будет каждый раз просить новую память в куче). С одной стороны, массив span-ов сам по себе не очень большой, копируется моментально. С другой стороны, на очень больших размерах каждый раз будет выделяться память в куче. С третьей стороны, операции ввода-вывода (даже векторные) наверняка перебьют всё остальное.

Вот мой бенчмарк: Собирался с установленным `libbenchmark-dev`. ```CMake find_package(benchmark REQUIRED) add_executable(iovec_bm iovec_bm.cpp) target_link_libraries(iovec_bm PRIVATE yutil cpp-testing-common benchmark::benchmark_main ) ```
Код: ```C++ #include "network/iovec.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace NUtils::NIOVector; using TIOVector = TContIOVector; struct RandomWords { static constexpr std::size_t size = 5'000; std::vector words; std::vector parts; public: RandomWords() { constexpr auto max_len = static_cast(std::numeric_limits::max()); std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution lens(0, max_len); std::uniform_int_distribution chars(static_cast('a'), static_cast('z')); const auto get_random_char = [&] { return static_cast(chars(gen)); }; const auto get_random_string_of = [&](std::size_t n) { std::string str; str.reserve(n); std::generate_n(std::back_inserter(str), n, get_random_char); return str; }; const auto get_random_string = [&] { const std::size_t length = lens(gen); return get_random_string_of(length); }; const auto str_to_tpart = [](std::string& str) noexcept { return TPart(str.data(), str.size()); }; words.reserve(size); words.push_back(get_random_string_of(max_len)); words.push_back(get_random_string_of(max_len)); std::generate_n(std::back_inserter(words), size - 2, get_random_string); parts.reserve(size); std::transform(words.begin(), words.end(), std::back_inserter(parts), str_to_tpart); parts[0].len = 0; parts[1].len = max_len; } }; inline auto random_words = RandomWords(); static void Cast(benchmark::State& state) { auto& parts = random_words.parts; const auto size = static_cast(state.range(0)); const auto path = SRC_("test_1"); auto file = std::fopen(path.data(), "w"); const int fd = fileno(file); for (auto _ : state) { parts[0].len += 1; parts[1].len -= 1; auto iov = reinterpret_cast(parts.data()); writev(fd, iov, size); } std::fclose(file); } BENCHMARK(Cast) ->Args({10}) ->Args({100}) ->Args({500}) ->Args({750}) ->Args({1000}) ->Args({2500}) ->Args({4000}) ->Args({5000}); static void PureMemcpy(benchmark::State& state) { auto& parts = random_words.parts; const auto size = static_cast(state.range(0)); TTempBuf heat(1); for (auto _ : state) { parts[0].len += 1; parts[1].len -= 1; TTempBuf buffer(size * sizeof(iovec)); auto iov = new (buffer.Data()) iovec[size]; std::memcpy(iov, parts.data(), size * sizeof(iovec)); benchmark::DoNotOptimize(buffer); } } BENCHMARK(PureMemcpy) ->Args({10}) ->Args({100}) ->Args({500}) ->Args({750}) ->Args({1000}) ->Args({2500}) ->Args({4000}) ->Args({5000}); static void Iovector(benchmark::State& state) { auto& parts = random_words.parts; const auto size = static_cast(state.range(0)); TTempBuf heat(1); const auto path = SRC_("test_2"); auto file = std::fopen(path.data(), "w"); const int fd = fileno(file); for (auto _ : state) { parts[0].len += 1; parts[1].len -= 1; TIOVector iov(parts.data(), size); iov.TryProcessAllBytes(fd); } std::fclose(file); } BENCHMARK(Iovector) ->Args({10}) ->Args({100}) ->Args({500}) ->Args({750}) ->Args({1000}) ->Args({2500}) ->Args({4000}) ->Args({5000}); static void Copy(benchmark::State& state) { auto& parts = random_words.parts; const auto size = static_cast(state.range(0)); TTempBuf heat(1); const auto path = SRC_("test_3"); auto file = std::fopen(path.data(), "w"); const int fd = fileno(file); for (auto _ : state) { parts[0].len += 1; parts[1].len -= 1; TTempBuf buffer(size * sizeof(iovec)); auto iov = new (buffer.Data()) iovec[size]; auto iter = parts.begin(); for (std::size_t i = 0; i < size; ++i, ++iter) { iov[i].iov_base = const_cast(iter->buf); iov[i].iov_len = iter->len; } writev(fd, iov, size); } std::fclose(file); } BENCHMARK(Copy) ->Args({10}) ->Args({100}) ->Args({500}) ->Args({750}) ->Args({1000}) ->Args({2500}) ->Args({4000}) ->Args({5000}); ```
Разные кейсы сравнивал так: ```bash /usr/share/benchmark/compare.py filters ./iovec_bm Cast Iovector ``` Если смотреть по цифрам, то на больших массивах, когда каждый раз в куче начинает выделяться память, видно ощутимое увеличение времени. Но с другой стороны, гугл бенчмарк закрашивает эти цифры красным и говорит, что ему не удалось доказать, что эти кейсы статистически различны. Наверное, разумнее считать, что до серьёзного абьюза кучи всё примерно одинаково со скидкой на ввод/вывод. А дальше непонятно.

Дополнительно util/memory/tempbuf.*:

tsayukov commented 6 months ago
tsayukov commented 6 months ago

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

Gazizonoki commented 5 months ago

Я починил сборку в CI, сделай rebase