本系列主要記錄筆者在網絡流量解析開發過程中,碰到的一些性能問題。
何為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這些關鍵字不是很了解,隻有坑過了之後才領悟深刻。這兩個關鍵字,隻要能加的場合盡量加上,不僅是閱讀代碼時必要,也更是編譯器優化時需要的。