Почему мой тест производительности дает неверные результаты? - PullRequest
4 голосов
/ 28 мая 2020

Существует опция clang-tidy performance-faster-string-find, которая обнаруживает использование метода std::basic_string::find (и связанных с ним) с односимвольным строковым литералом в качестве аргумента. По их словам, использование символьного литерала более эффективно.

Я хотел провести небольшой тест, чтобы проверить это. Поэтому я сделал эту небольшую программу:

#include <string>
#include <chrono>
#include <iostream>

int main() {
    int res = 0;
    std::string s(STRING_LITERAL);

    auto start = std::chrono::steady_clock::now();

    for(int i = 0; i < 10000000; i++) {
#ifdef CHAR_TEST
        res += s.find('A');
#else  
        res += s.find("A");
#endif
    }

    auto end = std::chrono::steady_clock::now();

    std::chrono::duration<double> elapsed_seconds = end-start;
    std::cout << "elapsed time: " << elapsed_seconds.count() << "s\n";

    return res;
}

В этой программе используются два макроса:

  • STRING_LITERAL, которые будут содержимым std::string, в котором мы вызовет функцию find. В моем тесте этот макрос может иметь два значения: небольшая строка, скажем "BAB" или длинная строка, скажем, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
  • CHAR_TEST, если определено, запустить тест для символьного литерала . Если нет, то find вызывается с односимвольным строковым литералом.

Вот результаты:

> (echo "char with small string" ; g++ -DSTRING_LITERAL=\"BAB\" -DCHAR_TEST -O3 -o toy_exe toy.cpp && ./toy_exe) ; (echo "string literal with small string" ; g++ -DSTRING_LITERAL=\"BAB\" -O3 -o toy_exe toy.cpp && ./toy_exe) ; (echo "char with long string" ; g++ -DSTRING_LITERAL=\"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\" -DCHAR_TEST -O3 -o toy_exe toy.cpp && ./toy_exe) ; (echo "string literal with long string" ; g++ -DSTRING_LITERAL=\"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\" -O3 -o toy_exe toy.cpp && ./toy_exe)

char with small string
elapsed time: 0.0551678s
string literal with small string
elapsed time: 0.0493302s

char with long string
elapsed time: 0.0599704s
string literal with long string
elapsed time: 0.188888s

Моя довольно уродливая команда запускает тест для четырех возможных комбинаций макросы и я обнаружили, что с длинным std::string действительно более эффективно использовать символьный литерал в качестве аргумента для find, но это уже не так для small std::string. Я повторил эксперимент и всегда обнаруживал увеличение примерно на 10% времени выполнения для символьного литерала с маленьким std::string.

Параллельно один из моих коллег сделал несколько тестов на quick-bench .com и обнаружил следующие результаты :

  • Маленький std::string с символьным литералом: 11 единиц времени
  • Маленький std::string с одиночным Символьный строковый литерал: 20 единиц времени
  • Длинный std::string с символьным литералом: 13 единиц времени
  • Длинный std::string с односимвольным строковым литералом: 22 единицы времени

Эти результаты согласуются с тем, что утверждает безвкусное (и звучит логично). Итак, что не так с моим тестом? Почему я получаю постоянные неверные результаты?


EDIT: Этот тест был выполнен с использованием G CC 6.3.0 в Debian. Я также запускаю его с помощью Clang 8.0.0 для аналогичных результатов.

Ответы [ 2 ]

3 голосов
/ 08 июня 2020

Мне не нравилась идея управления программой с помощью макросов, поэтому я переписал ее так:

#include <string>
#include <chrono>
#include <iostream>

template <typename T>
int test(std::string s, T pattern, const std::string & msg, size_t num_repeat)
{
  int res = 0;
  auto start = std::chrono::steady_clock::now();

  for(int i = 0; i < num_repeat; i++) 
  {
    s.find(pattern);
    s[0] = '.';
  }

  auto end = std::chrono::steady_clock::now();
  std::chrono::duration<double> elapsed_seconds = end-start;
  std::cout << msg << " elapsed time: " << elapsed_seconds.count() << "s\n";

  return res;

}


int main(int argc, const char* argv[]) 
{
  const int N = 10'000'000;
  int res = 0;
  std::string s = (argc == 1) ? "MNBVCXZLKJHGFDSAPOIUYTREWQ" : argv[1];

  res += test(s, 'A', s + ".find(char): ", N);
  res += test(s, "A", s + ".find(string): ", N);

  return res & 1;
}

Основная идея заключалась в том, чтобы обмануть компилятор настолько, чтобы он отказался от любой идеи оптимизации. out (это цель s[1] = '.' и чтения s из командной строки). Я хотел избежать ситуации, когда компилятор знает и искомую строку, и шаблон, поскольку это может позволить ему использовать некоторые приемы оптимизации, которые мы не хотим использовать в учетной записи int.

Я скомпилировал его, используя g cc 10.1.0 и clang 10.0.0, с -O3 в качестве единственной опции командной строки. (g ++ был запущен с -std=c++17, у меня это псевдоним).

Результаты зависят от компилятора (что уже можно увидеть в тесте, указанном в вопросе!)

Хорошо. маленькие строки, g ++:

pA1.find(char):  elapsed time: 0.124409s
pA1.find(string):  elapsed time: 0.125372s

clang:

pA1.find(char):  elapsed time: 0.122489s
pA1.find(string):  elapsed time: 0.126854s

Разницу трудно измерить. clang систематически дает большее время для строк, но обычно это 3-й значащий di git, вряд ли стоит упоминать.

Теперь строки среднего размера, g ++:

00000000000000000000000000000000000000000000000pA1.find(char):  elapsed time: 0.139219s
00000000000000000000000000000000000000000000000pA1.find(string):  elapsed time: 0.137838s

clang:

00000000000000000000000000000000000000000000000pA1.find(char):  elapsed time: 0.13962s
00000000000000000000000000000000000000000000000pA1.find(string):  elapsed time: 0.153506s

Результат для clang систематически в пользу метода «char»; Что касается g ++, победитель колеблется.

Теперь даже большие строки, g ++:

111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000pA1.find(char):  elapsed time: 0.170651s
111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000pA1.find(string):  elapsed time: 0.177381s

clang:

111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000pA1.find(char):  elapsed time: 0.172215s
111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000pA1.find(string):  elapsed time: 0.206911s

Для g ++ разницу вряд ли можно заметить, она находится в ожидаемом диапазоне случайных колебаний. Для clang разница ясна и систематична c.

Я повторил это со строкой, состоящей примерно из 1000 символов. Для g ++ разницы нет, для clang около 10%.

Итак, мой вывод - все зависит от компилятора. Для clang разумно следовать совету clang-tidy. Для g ++ этого не должно быть.

Этот ответ не является полным, поскольку было бы интересно узнать различия в реализации std::string::find между clang и g ++.

0 голосов
/ 03 июня 2020

Я не уверен, что что-то не так с вашей реперной оценкой. Я запускаю тот же самый код на платформе repl.io и получаю результаты, соответствующие «быстрой скамейке»:

char с small string. Прошедшее время: 0,402103 с

строка литерал с маленький прошедшее время строки: 0,489828 с

char с длинный прошедшее время строки: 0,400224 с

строка литерал с long прошедшее время строки: 0,53304 с

На ум приходит одна вещь, ваше профилирование выполняется через l oop, я бы профилировал только то, что в l oop.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...