第5篇:CPython內(nèi)部探究:PyUnicodeObject的實例化

示例:PyUnicodeObject初始化過程

那么關(guān)于“”這個unicode字符的是一個對應的PyUnicodeObject的內(nèi)存模型和PyASCIIObject和PyCompactUnicodeObject有一些差別,以“”字符串為例距潘,我們快速瀏覽一下PyUnicodeObject的初始化過程镰禾。

如果字符串嚴格由Latin-1范圍內(nèi)的字符組成,則Python將占用盡可能少的空間,并完全使用1字節(jié)的字符對象惶傻。 但是嚼摩,只要該字符串包含UCS-2字符,就必須將所有其他字符也轉(zhuǎn)換為占用2個字節(jié)阅签。同理, 如果字符包含UCS-4字符掐暮,那么字節(jié)序列中的所有字符都轉(zhuǎn)換為4字節(jié)。

如前一篇所述政钟,第一次調(diào)用PyUnicode_New函數(shù)路克,CPython總是假定被字符串是一個ASCII字符串,因此下圖分配以一個字節(jié)的位寬來給字符串分配內(nèi)存

因此第一次從PyUnicode_New返回給unicoce_decode_utf8函數(shù)一個PyASCIIObject的實例锥涕,如下圖所示

接著衷戈,unicoce_decode_utf8函數(shù)將該PyASCIIObject實例傳遞給ascii_decode函數(shù),


這里需要注意的是PyUnicode_1BYTE_DATA該宏函數(shù)獲取的是PyASCIIObject實例中有效負載部分的首個字節(jié)的內(nèi)存地址层坠,我們這里可以分析一下PyUnicode_1BYTE_DATA這個宏函數(shù)的行為,

在編譯時,按照如下圖的執(zhí)行順序最終等價于,在本示例中獲取PyASCIIObject對象的有效負載的地址是2870023376

(Py_UCS*)((PyASCIIObject*)(op) + 1)

話說回來,在ascii_decode函數(shù)的上下文殖妇,start參數(shù)持有C級別字符串首個字節(jié)的地址(本示例假定2869856865),end參數(shù)持有C級別字符串的尾指針(假定是2869856869)破花,*dest參數(shù)持有PyASCIIObject對象的有效負載的地址2870023376 谦趣,根據(jù)下圖的執(zhí)行軌跡,ascii_decode函數(shù)沒有對PyASCIIObject的有效負載部分做任何操作座每。并且返回一個0的指針偏移量前鹅。

在ascii_decode函數(shù)返回后,在當前unicode_decode_utf8函數(shù)的上下文峭梳,變量s持有仍然是指向C級別utf-8字節(jié)序列的首個字節(jié)舰绘,end指針仍然指向的是C級別utf-8字節(jié)序列的末端字節(jié)。如果你了解之前

asciilib_utf8_decode函數(shù)從unicode_decode_utf8函數(shù)獲取如下參數(shù)葱椭,參數(shù)inptr是二級指針捂寿,它使得事實上可以偏移C級別utf8字節(jié)序列的指針s,end參數(shù)指向C級別utf8字節(jié)序列的末端字節(jié)。參數(shù)outpos實際上修改_PyUnicodeWriter的pos字段的值

根據(jù)上圖的執(zhí)行軌跡孵运,在執(zhí)行到第三個紅框變量最終得到ch的值是128013秦陋,剛好正是“”unicode的十進制編碼,并且s指針已經(jīng)位移到4個字節(jié)已經(jīng)到達字節(jié)序列的末端治笨。內(nèi)存狀態(tài)圖如下

解碼后的ch值會返回給unicode_decode_utf8函數(shù)驳概,按照如下圖的執(zhí)行軌跡,ch=128013傳遞給PyUnicodeWriter_WriteCharInline函數(shù)赤嚼。

事實上整個PyUnicodeWriter_WriteCharInline的核心代碼,就是_PyUnicodeWriter_PrepareInternal函數(shù)顺又,執(zhí)行本示例時更卒,傳入該函數(shù)的參數(shù)length=1,參數(shù)maxchar就是128013

