灵光闪现
这一久一直在搞性能瓶颈排查,然后每次都需要计算某个代码段的耗时,传统的做法,就是在起始点记录时间点,结束做个时间点,然后再把该段耗时打到日志中进行分析。这样需要额外引入获取当前时间点的代码,又要写一些变量保存。然在在决定要使用单词耗时还是平均耗时,或者这么多次中的最小耗时都只能在日志文件中去用肉眼分析。不然也得引入太多代码,太多if,让代码看起眼花缭乱的。经过我仔细的思考,为啥不使用raii技术来进行统计呢。就行std::lock_guard<> 那样,只要模板类的指定类型包含bengin(), end() 函数就可以在消耗守护类构造时候调用bengin(), 析构调用end(), 然后在end()中实现不同模板类的功能,比如统计单次的计时,统计很多次总的耗时,统计很多次中最小的耗时,统计很多次中最大的耗时,统计很多次中平均耗时。等等功能。
这一久在性能瓶颈排查工作中,经常需要计算特定代码段的执行耗时。传统的方法是在代码段的起始和结束处分别记录时间,然后计算时间差并将其记录在日志中以供分析。这种做法的问题在于,我们需要额外地引入获取当前时间的代码,同时还要创建变量来保存时间点。
其中单次耗时、平均耗时、或者在多次执行中的最小耗时等,我们通常需要通过日志文件进行分析。如果我们试图在代码中实现这些功能,就需要引入大量的代码和条件语句,使代码变得复杂而混乱。
在深思熟虑后,我发现可以借助RAII(Resource Acquisition Is Initialization)技术来简化代码耗时统计。RAII的思想是在对象创建时开始计时,在对象销毁时计算这段耗时,并可以多次统计计算出最小值,最大值,或者平均值。这不就是std::lock_guard<>的思想吗?只需要实现一个类,其只要求模板参数类型包含begin()和end()函数。在RAII的模板类的构造函数中调用begin()函数,在析构函数中调用end()函数。通过在end()函数中实现不同的功能,就可以轻松实现需要统计单次耗时、多次总耗时、多次中的最小耗时、多次中的最大耗时,甚至是多次的平均耗时等功能。按照这个思路我画了一个思维导图,如下:
统计单次耗时
- 需要实现bengin()方法,
- 需要实现end()方法
- 成员仅仅需要名字即可。
- 使用chrono 的精准时间点做统计依据。如果还不太清楚chrono库,可以看看c++标准库中chrono的使用方法
class SingleElapsed
{
private:
std::chrono::steady_clock::time_point m_start;
// 记录调用者的名字,可以是函数名或者一些带有标识的字符串
std::string m_name;
public:
SingleElapsed(const std::string& name)
: m_name(name)
{
}
SingleElapsed(const SingleElapsed &) = delete; // no copy
// 代码相当简单 内联一下
inline void Begin()
{
m_start = std::chrono::steady_clock::now();
}
void End()
{
auto now = std::chrono::steady_clock::now();
auto diff = std::chrono::duration_cast<std::chrono::milliseconds>
(std::chrono::steady_clock::now() - m_start).count();
// 这里直接打印,也可以写文件打日志之类的
printf("%s elapse %lldms every one times \n", m_name.c_str(), diff);
}
};
统计总耗时
- 需要判断运行此时是否超过设置的counter
- 总耗时仅仅需要在end方法中把每次耗时相加即可,相当简单。
class TotalElapsed
{
private:
std::chrono::steady_clock::time_point m_start;
std::string m_name;
int m_counter; // 需要运行的次数
int m_index;
uint64_t m_totalElapsed; // 总耗时
public:
TotalElapsed(const std::string &name, int counter)
: m_name(name),
m_counter(counter),
m_index(0),
m_totalElapsed(0)
{
}
TotalElapsed(const TotalElapsed &) = delete; // no copy
inline void Begin()
{
m_start = std::chrono::steady_clock::now();
}
void End()
{
auto now = std::chrono::steady_clock::now();
auto diff = std::chrono::duration_cast<std::chrono::milliseconds>
(std::chrono::steady_clock::now() - m_start).count();
m_totalElapsed += diff;
if (++m_index >= m_counter)
{
printf("%s total elapse %lldms every %d times\n", m_name.c_str(), m_totalElapsed, m_counter);
m_index = 0;
}
}
};
统计最小耗时
- 需要判断运行此时是否超过设置的counter
- 只需要在end方法中比较每次的耗时,记录最小的耗时即可。
class MinElapsed
{
private:
std::chrono::steady_clock::time_point m_start;
std::string m_name;
int m_counter;
int m_index;
uint64_t m_minElapsed; // 最小耗时
public:
MinElapsed(const std::string &name, int counter)
: m_name(name),
m_counter(counter),
m_index(0),
m_minElapsed(UINT64_MAX) // 切记初始化为最大值
{
}
MinElapsed(const MinElapsed &) = delete; // no copy
inline void Begin()
{
m_start = std::chrono::steady_clock::now();
}
void End()
{
auto now = std::chrono::steady_clock::now();
auto diff = std::chrono::duration_cast<std::chrono::milliseconds>
(std::chrono::steady_clock::now() - m_start).count();
// 小于更新值
if (diff < m_minElapsed)
{
m_minElapsed = diff;
}
if (++m_index >= m_counter)
{
printf("%s min elapse %lldms every %d times \n", m_name.c_str(), m_minElapsed, m_counter);
m_index = 0;
}
}
};
统计最大耗时
- 需要判断运行此时是否超过设置的counter
- 只需要在end方法中比较每次的耗时,记录最大的耗时即可。
class MaxElapsed
{
private:
std::chrono::steady_clock::time_point m_start;
std::string m_name;
int m_counter;
int m_index;
uint64_t m_maxElapsed; // 最大耗时
public:
MaxElapsed(const std::string &name, int counter)
: m_name(name),
m_counter(counter),
m_index(0),
m_maxElapsed(0)
{
}
MaxElapsed(const MaxElapsed &) = delete; // no copy
inline void Begin()
{
m_start = std::chrono::steady_clock::now();
}
void End()
{
auto now = std::chrono::steady_clock::now();
auto diff = std::chrono::duration_cast<std::chrono::milliseconds>
(std::chrono::steady_clock::now() - m_start).count();
if (diff > m_maxElapsed)
{
m_maxElapsed = diff;
}
if (++m_index >= m_counter)
{
printf("%s max elapse %lldms every %d times \n", m_name.c_str(), m_maxElapsed, m_counter);
m_index = 0;
}
}
};
统计平均耗时
- 需要判断运行的次数是否超过设置的counter
- 像总耗时一样把每个耗时相加。然后在除以设置的counter即可
class AverageElapsed
{
private:
std::chrono::steady_clock::time_point m_start;
std::string m_name;
int m_counter;
int m_index;
uint64_t m_totalElapsed;
public:
AverageElapsed(const std::string &name, int counter)
: m_name(name),
m_counter(counter),
m_index(0),
m_totalElapsed(0)
{
}
AverageElapsed(const AverageElapsed &) = delete;
inline void Begin()
{
m_start = std::chrono::steady_clock::now();
}
void End()
{
auto now = std::chrono::steady_clock::now();
auto diff = std::chrono::duration_cast<std::chrono::milliseconds>
(std::chrono::steady_clock::now() - m_start).count();
m_totalElapsed += diff;
if (++m_index >= m_counter)
{
printf("%s average elapse %.2fms every %d times \n",
m_name.c_str(), static_cast<double>(m_totalElapsed) / static_cast<double>(m_index), m_counter);
m_totalElapsed = 0;
m_index = 0;
}
}
};
RAII 模板类
- 需要将具体的消耗计时类传入引用,像std::lock_guard<> 那样
- 需要在构造时候调用begin()
- 需要在析构时候调用end()
template<typename Elapsed>
class ElapsedGuard
{
private:
Elapsed &m_elapsed;
public:
ElapsedGuard(Elapsed &elapsed)
: m_elapsed(elapsed)
{
m_elapsed.Begin();
}
~ElapsedGuard()
{
m_elapsed.End();
}
};
测试
1.为了测试这么多消耗类是否达到我们的要求,我们应该准备一些测试代码,可以使用睡眠的方式模拟真实的代码耗时,所以定义了一个随机睡眠函数int RandomSleep() 。
int RandomSleep()
{
std::random_device rd;
std::mt19937 gen(rd());
// 可以把随机点落到500 ~ 1000 这个区间上
std::uniform_int_distribution<> dis(500, 1000);
int sleepMs = dis(gen);
std::this_thread::sleep_for(std::chrono::milliseconds(sleepMs));
// 返回睡眠时间是为了记录查看下
return sleepMs;
}
- 由于ElapsedGuard 模板类使用的是引用,所以需要先定义一些统计耗时的的对象。
// 测试的次数
int testCounter = 5;
SingleElapsed sigle("TestSigleTime");
TotalElapsed total("TestTotalTime", testCounter);
MinElapsed minElapsed("TestMinTime", testCounter);
MaxElapsed maxElapsed("TestMaxTime", testCounter);
AverageElapsed average("TestAverageTime", testCounter);
- 测试的代码,仅仅只用SingleElapsed 的对象定义一个ElapsedGuard 对象在构造,调begin, 析构调end 即可
void TestSigleTime()
{
ElapsedGuard<SingleElapsed> guard(sigle);
auto sleepMs = RandomSleep();
printf("-------sigle %d\n", sleepMs);
}
int TestTotalTime()
{
ElapsedGuard<TotalElapsed> guard(total);
return RandomSleep();
}
int TestMinTime()
{
ElapsedGuard<MinElapsed> guard(minElapsed);
return RandomSleep();
}
int TestMaxTime()
{
ElapsedGuard<MaxElapsed> guard(maxElapsed);
return RandomSleep();
}
int TestAverageTime()
{
ElapsedGuard<AverageElapsed> guard(average);
return RandomSleep();
}
- 剩下的工作就是怎么调用这些测试函数了,测试单次耗时的调用一次即可,其他的多调用几次,为了心里有谱,还把每次睡眠耗时给保存在vector中。
然后一一打印出来观察
int main()
{
TestSigleTime();
std::vector<int> sleepMss[4];
for (int i = 0; i < testCounter; i++)
{
sleepMss[0].push_back(TestTotalTime());
sleepMss[1].push_back(TestMinTime());
sleepMss[2].push_back(TestMaxTime());
sleepMss[3].push_back(TestAverageTime());
}
printf("tota \t min \t max\t average\n");
for (int i = 0; i < testCounter; i++)
{
printf("%d \t %d \t %d\t %d\n",
sleepMss[0][i], sleepMss[1][i], sleepMss[2][i], sleepMss[3][i]);
}
}
看看最后的输出,一目了然。
-------sigle 973
TestSigleTime elapse 982ms every one times
TestTotalTime total elapse 3864ms every 5 times
TestMinTime min elapse 531ms every 5 times
TestMaxTime max elapse 920ms every 5 times
TestAverageTime average elapse 689.00ms every 5 times
tota min max average
902 852 520 681
823 950 633 662
974 954 706 579
541 643 920 790
585 520 636 693
总结
我们只需要在代码块内创建一个ElapsedGuard对象即可,无需多余的代码开始打点和结束打点,即使代码块抛出了异常,ElapsedGuard的析构函数依旧会调用,计时依旧能够得到统计。使用这几个类基本上可以应对大部分的耗时统计工作,尽管这种方式可能无法取代专业的性能分析工具,但在日常开发中,它可以为我们提供快速且精准的耗时统计,帮助我们更好地理解和优化代码。如果你有更好的建议,可以在下方留言。