字符串的終極優化

   地球人(若是程序員還算是人的話)對字符串處理都不陌生,無論是低級語言仍是高級語言,都離不開對字符串的處理,而經常使用的開發工具都對字符處操做提供了儘量詳盡的幫助,標準C中的各類字符串函數(strcpy、strcmp和strcat……),C++標準庫中提供了功能更強大的string,而微軟的MFC中提供的CString更是強大至極,是否還有理由寫本身的字符串處理類?幸福的程序員都是類似的,大家徹底能夠在已存在的各類類庫中選擇,而我這年邁的老程序員卻很是不幸,由於我有太多的理由要寫本身的字符串處理類……表面看是爲了終極優化,其實歸根結底是由於我是屌絲,由於我很窮,由於我沒有錢購買更多更好的服務器。程序員

  先交待一下背景,本人某年革月某日忽然對搜索引擎技術發生了深厚的興趣,對於愛玩技術的我來講,這股激情涌來已難於抑制,向老婆大人彙報了個人思想動態,強烈要求提供資金支持。老婆大人向來對個人這種玩勁不支持不提倡不反對,在她看來我玩技術是比打麻將更高雅一點的娛樂而已,男人嘛,總得有一點不良愛好。因此此次她大方了一點,大筆一揮批了一萬元「賭資」。一萬元的資金,包括購買服務器和服務器託管等,而服務器託管費用一年至少要七千多吧?那服務器的價格只能是兩千多了(我不知道比普通PC還要廉價的服務器是否還能叫服務器)。而對於一個完整的搜索引擎來講,大致上須要幾個大模塊:數據抓取、數據分析、中文分詞、建立索引、信息去重、信息自動聚類、大數據存儲、數據壓縮和解壓縮、查詢和排序、WEB頁面生成、WEB服務器……若是你對這些的計算量尚未概念的話,我能夠告訴你,某公司對幾十萬個商品進行自動聚類計算,須要採用兩臺服務器跑三天……全部這些工做我都得在一臺最低端的「山寨服務器」上實時完成,這無異於要把大象裝進冰箱,無論元芳怎麼看,採用常規思路恐怕是行不通了,惟有優化而且往死命裏優化,纔有可能殺出生機。編程

  對於搜索引擎的整個工做來講,從數據抓取、數據分析、中文分詞、WEB頁面拼裝等都是對字符串在操做,能夠說90%以上的時間都與字符串操做有關,因此除了框架上的設計要有講究以外,字符串的處理效率直接影響到搜索引擎的總體性能。那得回答兩個問題:現成的字符串類在效率方面怎麼樣?是否還有可優化的空間?經本人對各類字符串類作了詳盡研究後發現,現成的字符串類在性能方面雖然表現不錯,但僅僅只是不錯而已,還有很是大的優化空間。下面結合個人具體應用慢慢道來。安全

 

1、對於服務器來講,內存管理是最重要的事情之一服務器

  對於搜索引擎服務來講,各類數據,特別是字符串的構造、拉接、清空、子串代替、提取子串、重置長度等等特別頻繁,對內存分配和釋放操做幾乎每一毫秒都在發生,這會有什麼嗎?本人曾在新浪微博中提過一個說法「一個混沌的系統如何可以天然而然地達到有序?這是不可能發生的事情,在非人工干預的狀況下,全部事情的發展都是從有序到混沌的過程。對於長時間連續運行的服務器系統來講,內存資源不停地申請和釋放交替進行,不可避免地造成內存碎片。意識到這一點你成不了大牛,但沒有意識到這一點就連牛B都吹不響。」框架

  當你意識到內存碎片對服務器的影響裏,首先想到的是內存池。不錯,一個設計優良的內存池能夠減小內存碎片,同時也大大提高了性能。話說我們的字符串類,首要考慮的問題就是與內存池技術的結合了。如何搞呢?不少現成的字符串類都提供定製內存管理的接口(網上已有不少這方面的資料,若有興趣能夠搜索),字符串類也必須有這個。因爲搜索引擎的工做機制,各線程都是獨立工做的,不多有線程間的互訪問題。因此字符串對象的全部操做都是在一個線程內進行,那麼在內存池的設計中爲了性能的緣由而採用「無鎖編程」技術是至關容易的,邊內存的申請和釋放都不該使用系統的new和delete(由於這兩個操做是有鎖的),而改用私有堆的方式。這雖與字符串的性能有很大關係,但畢竟是內存管理方面的內容,之後有空再詳細聊了,先此打住。ide

 

