c++ - 为什么我的性能基准给出了错误的结果?
问题描述
有一个 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
我的非常丑陋的命令运行了四种可能的宏组合的基准测试,我发现,使用 longstd::string
时,使用字符文字作为参数确实更有效,find
但对于 small ,它不再适用std::string
。我重复了这个实验,我总是发现使用 small 的字符文字的执行时间增加了大约 10% std::string
。
与此同时,我的一位同事在quick-bench.com上做了一些基准测试,结果如下:
- 小字
std::string
面量:11 个时间单位 std::string
带单字符串字面量的小:20 个时间单位- Long
std::string
with character literal: 13 个时间单位 - Long
std::string
单字符串字面量:22 个时间单位
这些结果与声称的整洁(并且听起来合乎逻辑)是一致的。那么,我的基准测试有什么问题?为什么我有一致的错误结果?
编辑:这个基准测试是在 Debian 上使用 GCC 6.3.0 执行的。我也使用 Clang 8.0.0 运行它以获得类似的结果。
解决方案
我不喜欢通过宏控制程序的想法,所以我将其重写为:
#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::find
clang 和 g++ 之间的实现差异会很有趣。
推荐阅读
- c# - 如何使用网络用户的凭据登录 android?
- python - POST 和 GET 中的 Django Formset 初始化
- java - 从一个 JSP 获取数据到另一个 JSP 使用 foreach 循环显示 JSP 数据的地方
- opengl - OpenGL中非状态改变纹理重新绑定的惩罚
- wordpress - Wordpress 用户权限,允许非管理员用户访问插件内容
- mongodb - 来自MongoDB大学课程的MongoDB聚合查询
- angular - Angular 9 中的类和接口
- php - 在同一页面上实时使用 AJAX 将来自 html 文本输入的值存储在 PHP 变量中
- angular - 如何在 Angular 区域外调用 HttpClient?
- java - Android:单击视图下拉列表时,文本视图