使用RAII技术简化代码耗时统计

灵光闪现

这一久一直在搞性能瓶颈排查,然后每次都需要计算某个代码段的耗时,传统的做法,就是在起始点记录时间点,结束做个时间点,然后再把该段耗时打到日志中进行分析。这样需要额外引入获取当前时间点的代码,又要写一些变量保存。然在在决定要使用单词耗时还是平均耗时,或者这么多次中的最小耗时都只能在日志文件中去用肉眼分析。不然也得引入太多代码,太多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()函数中实现不同的功能,就可以轻松实现需要统计单次耗时、多次总耗时、多次中的最小耗时、多次中的最大耗时,甚至是多次的平均耗时等功能。按照这个思路我画了一个思维导图,如下:在这里插入图片描述

统计单次耗时

  1. 需要实现bengin()方法,
  2. 需要实现end()方法
  3. 成员仅仅需要名字即可。
  4. 使用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);
	}
};

统计总耗时

  1. 需要判断运行此时是否超过设置的counter
  2. 总耗时仅仅需要在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;
		}
	}
};

统计最小耗时

  1. 需要判断运行此时是否超过设置的counter
  2. 只需要在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;
		}
	}
};

统计最大耗时

  1. 需要判断运行此时是否超过设置的counter
  2. 只需要在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;
		}
	}
};

统计平均耗时

  1. 需要判断运行的次数是否超过设置的counter
  2. 像总耗时一样把每个耗时相加。然后在除以设置的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 模板类

  1. 需要将具体的消耗计时类传入引用,像std::lock_guard<> 那样
  2. 需要在构造时候调用begin()
  3. 需要在析构时候调用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;
}
  1. 由于ElapsedGuard 模板类使用的是引用,所以需要先定义一些统计耗时的的对象。
// 测试的次数
int testCounter = 5;
SingleElapsed sigle("TestSigleTime");
TotalElapsed total("TestTotalTime", testCounter);
MinElapsed minElapsed("TestMinTime", testCounter);
MaxElapsed maxElapsed("TestMaxTime", testCounter);
AverageElapsed average("TestAverageTime", testCounter);
  1. 测试的代码,仅仅只用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();
}
  1. 剩下的工作就是怎么调用这些测试函数了,测试单次耗时的调用一次即可,其他的多调用几次,为了心里有谱,还把每次睡眠耗时给保存在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的析构函数依旧会调用,计时依旧能够得到统计。使用这几个类基本上可以应对大部分的耗时统计工作,尽管这种方式可能无法取代专业的性能分析工具,但在日常开发中,它可以为我们提供快速且精准的耗时统计,帮助我们更好地理解和优化代码。如果你有更好的建议,可以在下方留言。

  • 23
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值