2、支持寫時拷貝技術是提高性能的基本函數

  寫時拷貝是什麼東西?寫時拷貝的英文縮寫COW (copy-on-write), 簡單來講就是在複製一個對象的時候並非真正的把原先的對象複製到內存的另一個位置上,而是在新對象的內存映射表中設置一個指針,指向源對象的位置,並把那塊內存內部作只讀標誌。這樣,在對新的對象執行讀操做的時候,內存數據不發生任何變更,直接執行讀操做;而在對新的對象執行寫操做(有變化)時,纔將真正的對象複製到新的內存地址中,並修改新對象的內存映射表指向這個新的位置,並在新的內存位置上執行寫操做。工具

  這種寫時拷貝的技術在操做系統底層是普通存在的,好比在同一個系統中運行多個進程,將多進程中一樣的對象(數據)在物理存儲其中只有一個物理存儲空間,而當其中的某一個進程試圖對該區域進行寫操做時,內核就會在物理存儲器中開闢一個新的物理頁面,將須要寫的區域內容複製到新的物理頁面中,而後對新的物理頁面進行寫操做。這時就是實現了對不一樣進程的操做而不會產生影響其餘的進程,同時也節省了不少的物理存儲器。字符串雖然與操做系統不在同一層級,但也有其共有的原理。在各函數中,免不了以字符串對象爲參數或做爲返回值的,還有在大量的字符串運算場合中,寫時拷貝技術將大大減小沒必要要的內存申請、釋放、拷貝等。性能

  在我所能找到的字符串類中,基本上能夠分紅三種,一是有的庫提供的字符串類竟沒有支持寫時複製;二是有支持寫時複製的,但爲了線程安全而採用了鎖技術,形成性能慢得象老牛;3、也有支持寫時複製技術而不採用鎖的,但在內存管理接口竟是全局的,沒法支持「無鎖編程」。我一直記得前面說過的,前提是已擁有基於私有堆的無鎖的高效的內存池,若是我們的字符串類沒有利用這一成果,那是自宮行爲。開發工具

 

3、優化格式化函數

  字符串的格式化操做幾乎是無所不在,有小的操做,好比整數、浮點數、日期、時間等轉字符串形式。而大的操做,好比在生成WEB頁面時須要往模板中按格式填入真實數據。在傳統的字符串處理類中(好比MFC帶的CString)提供了Format和AppendFormat兩個方法,在應用時會發現性能低下,經分析發現好傢伙,這兩個函數在內部潛伏了CPU大鱷:GetFormattedLengt。這是幹什麼的呢?就是在真正格式化以前先計算所須要存儲區的長度的,

這個函數至關的費時間。我採用空間換時間的理念,爲Format和AppendFormat兩個方法增長估計長度參數,在外部調用前就先估計其長度(通常都是比實現可能長度還要大不少,雖然有可能形成空間的小浪費,但獲得性能的回報也是值得的),這樣在內部真實執行格式化時不須要再計算,性能直接提高一倍多。

 

4、優化「+=」操做

  別告訴我你沒有見過字符串的「+=」操做,字符串的拼接操做比街上的美女還要多,若是你採用「Str1 = Str1 + Str2」的形式而不是「Str1 += Str2」的形式來拼接,這無異於讓美女放棄優雅與你吐口水玩。

雖然因爲有寫時拷貝技術的支持,「Str1 = Str1 + Str2」形式的操做雖不優雅但在性能上也並不差得太多,而畢竟是有「相加」和「賦值」兩個操做在裏面,給後續優化帶來了困難。對於WEB服務器來講,拼接一個幾十K甚至幾百K的WEB頁面是分分秒秒的事情。而傳統的字符串類對此並無作什麼特別的處理,頻繁而連續的「+=」操做形成內存不斷地被申請、釋放、拷貝。針對這種應用場景,我採用了兩個策略,一個是上面提到的「空間換時間」策略,在開始就對字符串對象分配足夠大的內存。二是由於「足夠大」是很差評估的,爲了不太頻繁的大內存移動操做,我對「+=」操做搞了一點點策略性的優化,也就是先用小內存塊列表保存新加進來的數據而不是當即拼接到原來的字符串後面,在達到必定規模或者須要進行其它操做時才一次性地分配內存並完成真正的拼接過程。在應用環境下進行實測發現,性能提升了一個數量級。

 

5、針對小數據字符串作特殊處理

  在搜索中,小數據字符串象星星同樣遍及各地,這些傢伙個頭不大但數量繁多,好比達百萬級的關鍵詞和達千萬級的用戶查詢短語,若是都使用上面所說的「空間換時間」理念,那浪費的內存空間會讓你死得很難看(打個比方,這就象你買房子多花那麼千兒八百的那無所謂了,若是你天天吃飯都多花千兒八百的,就等着你老婆砍你吧)。對於小數據的狀況,一方面是內存分配時的成長粒度上要斤斤計較,另外一方面在內存池的處理上也要對小內存的分配和釋放作策略上的調整。

這提及來好象挺簡單的,在實施過程當中比較折磨人,沒有固定的模式,全是經驗模型。

 

  以上就是比較接近常規方式的字符串處理類的優化過程,在實際應用中的效果至關使人滿意。

  之因此說以上的優化都是接近常規的方式,是由於本人還瘋狂地建立了「零內存」字符串類,以幾乎不增長內存的方式對大量字符串數據作各類處理,好比爲了提升搜索性能而對關鍵字建立搜索樹等。既然是很是規方式,那也也只適用於特定應用場景。

相關文章
相關標籤/搜索