天天看点

万兆流量下,如何正确使用字符串进行处理

本系列主要记录笔者在网络流量解析开发过程中,碰到的一些性能问题。

何为Copy-On-Write

即写时复制,也叫成为引用计数。这种策略可以消除不必要的内存分配和不必要的字符拷贝,从而可以提高程序运行效率。比如:

void Print(std::string str)
{
	std::cout << str << std::endl;
}           

上述代码中,参数传值重新构造str并不涉及深拷贝,只进行了一次浅拷贝。

引用技术本身是没有问题的。问题出在什么时候进行深拷贝?下面来看gcc-4.8.5中string::operator[]的实现。

const_reference
operator[] (size_type __pos) const
{
    __glibcxx_assert(__pos <= size());
    return _M_data()[__pos];
}

reference
operator[](size_type __pos)
{
    __glibcxx_assert(__pos <= size());

    _M_leak();
    return _M_data()[__pos];
}           

问:为什么需要区分这两种?

答:我们可以看到有两个重载函数。其中一个是常量函数(不涉及字符串更改),另一个是普通函数(可能会改动字符串)。普通函数返回了字符串引用,这次调用是可能出现修改字符串的。但是,这个string本身可能有多个引用副本,这时候就需要进行Copy。不复制就会意外修改其他无辜string。对于前面的复制操作也失去了意义。

问题一:在常量场合使用非常量操作

先来看一段代码。

int StringFind(String str)
{
  for (int i=0; i<str.size(); i+=)
  {
  	if (isprint(str[i]))
    	return i;
  }
  return -1;
}           

上述代码简单地返回第一个可打印字符串位置。整个操作其实是不修改str的。但是由于str不是常量,默认调用了普通的operator[]。因此,第一次调用str[i]会进行一次字符串的深拷贝。如果字符串很长,则在这个函数中消耗大量的性能。如果是多线程情况,由于_M_mutate()是会进行上锁操作,也很影响性能。

如何解决?看一下实例:

int StringFind(const String& str)
{
  for (int i=0; i<str.size(); i+=)
  {
  	if (isprint(str[i]))
    	return i;
  }
  return -1;
}           

我们只需要将参数改为const类型的引用就可以避免这个问题。

问题二:在非常量场合违规操作

经常调试性能的同学都知道,直接char* 字符串操作性能远大于string提供的API。如:

void StringRelace(String& str)
{
  char* ptr = str.c_str(); // 也可以调用str.data()
  for (int i=0; i<str.size(); i+=)
  {
  	if (! isprint(str[i]))
    	ptr[i] = '.';
  }
}           

上述代码实现输入一个字符串将不可见字符转换为点'.'。可以看到,为了性能取巧地使用了str.c_str()先获得字符串地址,然后进行遍历修改。看似没问题?问题大了!

我们上面说过字符串存在Copy-On-Write, 这个str不知道多少个string与之共享同一个地址。这时候str不通过string官方的方式进行修改,就会造成其他string也一起被改变。那这里该如何操作?简单!我们执行一次非const的operator[]不就行了。

void StringRelace(String& str)
{
  char* ptr = &str[0];
  for (int i=0; i<str.size(); i+=)
  {
  	if (! isprint(str[i]))
    	ptr[i] = '.';
  }
}           

将char* ptr = str.c_str(); 改为 char* ptr = &str[0];。进行一次写时深拷贝,再进行操作。

结束语

以上便是std::string引用计数相关的介绍。开始写代码时,const,static这些关键字不是很了解,只有坑过了之后才领悟深刻。这两个关键字,只要能加的场合尽量加上,不仅是阅读代码时必要,也更是编译器优化时需要的。

继续阅读