关于BenchMark/c++11计时器/Chrome:tracing 的一些笔记


A benchmark is a test of the performance of a computer system.

​ 基准测试是对计算机系统的性能的测试

计时器

性能的指标就是时间,在c++11后计时十分方便,因为有<chrono>神器

在性能测试中,一般依赖堆栈上的生命周期来进行计时

计时器的实现全貌

class InstrumentationTimer { private:     chrono::time_point<chrono::steady_clock> start;     const char *m_hint;  public:     explicit InstrumentationTimer(const char *hint) : m_hint(hint) {         start = chrono::steady_clock::now();     }       ~InstrumentationTimer() {         auto end = chrono::steady_clock::now();         cout << m_hint << ':' << static_cast<double>((end - start).count()) / 1e6 << "msn";         long long llst = chrono::time_point_cast<chrono::microseconds>(start).time_since_epoch().count();         long long lled = chrono::time_point_cast<chrono::microseconds>(end).time_since_epoch().count();          //Instrumentor::Get().WriteProfile({m_hint, llst, lled});     } }; 

非常简单的原理 就是应用作用域自动调用析构函数来停止计时

唯一难搞的就是chrono的层层包装

本文非常功利 不深究底层 ~

time_pointer

chrono::time_point<chrono::steady_clock> start; 

在chrono命名空间下(std下层) 有个神奇的类型 叫时间点time_point

在不同的操作环境下 有不同的实现 所以这是一个模板

模板类型可以有

