首页 > 解决方案 > 为什么我的性能基准给出了错误的结果?

问题描述

有一个 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;
}

该程序中使用了两个宏:

结果如下:

> (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

我的非常丑陋的命令运行了四种可能的宏组合的基准测试,我发现,使用 longstd::string时,使用字符文字作为参数确实更有效,find但对于 small ,它不再适用std::string。我重复了这个实验,我总是发现使用 small 的字符文字的执行时间增加了大约 10% std::string

与此同时,我的一位同事在quick-bench.com上做了一些基准测试,结果如下:

这些结果与声称的整洁(并且听起来合乎逻辑)是一致的。那么,我的基准测试有什么问题?为什么我有一致的错误结果?


编辑:这个基准测试是在 Debian 上使用 GCC 6.3.0 执行的。我也使用 Clang 8.0.0 运行它以获得类似的结果。

标签: c++stdstringmicrobenchmark

解决方案


我不喜欢通过宏控制程序的想法,所以我将其重写为:

#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;
}

主要思想是愚弄编译器,使其放弃任何优化事物的想法(这是从命令行s[1] = '.'读取的目的s)。我想避免编译器知道搜索的字符串和模式的情况,因为这可能让它使用一些我们不想考虑 int 的优化技巧。

我使用 gcc 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

铛:

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

差异几乎无法衡量。clang 系统地为字符串产生更大的时间,但这通常在第 3 位有效数字上,几乎不值得一提。

现在中等大小的字符串,g++:

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

铛:

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

铛:

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

对于 g++,几乎无法观察到差异,它在随机波动的预期范围内。对于铿锵而言,区别是清晰而系统的。

我用一个由大约 1000 个字符组成的字符串重复了它。对于 g++ 没有区别,对于 clang 来说大约是 10%。

所以,我的结论是这一切都取决于编译器。对于 clang 来说,遵循 clang-tidy 发布的建议是合理的。对于 g++,不必如此。

但是,这个答案并不完整,因为了解std::string::findclang 和 g++ 之间的实现差异会很有趣。


推荐阅读