根據(jù)_PyUnicodeWriter_PrepareInternal函數(shù)上下文的執(zhí)行軌跡,再次調(diào)用PyUnicode_New函數(shù),傳入的參數(shù)size=4,maxchar=128013


查看上圖的執(zhí)行軌跡待榔,顯然計算分配內(nèi)存的尺寸

  • struct_size表示PyCompactUnicode的頭部尺寸逞壁,本示例是74字節(jié)
  • (size+1)*char_size表示有效負載,本示例20字節(jié)

下圖是PyUnicode_New函數(shù)下半部分代碼的執(zhí)行軌跡锐锣。這里值得一提的是第二個紅框的代碼塊

  • unicode+1這個表達式從PyCompactUnicodeObject的首個字節(jié)指向腌闯,有效負載的首個字節(jié),PyCompactUnicodeObject頭部和有效負載的地址邊界雕憔。
  • PyUnicode_LENGTH宏用于修改PyCompactUnicode對象的length字段
  • PyUnicode_HASH宏用于修改PyCompactUnicode對象的hash字段
  • PyUnicode_STATE宏用于獲取PyCompactUnicode對象的state字段,state是PyASCIIObject的內(nèi)部類姿骏,并且修改其內(nèi)部類的屬性。

當PyUnicode_New函數(shù)返回_PyUnicodeWriter_PrepareInternal函數(shù)時斤彼,堆內(nèi)存中已經(jīng)存在兩個字符串對象分瘦,一個是PyASCIIObject實例,一個是PyCompactUnicodeObject實例琉苇。注意這個PyCompactUnicodeObject有些怪異嘲玫,你發(fā)現(xiàn)了嗎?

就是其kind字段為4并扇,表示該PyCompactUnicodeObject會進一步衍生為PyUnicodeObject對象去团。在以上內(nèi)存圖可知由于writer->pos=0,那么_PyUnicode_FastCopyCharacters函數(shù)內(nèi)部調(diào)用_copy_character函數(shù)什么事情都沒做會馬上返回_PyUnicodeWriter_PrepareInternal函數(shù)。

這里我們將注意力放到Py_SETREF這個宏函數(shù)穷蛹,它將PyCompactUnicodeObject實例的內(nèi)存地址綁定到writer->buffer并且將舊的PyASCIIObject執(zhí)行內(nèi)存釋放

那么_PyUnicodeWriter對象它托管了新的PyCompactUnicodeObject實例土陪,writer的字段kind和PyCompactUnicodeObject對象的kind字段信息顯然是不對稱的嘛~

那么_PyUnicodeWriter_PrepareInternal函數(shù)會繼續(xù)調(diào)用PyUnicodeWriter_Update函數(shù)刷新_PyUnicodeWriter對象。

_PyUnicodeWriter_Prepare調(diào)用_PyUnicodeWriter_PrepareInternal函數(shù)已經(jīng)返回0肴熏,那么理所當然就調(diào)用PyUnicode_WRITE這個宏函數(shù)

當執(zhí)行完_PyUnicodeWriter_WriteCharInline函數(shù)后鬼雀,返回到unicode_decode_utf8函數(shù)后,PyCompactUnicodeObject的內(nèi)存圖如下圖所示蛙吏。

這個內(nèi)存圖還是有些詭異是吧~的確源哩,因為unicode_decode_utf8函數(shù)值到目前為止,仍然是以2字節(jié)位寬的模式來構(gòu)建一個PyCompactUnicodeObject內(nèi)存實體鸦做,而從PyUnicodeWriter對象的字段信息和PyCompactUnicodeObject的state字段看來信息是不對稱的璧疗。還有當前

所以下一步會調(diào)用_PyUnicodeWriter_Finish函數(shù)做進一步處理。

unicode_decode_utf8函數(shù)退出內(nèi)置while循環(huán)馁龟,安裝上下文的代碼順序執(zhí)行End分支區(qū)塊內(nèi)的代碼,我們這里主要關(guān)注_PyUnicodeWriter_Finish函數(shù)

當字符串對象和的length字段和_PyUnicodeWriter對象的pos字段不一致時漆魔,_PyUnicodeWriter_Finish函數(shù)主要調(diào)用函數(shù)resize_compact函數(shù)對對應的字符串對象嘗試內(nèi)存重分配坷檩。

在resize_compact函數(shù)中上下文

  • 參數(shù)unicode是對PyCompactUnicodeObject對象的引用
  • 參數(shù)length持有對_PyUnicodeWriter_Finish函數(shù)傳遞的writer->pos=1

從代碼的執(zhí)行軌跡來看却音,本示例resize_compact函數(shù)調(diào)用PyObject_REALLOC函數(shù),

我們重點理解一下重新計算分配內(nèi)存的細節(jié)矢炼。struct_size是PyCompactUnicodeObject的頭部尺寸72字節(jié)系瓢,而后面是(length+1)*char_size是什么東東呢?也就是請好好回憶一下PyUnicodeObject的結(jié)構(gòu)體定義

typedef struct {
    PyCompactUnicodeObject _base; //72字節(jié)
    union {
        void *any;
        Py_UCS1 *latin1;
        Py_UCS2 *ucs2;
        Py_UCS4 *ucs4;
    } data;                     /* Canonical, smallest-form Unicode buffer */
} PyUnicodeObject;

Ok,PyUnicodeObject的頭部實際上是PyCompactUnicodeObject的頭部+聯(lián)合體data句灌,我們知道length=1夷陋,(length+1)*char_size=2×4=8字節(jié)。也就是聯(lián)合體data僅在本示例是8個字胰锌∑疲可能有人會問“不是聯(lián)合體內(nèi)所有字段的尺寸在對齊后的總內(nèi)存嗎?”如果你這種錯誤的理解资昧,表示你對union這個數(shù)據(jù)類型的內(nèi)存模型不了解酬土。

事實上,由于聯(lián)合體內(nèi)有void*指針的存在,由于void*指針表明它可以指向任意字寬的字節(jié)序列格带,在CPython的PyUnicodeObject實例化過程中撤缴,union的內(nèi)存尺寸取決于聯(lián)合體內(nèi)部成員中最大類型尺寸的某一個成員。也就是當多個數(shù)據(jù)成員每次只能取其一因此聯(lián)合體的每一項元素起始地址都一樣叽唱,都跟聯(lián)合體 union 的地址偏移量為0屈呕;

那可能有人挑刺了:“你憑什么說該該內(nèi)存分配就一定是PyUnicodeObject嗎?通篇代碼都沒有顯式聲明PyUnicodeObject*類型的內(nèi)存分配代碼肮淄ぁ虎眨!”,拜托!對此類無知的問題我是不屑一顧的侦铜,反問一下自己比PyCompactUnicode的類型尺寸還大的類型专甩,在CPython3.3+實現(xiàn)中,除了PyUnicodeObject之外钉稍,還有額外的字符串類型嗎5佣恪!

從聯(lián)合體data的void指針成員贡未,也看得出即便將來有比4字節(jié)編碼更大的編碼類型出現(xiàn)种樱,PyUnicodeObject對象可以兼容任意字寬的編碼類型的字節(jié)序列,因為有void*指針配合kind字段就能解碼任意字節(jié)序列俊卤,當然這是理論上的嫩挤。

性能問題

到目前為止,我介紹了PyASCIIObject消恍、PyCompactUnicodeObject岂昭、PyUnicodeObject的初始化過程。已經(jīng)知道

  • 單個ASCII字節(jié)會優(yōu)先緩存在unicode_latin1全局靜態(tài)字符數(shù)組中狠怨。
  • PyASCIIObject初始化最多就涉及一次malloc函數(shù)的調(diào)用约啊。
  • PyCompactUnicodeObject的初始化涉及2次malloc函數(shù)調(diào)用邑遏。
  • PyUnicodeObject的初始化涉及3次malloc函數(shù)調(diào)用

然而這一切CPython都無法事先預知的,而是在遍歷C級別字節(jié)序列對帶有明顯特征的字節(jié)進行檢測時才確定哪一種適合當前傳入的C級別字節(jié)序列的初始化方案恰矩。CPython在字符串初始化的起始階段采取的邏輯是先一刀切地假定是PyASCIIObject方案记盒,若檢測字節(jié)特征不符合PyASCIIObject初始化方案,再選擇PyCompactUnicodeObject初始化方案(第2次調(diào)用malloc),若后續(xù)遍歷字節(jié)序列外傅,檢測到不符合2字節(jié)位寬的字節(jié)特征碼纪吮,CPython會最后選擇PyUnicodeObject的初始化方案(第3次調(diào)用malloc)。

CPython之所以這樣做的目地是為了最大限度地節(jié)省內(nèi)存萎胰。但犧牲的是時間效率碾盟,對于CPython的內(nèi)部而言,即便初始化一個4字節(jié)位寬的字符串也要經(jīng)歷兩個嵌套在一起while內(nèi)外循環(huán)為主體函數(shù)調(diào)用奥洼,它們一般情況下是O(n)巷疼,對于復雜的字節(jié)序列包含拉丁字符,中文字或一些unicode編碼靠后的字符的字符序列灵奖,那么這是最壞的情況是O(n^2)嚼沿。因此對于密集性的字符串i/o必然需要大量字符串初始化的操作。因此你不要告訴我還有字符串駐留這一特性瓷患,現(xiàn)實中這特性是于事無補的骡尽。原生的Python代碼寫的字符串I/O處理代碼不論在內(nèi)存開銷還是時間開銷都不是字符串密集I/O應用場景的最佳選擇。

曾幾何時我跟某些Python程序員辯論到這一問題,有人就反駁我:“既然你都說的CPython那么不堪了擅编,拉倒吧~還用它干什么呢攀细!”,首先我們要辯證地正視問題爱态,那解決方案有嗎谭贪?Sure,it is Cython,Cython下的語境是C級別下的字符串,當然你也可以調(diào)用內(nèi)置C++標準庫的string容器锦担。Cython下的字符串初始化的這些類似操作能夠直接在C底層完成俭识,我們稱為Python的后端。相反地洞渔,Python解釋器的內(nèi)部就稱為前端套媚。如果Python解析器需要讀取Cython處理的字符串就需要經(jīng)歷類似CPython內(nèi)部的初始化邏輯,性能就急劇下降磁椒。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末堤瘤,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子浆熔,更是在濱河造成了極大的恐慌本辐,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異慎皱,居然都是意外死亡环葵,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門宝冕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人邓萨,你說我怎么就攤上這事地梨。” “怎么了缔恳?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵宝剖,是天一觀的道長。 經(jīng)常有香客問我歉甚,道長万细,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任纸泄,我火速辦了婚禮赖钞,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘聘裁。我一直安慰自己雪营,他們只是感情好,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布衡便。 她就那樣靜靜地躺著献起,像睡著了一般。 火紅的嫁衣襯著肌膚如雪镣陕。 梳的紋絲不亂的頭發(fā)上谴餐,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天,我揣著相機與錄音呆抑,去河邊找鬼岂嗓。 笑死,一個胖子當著我的面吹牛理肺,可吹牛的內(nèi)容都是我干的摄闸。 我是一名探鬼主播,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼妹萨,長吁一口氣:“原來是場噩夢啊……” “哼年枕!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起乎完,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤熏兄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體摩桶,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡桥状,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了硝清。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片辅斟。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖芦拿,靈堂內(nèi)的尸體忽然破棺而出士飒,到底是詐尸還是另有隱情,我是刑警寧澤蔗崎,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布酵幕,位于F島的核電站,受9級特大地震影響缓苛,放射性物質(zhì)發(fā)生泄漏芳撒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一未桥、第九天 我趴在偏房一處隱蔽的房頂上張望笔刹。 院中可真熱鬧,春花似錦钢属、人聲如沸徘熔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽酷师。三九已至,卻和暖如春染乌,著一層夾襖步出監(jiān)牢的瞬間山孔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工荷憋, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留台颠,地道東北人。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓勒庄,卻偏偏與公主長得像串前,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子实蔽,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359