  • chrono::high_resolution_clock 高解析度类型 不建议使用 因为这个可能有移植的问题 但好像进度最高?
  • chrono::steady_clock 稳得一批的钟 我超爱这个 因为这个不仅进度不低 而且调用的时间短,影响极小 (300ns
  • chrono::system_clock 系统带的钟 不大行 精度因系统而定? windows是100ns

所以 你懂的 用steady就好了(也不用太纠结几纳秒

给时间点一个当前时间 注意类型一致

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

duration

auto  dur = end - start; 

为啥用auto 因为方便昂(duration 模板具体化写到头皮发麻

时间点运算得到的是时间段 因为默认的时间点单位时间是纳秒(steady_clock),所以得到的时间是内部以longlong存储的多少纳秒

如何调出时间?

(end - start).count() 

得到的是longlong ns

如何更改单位时间?

一个是转换时间段的格式

chrono::duration_cast<chrono::microseconds>(end - start).count()) 

一个是转换时间点的格式

chrono::time_point_cast<chrono::microseconds>(start) 

如何调出一个时间戳?(系统从我也不知道哪开始算起的时间段 1970.1.1大概? 相当于帮你减了一下

start.time_since_epoch().count() 

可选格式:

  • chrono::nanoseconds

  • chrono::microseconds

  • chrono::milliseconds

  • chrono::seconds

  • chrono::minutes

  • chrono::hours

回到实现

构造函数没啥好讲的 就是开始计时

重点是析构函数

~InstrumentationTimer() {         auto end = chrono::steady_clock::now();         cout << m_hint << ':' << static_cast<double>((end - start).count()) / 1e6 << "msn";         long long llst = chrono::time_point_cast<chrono::microseconds>(start).time_since_epoch().count();         long long lled = chrono::time_point_cast<chrono::microseconds>(end).time_since_epoch().count();          Instrumentor::Get().WriteProfile({m_hint, llst, lled});     } 

思路:

  • 首先!!!一定先停止计时 (你不会还想增大误差吧) 用auto接住 省一个成员

  • 然后 输出的是你要计时的位置的注释(hint) 接一个时间段

    因为时间段输出的是longlong 我看多了几点几ms觉得非常亲切 所以用纳秒算时间段(默认)后再除1e6得到毫秒

  • 留两个时间戳后面有用

  • 然后是后面的调用记录某一段程序运行时间的函数啦 这里传进去的有hint 开始和结束的时间戳 有了这些 你就能算出经过的时间

整理输出部分

Chrome大法好

chromo 自带了个可视化分析软件 在地址栏上输入chrome://tracing/就可以看到

它接受的是json文件 所以我们要把我们记录下来的东西打包成json拖到界面上 就可以看到精美(并不) 的可视化界面

这是打包器+记录器的全貌

class Instrumentor { private:     ofstream m_OutputStream;     bool m_Fir;  public:     Instrumentor() : m_Fir(true) {}      void BeginSession(const string &filepath = "results.json") {         m_OutputStream.open(filepath);         WriteHeader();      }      void EndSession() {         WriteFooter();         m_OutputStream.close();         m_Fir = true;     }      void WriteProfile(const ProfileResult &result) {         if (!m_Fir) { //not add ',' when first time             m_OutputStream << ',';         } else m_Fir = false;          string name(result.Name);         replace(name.begin(), name.end(), '"', ''');         m_OutputStream << R"({)";         m_OutputStream << R"("cat":"function",)";         m_OutputStream << R"("dur":)" << result.end - result.start << ",";         m_OutputStream << R"("name":")" << name << "",";         m_OutputStream << R"("ph":"X",)";         m_OutputStream << R"("pid":0,)";         m_OutputStream << R"("tid":0,)";         m_OutputStream << R"("ts":)" << result.start;         m_OutputStream << R"(})";         m_OutputStream.flush();     }      void WriteHeader() {         m_OutputStream << R"({"otherData":{},"traceEvents":[)";         m_OutputStream.flush();     }      void WriteFooter() {         m_OutputStream << "]}";         m_OutputStream.flush();     }      static Instrumentor &Get() {         static auto instance = new Instrumentor();         return *instance;     } }; 

以及我们的目标 Chrome能识别的json文件

{   "otherData": {},   "traceEvents": [     {       "cat": "function",       "dur": 2166411,       "name": "void core1(int)",       "ph": "X",       "pid": 0,       "tid": 0,       "ts": 19699253339     },     {       "cat": "function",       "dur": 1649285,       "name": "void core2()",       "ph": "X",       "pid": 0,       "tid": 0,       "ts": 19701420118     },     {       "cat": "function",       "dur": 3816266,       "name": "void benchMark()",       "ph": "X",       "pid": 0,       "tid": 0,       "ts": 19699253338     }   ] } 

Get( )

首先看到最后的Get( )

static Instrumentor &Get() {     static auto instance = new Instrumentor();     return *instance; } 

这个能提供给我们一个单例,就是仅存在一个与我们运行时的对象

static 显式的指出Get得到的东西是和我们exe文件存在时间一样长的 而且这个定义只执行一次

如果你没有听懂 就只要记住它返回的永远是同一个对象 要用这个对象的时候就用Get

该这么用:

Instrumentor::Get().balabala(); 

初始化

private:     ofstream m_OutputStream;     bool m_Fir;  public:     Instrumentor() : m_Fir(true) {}      void BeginSession(const string &filepath = "results.json") {         m_OutputStream.open(filepath);         WriteHeader();      }      void EndSession() {         WriteFooter();         m_OutputStream.close();         m_Fir = true;     }   

ofsteam文件输出流用于输出到文件默认是results.json

不要忘记列表中的逗号的处理 我们用m_Fir检测是不是第一个

然后是注意到json开头和结尾是固定的

void WriteHeader() {     m_OutputStream << R"({"otherData":{},"traceEvents":[)";     m_OutputStream.flush(); }  void WriteFooter() {     m_OutputStream << "]}";     m_OutputStream.flush(); } 

R"( string )"即原始字符串 可以输出字符串里面的原本的字符 感兴趣的可以自行拓展更多有关知识 这里用了之后就不用打转义的双引号了

每次输出到文件时记得及时刷新 m_OutputStream.flush();防止之后的线程出现毛病

ok 现在我们可以这么用了

int main() {     Instrumentor::Get().BeginSession();     benchMark(); //测试的函数放这里     Instrumentor::Get().EndSession(); } 

中间列表的填写

但是?最最最重要的中间列表的填写呢?

在这里

void WriteProfile(const ProfileResult &result) {     if (!m_Fir) { //not add ',' when first time         m_OutputStream << ',';     } else m_Fir = false;      string name(result.Name);     replace(name.begin(), name.end(), '"', ''');     m_OutputStream << R"({)";     m_OutputStream << R"("cat":"function",)";     m_OutputStream << R"("dur":)" << result.end - result.start << ",";     m_OutputStream << R"("name":")" << name << "",";     m_OutputStream << R"("ph":"X",)";     m_OutputStream << R"("pid":0,)";     m_OutputStream << R"("tid":0,)";     m_OutputStream << R"("ts":)" << result.start;     m_OutputStream << R"(})";     m_OutputStream.flush(); } 

在InstrumentationTimer中的调用:

//m_hint 是计时器注释  llst 开始时间戳  lled 结束时间戳 Instrumentor::Get().WriteProfile({m_hint, llst, lled}); 

定义传进来的参数 可以扩展

struct ProfileResult {     string Name;     long long start, end; }; 

就是简单的往里面塞东西啦

值得注意的是 chrome 的tracing 默认时间戳的单位时间是microseconds 即毫秒 所以要记得转换格式哦

long long llst = chrono::time_point_cast<chrono::microseconds>(start).time_since_epoch().count(); long long lled = chrono::time_point_cast<chrono::microseconds>(end).time_since_epoch().count(); 

考虑到传进来的函数名字可能会带有" " 让json出错 所以退而求其次 把它转成 ' ' (其实在前面加一个转义字符更好 但是实现起来太麻烦了)

string name(result.Name); replace(name.begin(), name.end(), '"', '''); 

好啦 包装弄好了 下一步开始高效插桩

打桩

神说:“我怕麻烦。”

于是就有了宏

低级打桩

先看

void core1() {     InstrumentationTimer tt("halo world 0 to 9999");     for (int i = 0; i < 10000; ++i) {         cout << "Hello world #" << i << endl;     } }  void benchMark() {     InstrumentationTimer tt("shart benchMark");     core1(); } 

在一个函数的开头放上计时器 计时器就会自动记录这个作用域自它定义开始到结束所经过的时间和有关的信息

在计时器销毁前几微秒 它会将它所看到的的东西传给Instrumentor来记录所发生的事情

但是!!这未免也太傻了

为什么还要我手动给一个名字

让它自动生成独一无二的名字就行了嘛

中级打桩

有那么个宏 是所有编辑器都能自动展开的 叫 __FUNCTION__ 它会变成它所在的函数的名字的字符串

于是就有了

#define PROFILE_SCOPE(name) InstrumentationTimer tt(name) #define PROFILE_FUNCTION() PROFILE_SCOPE(__FUNCTION__) 
void core1() {     PROFILE_FUNCTION();     for (int i = 0; i < 10000; ++i) {         cout << "Hello world #" << i << endl;     } }  void benchMark() {     PROFILE_FUNCTION();     core1(); } 

好 但还不够好

所有的计时器都是一个名称 万一不小心重名了 那事情就不好整了

又有一个宏 叫 __LINE__ 它会变成所在行号(数字)

而宏能用神奇的 #将东西黏在一起

就有了

#define PROFILE_SCOPE(name) InstrumentationTimer tt##__LINE__(name) 

好 但还不够好

万一我的函数是重载的 输出的是一样的函数名字 我咋知道调用的是哪个版本的函数

又有一个宏 叫 __PRETTY_FUNCTION__ MSVC是 __FUNCSIG__它能变成完整的函数签名的字符串 就像 "void core1(int)"

#define PROFILE_FUNCTION() PROFILE_SCOPE(__PRETTY_FUNCTION__) 

好 但还不够好

这个我可不想把它保留在release下 让用户也帮我测测时间 怎么才能方便的关掉呢

对 还是宏

高级打桩

#define PROFILING 1 #if PROFILING #define PROFILE_SCOPE(name) InstrumentationTimer tt##__LINE__(name) #define PROFILE_FUNCTION() PROFILE_SCOPE(__PRETTY_FUNCTION__) #else #define PROFILE_SCOPE(name) #define PROFILE_FUNCTION() #endif  void core(int useless) {     PROFILE_FUNCTION();     for (int i = 0; i < 10000; ++i) {         cout << "Hello world #" << i << endl;     } }  void core() {     PROFILE_FUNCTION();     for (int i = 0; i < 10000; ++i) {         cout << "Hello world #" << sqrt(i) << endl;     } }  void benchMark() {     PROFILE_FUNCTION();     core(23333);     core(); } 

这就是了 如果我想关掉测试 就把profiling设为1 这是所有测试都只是空行 而release对于没有使用的函数则自动删去了 丝毫不影响性能

多线程

扩展

拓展ProfileResult

struct ProfileResult {     string Name;     long long start, end;     uint32_t TheadID; }; 

更改输出

m_OutputStream << R"("tid":)" << result.TheadID << ","; 

在Timer中捕获该线程的id 并用自带hash转换成uint32方便输出

uint32_t threadID = hash<std::thread::id>{}(std::this_thread::get_id()); 

传递id

Instrumentor::Get().WriteProfile({m_hint, llst, lled,threadID}); 

最后变成了这样

~InstrumentationTimer() {     auto end = chrono::steady_clock::now();     cout << m_hint << ':' << static_cast<double>((end - start).count()) / 1e6 << "msn";     long long llst = chrono::time_point_cast<chrono::microseconds>(start).time_since_epoch().count();     long long lled = chrono::time_point_cast<chrono::microseconds>(end).time_since_epoch().count();      uint32_t threadID = hash<std::thread::id>{}(std::this_thread::get_id());      Instrumentor::Get().WriteProfile({m_hint, llst, lled,threadID}); } 

测试

搞一个多线程出来

void benchMark() {     PROFILE_FUNCTION();     cout << "Running BenchMarks...n";     thread a([]() { core(23333); });     thread b([]() { core(); });      a.join();     b.join(); } 

用lamda可以非常简洁的开多线程重载函数

最后加入2个join函数 这样在这两个线程都完成它们的工作之前 我们不会真正退出这个benchmark函数

完成

好啦 我们的工作完成了 欣赏一下代码吧

#include <bits/stdc++.h> #include <sstream>  using namespace std;  struct ProfileResult {     string Name;     long long start, end;     uint32_t TheadID; };  class Instrumentor { private:     ofstream m_OutputStream;     bool m_Fir;  public:     Instrumentor() : m_Fir(true) {}      void BeginSession(const string &filepath = "results.json") {         m_OutputStream.open(filepath);         WriteHeader();      }      void EndSession() {         WriteFooter();         m_OutputStream.close();         m_Fir = true;     }      void WriteProfile(const ProfileResult &result) {         if (!m_Fir) { //not add ',' when first time             m_OutputStream << ',';         } else m_Fir = false;          string name(result.Name);         replace(name.begin(), name.end(), '"', ''');         m_OutputStream << R"({)";         m_OutputStream << R"("cat":"function",)";         m_OutputStream << R"("dur":)" << result.end - result.start << ",";         m_OutputStream << R"("name":")" << name << "",";         m_OutputStream << R"("ph":"X",)";         m_OutputStream << R"("pid":0,)";         m_OutputStream << R"("tid":)" << result.TheadID << ",";         m_OutputStream << R"("ts":)" << result.start;         m_OutputStream << R"(})";         m_OutputStream.flush();     }      void WriteHeader() {         m_OutputStream << R"({"otherData":{},"traceEvents":[)";         m_OutputStream.flush();     }      void WriteFooter() {         m_OutputStream << "]}";         m_OutputStream.flush();     }      static Instrumentor &Get() {         static auto instance = new Instrumentor();         return *instance;     } };   class InstrumentationTimer { private:     chrono::time_point<chrono::steady_clock> start;     const char *m_hint;  public:     explicit InstrumentationTimer(const char *hint) : m_hint(hint) {         start = chrono::steady_clock::now();     }       ~InstrumentationTimer() {         auto end = chrono::steady_clock::now();         cout << m_hint << ':' << static_cast<double>((end - start).count()) / 1e6 << "msn";         long long llst = chrono::time_point_cast<chrono::microseconds>(start).time_since_epoch().count();         long long lled = chrono::time_point_cast<chrono::microseconds>(end).time_since_epoch().count();          uint32_t threadID = hash<std::thread::id>{}(std::this_thread::get_id());          Instrumentor::Get().WriteProfile({m_hint, llst, lled,threadID});     } };  #define PROFILING 1 #if PROFILING #define PROFILE_SCOPE(name) InstrumentationTimer tt##__LINE__(name) #define PROFILE_FUNCTION() PROFILE_SCOPE(__PRETTY_FUNCTION__) #else #define PROFILE_SCOPE(name) #define PROFILE_FUNCTION() #endif  void core(int useless) {     PROFILE_FUNCTION();     for (int i = 0; i < 10000; ++i) {         cout << "Hello world #" << i << endl;     } }  void core() {     PROFILE_FUNCTION();     for (int i = 0; i < 10000; ++i) {         cout << "Hello world #" << sqrt(i) << endl;     } }  void benchMark() {     PROFILE_FUNCTION();     cout << "Running BenchMarks...n";     thread a([]() { core(23333); });     thread b([]() { core(); });      a.join();     b.join(); }   int main() {     Instrumentor::Get().BeginSession();     benchMark();     Instrumentor::Get().EndSession(); } 

最后的json

{   "otherData": {},   "traceEvents": [     {       "cat": "function",       "dur": 3844575,       "name": "void core(int)",       "ph": "X",       "pid": 0,       "tid": 1709724944,       "ts": 24887197644     },     {       "cat": "function",       "dur": 4039317,       "name": "void core()",       "ph": "X",       "pid": 0,       "tid": 2740856708,       "ts": 24887197714     },     {       "cat": "function",       "dur": 4040539,       "name": "void benchMark()",       "ph": "X",       "pid": 0,       "tid": 2850328247,       "ts": 24887196811     }   ] } 

细心的小伙伴可以推一推运行这段代码时间是什么时候呢~

发表评论

相关文章