【问题标题】:Why my performance benchmark gives me wrong results?为什么我的性能基准给出了错误的结果?
【发布时间】:2020-09-15 10:13:48
【问题描述】:

有一个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 的参数确实更有效,但它不再小std::string 是这样。我重复了这个实验,我总是发现小std::string 的字符文字的执行时间增加了大约 10%。

同时,我的一位同事对quick-bench.com 进行了一些基准测试,发现以下results

  • 带有字符文字的小std::string:11 个时间单位
  • std::string,带单个字符串字面量:20 个时间单位
  • std::string 字符字面量:13 个时间单位
  • std::string 单字符串字面量:22 个时间单位

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


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

【问题讨论】:

  • 你玩过编译器的优化级别吗?
  • @mlc 在我所有的测试中,我使用了-O3 优化级别。在 quick-bench.com 上进行的基准测试也是如此。
  • 我假设您也在相同的编译器版本和相同的 c++ 标准中运行它?
  • @mlc 是的,您可以在我的问题中看到命令:g++ -DSTRING_LITERAL=\"BAB\" -DCHAR_TEST -O3 -o toy_exe toy.cpp。不过,我也用 clang 和 -std=c++11 做了一些测试,并观察到类似的结果。
  • 查看 quick-bench 上的代码,我看到他们使用benchmark::DoNotOptimize()。似乎编译器为您和 quick-bench 上的代码做了不同的优化。请参阅:github.com/google/benchmark#preventing-optimization

标签: c++ stdstring microbenchmark


【解决方案1】:

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

#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++,不必如此。

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

【讨论】:

    【解决方案2】:

    我不确定您的基准标记有什么问题。我在 repl.io 平台上运行完全相同的代码,得到的结果与“quick bench”匹配:

    charsmall 字符串 .经过时间:0.402103s

    string 文字与 small 字符串经过时间:0.489828s

    charlong 字符串经过时间:0.400224s

    string 文字与 long 字符串经过时间:0.53304s

    想到一件事,您的分析是在循环中完成的,我将只分析循环中的内容。

    【讨论】:

    • 你用的是什么编译器?什么版本?我添加了我在问题中使用的版本。另外,您是否尝试在物理机上运行基准测试?
    • g++ (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0 - 我没有尝试在物理机上运行它
    • 不幸的是,我没有这个版本的 g++ 来检查我是否有类似的结果。我想知道结果是否取决于编译器及其版本。
    猜你喜欢
    • 1970-01-01
    • 2018-07-04
    • 1970-01-01
    • 1970-01-01
    • 2013-03-09
    • 1970-01-01
    • 2011-01-07
    • 1970-01-01
    • 2012-12-06
    相关资源
    最近更新 更多