В догонку к предыдущему посту про скорость самодельного UTF-8 валидатора. Захотелось добавить в это же сравнение и is_utf8 на базе SIMD.
Задействовать is_utf8 несложно, пишется очередная check_validity_with_*:
bool check_validity_with_is_utf8_simd(std::string_view str) { return is_utf8( str.data(), str.size() ); } |
И все.
Но не совсем, т.к. эта новая check_validity только возвращает true/false, тогда как старые реализации еще и суммировали извлеченные code-point-ы:
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_restinio(std::string_view str) { restinio::utils::utf8_checker_t checker; for( const auto ch : str ) { if( !checker.process_byte( static_cast<unsigned char>(ch) ) ) { return false; } } return true; } |
OK, сделано. Можно запускать бенчмарк и...
И внезапно я вижу какие-то нереальные цифры:
*** restinio: 8us ***
*** decode_2009: 12us ***
*** decode_2010: 11us ***
*** simd_is_utf8: 49176us ***
Это GCC-13.2 под Windows на i7-8550u, ключи компиляции:
g++ -O2 -std=c++20 -o utf8_checker_speed-gcc13.exe *.cpp
В общем, явно что-то не то, ну не может работа выполняться за 8us там, где раньше было около секунды. Но что именно не то?
Оказалось, что компилятор GCC-13 настолько продвинут, что смог обнаружить отсутствие побочных эффектов в вызове обновленных версий check_validity_with_restinio, check_validity_with_decode_2009, check_validity_with_decode_2010. И ведь действительно, побочных эффектов там нет: на вход подаются одни и те же данные, на основе одних и тех же данных выполняются одни и те же вычисления. Соответственно, и результат вычислений всегда будет один и тот же.
Ну а раз результат всегда один и тот же, то нет смысла вызывать функции check_validity_with_restinio 100k раз, достаточно всего одного раза. Что GCC и сделал.
Отсюда и такие маленькие цифры -- это замер для всего одного вызова check_validity_with_*. А не для 100K вызовов, как задумывалось.
Тогда как в случае с is_utf8 компилятор не смог сделать выводов об отсутствии побочных эффектов (полагаю потому, что сама is_utf8 находилась в другой единице трансляции) и вызов check_validity_with_is_utf8_simd честно выполнялся 100K раз.
Чтобы понять что происходит пришлось заглянуть в ассемблерный выхлоп компилятора, чего я уже не делал лет сто, если не сто пятьдесят ;)
Еще прикольнее то, что GCC-11 не разобрался с отсутствием побочных эффектов у check_validity_with_restinio и честно вызывал ее 100k раз, тогда как вызовы check_validity_with_decode_2009 и check_validity_with_decode_2010 успешно "заоптимизировал". А вот GCC-13 смог справиться даже с utf8_checker_t из RESTinio. Что для меня выглядит ну совсем уже фантастикой.
Нужно сказать, что только GCC смог в такую крутую "оптимизацию". Ни с VC++, ни с clang ничего подобного не было.
PS. Если кому-то интересно, то вот результаты GCC-13.2 когда заставляешь его делать все 100k вызовов:
*** restinio: 535279us ***
*** decode_2009: 1197288us ***
*** decode_2010: 1137498us ***
*** simd_is_utf8: 53430us ***