Created
June 19, 2024 13:23
-
-
Save eao197/4e142bb134e2542239558c3be10a96ac to your computer and use it in GitHub Desktop.
Простая программа для сравнения нескольких реализаций utf8_checker/decoder.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <algorithm> | |
#include <array> | |
#include <chrono> | |
#include <iostream> | |
#include <span> | |
#include <string> | |
#include <string_view> | |
#include <cstdint> | |
namespace restinio | |
{ | |
namespace utils | |
{ | |
// | |
// utf8_checker_t | |
// | |
/*! | |
* @brief Helper class for checking UTF-8 byte sequence during parsing | |
* URI or incoming byte stream. | |
*/ | |
class utf8_checker_t | |
{ | |
//! Enumeration of all possible checker states. | |
enum class state_t | |
{ | |
wait_first_byte, | |
wait_second_of_two, | |
wait_second_of_three, | |
wait_second_of_four, | |
wait_third_of_three, | |
wait_third_of_four, | |
wait_fourth_of_four, | |
invalid, | |
}; | |
//! The current UNICODE symbol. | |
/*! | |
* Contains a valid value only if some bytes were successfully | |
* processed by process_byte() and the current state is | |
* wait_first_byte. | |
*/ | |
std::uint32_t m_current_symbol = 0u; | |
//! The current state of the checker. | |
state_t m_state{ state_t::wait_first_byte }; | |
void | |
on_first_byte( std::uint8_t byte ) noexcept | |
{ | |
if( byte <= 0x7Fu ) | |
{ | |
m_state = state_t::wait_first_byte; | |
m_current_symbol = byte; | |
} | |
else if( 0xC0u == (byte & 0xE0u) ) | |
{ | |
m_state = state_t::wait_second_of_two; | |
m_current_symbol = (byte & 0x1Fu); | |
} | |
else if( 0xE0u == (byte & 0xF0u) ) | |
{ | |
m_state = state_t::wait_second_of_three; | |
m_current_symbol = (byte & 0x0Fu); | |
} | |
else if( 0xF0u == (byte & 0xF8u) ) | |
{ | |
m_state = state_t::wait_second_of_four; | |
m_current_symbol = (byte & 0x07u); | |
} | |
else | |
{ | |
// Because UTF-8 can represent only ranges from: | |
// | |
// 0000 0000-0000 007F | |
// 0000 0080-0000 07FF | |
// 0000 0800-0000 FFFF | |
// 0001 0000-0010 FFFF | |
// | |
// There is no need to check masks like 0b111110xx and so on. | |
// | |
// See https://datatracker.ietf.org/doc/html/rfc3629 | |
// | |
m_state = state_t::invalid; | |
} | |
} | |
void | |
on_second_of_two( std::uint8_t byte ) noexcept | |
{ | |
if( 0x80u == (byte & 0xC0u) ) | |
{ | |
m_current_symbol <<= 6; | |
m_current_symbol |= (byte & 0x3Fu); | |
// Check for overlong sequence. | |
// The valid range for two bytes representation is 0x0080..0x07FF. | |
if( m_current_symbol < 0x0080u ) | |
{ | |
// The value is too small, it's overlong. | |
m_state = state_t::invalid; | |
} | |
else | |
// Three is no need to check the result value against | |
// invalid ranges (0xD800..0xDFFF and 0x110000..) | |
// because two bytes only represents 0x0080..0x07FF. | |
m_state = state_t::wait_first_byte; | |
} | |
else | |
{ | |
m_state = state_t::invalid; | |
} | |
} | |
void | |
on_second_of_three( std::uint8_t byte ) noexcept | |
{ | |
if( 0x80u == (byte & 0xC0u) ) | |
{ | |
m_current_symbol <<= 6; | |
m_current_symbol |= (byte & 0x3Fu); | |
m_state = state_t::wait_third_of_three; | |
} | |
else | |
{ | |
m_state = state_t::invalid; | |
} | |
} | |
void | |
on_second_of_four( std::uint8_t byte ) noexcept | |
{ | |
if( 0x80u == (byte & 0xC0u) ) | |
{ | |
m_current_symbol <<= 6; | |
m_current_symbol |= (byte & 0x3Fu); | |
m_state = state_t::wait_third_of_four; | |
} | |
else | |
{ | |
m_state = state_t::invalid; | |
} | |
} | |
void | |
on_third_of_three( std::uint8_t byte ) noexcept | |
{ | |
if( 0x80u == (byte & 0xC0u) ) | |
{ | |
m_current_symbol <<= 6; | |
m_current_symbol |= (byte & 0x3Fu); | |
// Check for overlong sequence. | |
// The valid range for three bytes representation is 0x0800..0xFFFF. | |
if( m_current_symbol < 0x0800u ) | |
{ | |
// The value is too small, it's overlong. | |
m_state = state_t::invalid; | |
} | |
else | |
{ | |
// It's necessary to check illigal points 0xD800..0xDFFF. | |
if( m_current_symbol >= 0xD800 && m_current_symbol <= 0xDFFF ) | |
m_state = state_t::invalid; | |
else | |
m_state = state_t::wait_first_byte; | |
} | |
} | |
else | |
{ | |
m_state = state_t::invalid; | |
} | |
} | |
void | |
on_third_of_four( std::uint8_t byte ) noexcept | |
{ | |
if( 0x80u == (byte & 0xC0u) ) | |
{ | |
m_current_symbol <<= 6; | |
m_current_symbol |= (byte & 0x3Fu); | |
m_state = state_t::wait_fourth_of_four; | |
} | |
else | |
{ | |
m_state = state_t::invalid; | |
} | |
} | |
void | |
on_fourth_of_four( std::uint8_t byte ) noexcept | |
{ | |
if( 0x80u == (byte & 0xC0u) ) | |
{ | |
m_current_symbol <<= 6; | |
m_current_symbol |= (byte & 0x3Fu); | |
// Check for overlong sequence. | |
// The valid range for three bytes representation is 0x10000..0x10FFFF. | |
if( m_current_symbol < 0x10000u ) | |
{ | |
// The value is too small, it's overlong. | |
m_state = state_t::invalid; | |
} | |
else | |
{ | |
// It's necessary to check for values above 0x10FFFF. | |
// There is no need to check 0xD800..0xDFFF range because | |
// it was already handled by overlong check. | |
if( m_current_symbol >= 0x110000 ) | |
m_state = state_t::invalid; | |
else | |
m_state = state_t::wait_first_byte; | |
} | |
} | |
else | |
{ | |
m_state = state_t::invalid; | |
} | |
} | |
public: | |
utf8_checker_t() = default; | |
/*! | |
* Checks another byte. | |
* | |
* @note | |
* The actual value of the current symbol can be obtained only if | |
* process_byte() returns `true` and the subsequent call to | |
* finalized() returns `true`: | |
* | |
* @code | |
* utf8checker_t checker; | |
* for( const auto ch : some_string ) | |
* { | |
* if( checker.process_byte() ) | |
* { | |
* if( checker.finalized() ) | |
* process_unicode_symbol( checker.current_symbol() ); | |
* } | |
* else | |
* { | |
* ... // Invalid sequence found! | |
* break; | |
* } | |
* } | |
* @endcode | |
* | |
* @retval true if the sequence is still valid and the next byte | |
* can be given to the next call to process_byte(). | |
* | |
* @retval false if the sequence is invalid an there is no sense | |
* to continue call process_byte(). | |
*/ | |
[[nodiscard]] | |
bool | |
process_byte( std::uint8_t byte ) noexcept | |
{ | |
switch( m_state ) | |
{ | |
case state_t::wait_first_byte: | |
on_first_byte( byte ); | |
break; | |
case state_t::wait_second_of_two: | |
on_second_of_two( byte ); | |
break; | |
case state_t::wait_second_of_three: | |
on_second_of_three( byte ); | |
break; | |
case state_t::wait_second_of_four: | |
on_second_of_four( byte ); | |
break; | |
case state_t::wait_third_of_three: | |
on_third_of_three( byte ); | |
break; | |
case state_t::wait_third_of_four: | |
on_third_of_four( byte ); | |
break; | |
case state_t::wait_fourth_of_four: | |
on_fourth_of_four( byte ); | |
break; | |
case state_t::invalid: | |
// Nothing to do. | |
break; | |
} | |
return (state_t::invalid != m_state); | |
} | |
/*! | |
* @return true if the current sequence finalized. | |
*/ | |
[[nodiscard]] | |
bool | |
finalized() const noexcept | |
{ | |
return state_t::wait_first_byte == m_state; | |
} | |
/*! | |
* Return the object into the initial state. | |
*/ | |
void | |
reset() noexcept | |
{ | |
m_current_symbol = 0u; | |
m_state = state_t::wait_first_byte; | |
} | |
/*! | |
* Get the collected value of the current symbol. | |
* | |
* @note | |
* It returns the actual value only if: | |
* | |
* - some bytes were successfully feed into process_byte(); | |
* - finalized() returns `true`. | |
*/ | |
[[nodiscard]] | |
std::uint32_t | |
current_symbol() const noexcept { return m_current_symbol; } | |
}; | |
} /* namespace utils */ | |
} /* namespace restinio */ | |
namespace decode_2009 | |
{ | |
constexpr std::uint32_t utf8_accept = 0; | |
constexpr std::uint32_t utf8_reject = 1; | |
static const uint8_t utf8d[] = { | |
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 00..1f | |
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 20..3f | |
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 40..5f | |
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 60..7f | |
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, // 80..9f | |
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, // a0..bf | |
8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // c0..df | |
0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, // e0..ef | |
0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, // f0..ff | |
0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, // s0..s0 | |
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, // s1..s2 | |
1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, // s3..s4 | |
1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, // s5..s6 | |
1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // s7..s8 | |
}; | |
std::uint32_t inline | |
decode(std::uint32_t* state, std::uint32_t* codep, std::uint32_t byte) { | |
std::uint32_t type = utf8d[byte]; | |
*codep = (*state != utf8_accept) | |
? (byte & 0x3fu) | (*codep << 6) | |
: (0xff >> type) & (byte); | |
*state = utf8d[256 + *state*16 + type]; | |
return *state; | |
} | |
} /* namespace decode_2009 */ | |
namespace decode_2010 | |
{ | |
constexpr std::uint32_t utf8_accept = 0; | |
constexpr std::uint32_t utf8_reject = 12; | |
static const uint8_t utf8d[] = { | |
// The first part of the table maps bytes to character classes that | |
// to reduce the size of the transition table and create bitmasks. | |
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, | |
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, | |
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, | |
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, | |
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, | |
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, | |
8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, | |
10,3,3,3,3,3,3,3,3,3,3,3,3,4,3,3, 11,6,6,6,5,8,8,8,8,8,8,8,8,8,8,8, | |
// The second part is a transition table that maps a combination | |
// of a state of the automaton and a character class to a state. | |
0,12,24,36,60,96,84,12,12,12,48,72, 12,12,12,12,12,12,12,12,12,12,12,12, | |
12, 0,12,12,12,12,12, 0,12, 0,12,12, 12,24,12,12,12,12,12,24,12,24,12,12, | |
12,12,12,12,12,12,12,24,12,12,12,12, 12,24,12,12,12,12,12,12,12,24,12,12, | |
12,12,12,12,12,12,12,36,12,36,12,12, 12,36,12,12,12,12,12,36,12,36,12,12, | |
12,36,12,12,12,12,12,12,12,12,12,12, | |
}; | |
std::uint32_t inline | |
decode(std::uint32_t* state, std::uint32_t* codep, std::uint32_t byte) { | |
std::uint32_t type = utf8d[byte]; | |
*codep = (*state != utf8_accept) ? | |
(byte & 0x3fu) | (*codep << 6) : | |
(0xff >> type) & (byte); | |
*state = utf8d[256 + *state + type]; | |
return *state; | |
} | |
} /* namespace decode_2010 */ | |
bool | |
check_validity_with_restinio(std::string_view str, std::uint32_t & out) | |
{ | |
restinio::utils::utf8_checker_t checker; | |
for( const auto ch : str ) | |
{ | |
if( checker.process_byte( ch ) ) | |
{ | |
if( checker.finalized() ) | |
out += checker.current_symbol(); | |
} | |
else | |
return false; | |
} | |
return true; | |
} | |
bool | |
check_validity_with_decode_2009(std::string_view str, std::uint32_t & out) | |
{ | |
std::uint32_t state = decode_2009::utf8_accept; | |
std::uint32_t code_point; | |
for( const auto ch : str ) | |
{ | |
switch( decode_2009::decode( &state, &code_point, | |
static_cast<std::uint32_t>( static_cast<unsigned char>(ch) ) ) ) | |
{ | |
case decode_2009::utf8_accept: | |
out += code_point; | |
break; | |
case decode_2009::utf8_reject: | |
return false; | |
} | |
} | |
return true; | |
} | |
bool | |
check_validity_with_decode_2010(std::string_view str, std::uint32_t & out) | |
{ | |
std::uint32_t state = decode_2010::utf8_accept; | |
std::uint32_t code_point; | |
for( const auto ch : str ) | |
{ | |
switch( decode_2010::decode( &state, &code_point, | |
static_cast<std::uint32_t>( static_cast<unsigned char>(ch) ) ) ) | |
{ | |
case decode_2010::utf8_accept: | |
out += code_point; | |
break; | |
case decode_2010::utf8_reject: | |
return false; | |
} | |
} | |
return true; | |
} | |
template< typename Checker > | |
std::pair< bool, std::uint32_t > | |
checking_loop( | |
Checker && checker, | |
unsigned int loops, | |
std::string_view str ) | |
{ | |
std::uint32_t out = 0; | |
bool result = false; | |
for( unsigned int i = 0; i < loops; ++i ) | |
{ | |
out = 0; | |
result = checker( str, out ); | |
} | |
return { result, out }; | |
} | |
class duration_meter | |
{ | |
const char * _name; | |
const std::chrono::high_resolution_clock::time_point _started_at; | |
public: | |
duration_meter( const char * name ) | |
: _name{ name } | |
, _started_at{ std::chrono::high_resolution_clock::now() } | |
{} | |
~duration_meter() | |
{ | |
const auto f = std::chrono::high_resolution_clock::now(); | |
std::cout << "*** " << _name << ": " | |
<< std::chrono::duration_cast<std::chrono::microseconds>( | |
f - _started_at ).count() | |
<< "us *** " << std::endl; | |
} | |
}; | |
template<typename Lambda> | |
decltype(auto) | |
measure( const char * name, Lambda && lambda ) | |
{ | |
duration_meter meter{ name }; | |
return lambda(); | |
} | |
int main() | |
{ | |
std::string_view text{ | |
"В последний раз статья, целиком посвященная открытому проекту RESTinio, вышла " | |
"на Хабре в декабре 2020-го года, без малого три года назад. Это был рассказ " | |
"о релизе версии 0.6.13. По сути, это был последний релиз, в котором в " | |
"RESTinio появилось что-то новое и важное. Потом были только небольшие " | |
"корректирующие релизы, исправляющие ошибки или адаптирующие RESTinio к " | |
"свежим версиям зависимостей. " | |
" " | |
"И вот спустя три года нам удалось выпустить новое существенное обновление. А " | |
"посему есть повод поговорить о том, что было удалено/добавлено/изменено в этой " | |
"версии. Ну и о причинах паузы в развитии и перспективах проекта вообще. " | |
" " | |
"Кому интересно, милости прошу под кат. " | |
" " | |
"Для тех же, кто про данную разработку слышит в первый раз: это наша попытка " | |
"сделать встраиваемый в C++ приложения HTTP(S)/WebSocket сервер, который бы " | |
"обладал и большой гибкостью, и нормальной производительностью, освобождал бы " | |
"пользователя от рутины, но не прятал бы абсолютно все детали 'под капот', и " | |
"удовлетворял бы нашим представлениям о том, как подобные вещи должны " | |
"выглядеть... " | |
" " | |
"Вроде бы получилось. Мне кажется, что раз уж RESTinio сумел набрать тысячу " | |
"звезд на GitHub, результат понравился и пригодился не только нам. Впрочем, это " | |
"уже совсем другая история. Давайте вернемся к рассказу об изменениях в версии " | |
"0.7.0 и к тому, почему этих изменений пришлось ждать так долго... " | |
" " | |
"Что нового в 0.7.0 " | |
"Переход на C++17 " | |
"В версии 0.7.0 мы перешли с C++14 на C++17. Вероятно, это не самое лучшее из " | |
"наших решений, ведь кто-то все еще вынужден оставаться на C++14 не имея " | |
"возможности обновиться до C++17, однако мы для себя больше не видели смысла " | |
"держаться за C++14. " | |
" " | |
"Выгода от перехода на C++17 заключалась прежде всего в том, что удалось " | |
"избавиться от таких зависимостей, как optional-lite, string_view-lite и " | |
"variant-lite, т.к. теперь это все доступно в стандартной библиотеке. Так что " | |
"остается сказать большое спасибо Martin Moene за его труд по написанию и " | |
"сопровождению этих библиотек, они нам здорово помогали в течении шести лет, но " | |
"дальше мы пойдем с stdlib 🙂 " | |
" " | |
"Хотя осталась зависимость от expected-lite, но с ней придется жить еще долго. " | |
"Если уж мы на 17-ые плюсы перебрались только в 2023-ем, то перехода на C++23 " | |
"нужно будет подождать еще лет пять-шесть, а то и девять-десять 😆 " | |
" " | |
"Выгода от 17-го стандарта проявилась еще и в том, что в ряде мест мы смогли " | |
"выбросить сложные (и не очень) шаблонные конструкции в пользу простых if " | |
"constexpr и fold expressions. " | |
" " | |
"Так что дальше пойдем уже в рамках C++17. Если кого-то это расстраивает, то уж " | |
"простите за откровенность, но за поддержку C++14 нам никто не платит. " | |
" " | |
"Переход на llhttp, Catch2 v3 и modern CMake " | |
"Изначально RESTinio использовал nodejs/http-parser в качестве парсера " | |
"HTTP-запросов. Но несколько лет назад его развитие и поддержка прекратились. " | |
"Посему в версии 0.7.0 мы переехали на nodejs/llhttp. Собственно, этот переезд и " | |
"был главной мотивацией для выпуска версии 0.7.0. " | |
" " | |
"Заодно мы обновили у себя Catch2. Эта библиотека начиная с версии 3.0 уже не " | |
"является header-only и требует компиляции. " | |
}; | |
auto v1 = measure( " restinio", [&]() { | |
return checking_loop( | |
check_validity_with_restinio, 100'000u, text ); | |
} ); | |
auto v2 = measure( "decode_2009", [&]() { | |
return checking_loop( | |
check_validity_with_decode_2009, 100'000u, text ); | |
} ); | |
auto v3 = measure( "decode_2010", [&]() { | |
return checking_loop( | |
check_validity_with_decode_2010, 100'000u, text ); | |
} ); | |
std::cout << v1.first << " " << v1.second << std::endl; | |
std::cout << v2.first << " " << v2.second << std::endl; | |
std::cout << v3.first << " " << v3.second << std::endl; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment