пятница, 28 июня 2024 г.

[prog.c++] Пример того, что могут творить оптимизирующие C++ компиляторы

В догонку к предыдущему посту про скорость самодельного 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;
   forconst 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;
   forconst 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 ***

2 комментария:

DimaGogolev комментирует...

Почему не используете -O3 с gcc?

eao197 комментирует...

> Почему не используете -O3 с gcc?

Привычка, наверное. Не знаю как сейчас, а раньше -O3 описывалась как "на свой страх и риск".