我到Python虛擬機(jī)里逛了一圈,回來就被干掉了!
我出生在C盤一個(gè)很深的目錄下,也不知道是誰把我放到這里的。
我無事可干,整天就是睡覺,睡醒了就和我的鄰居Account.class聊天,他曾經(jīng)去過一次內(nèi)存的Java虛擬機(jī),不停地給我重復(fù)他的JVM奇遇記,什么陌生警察,什么虛擬機(jī)大樓,什么清理者,讓我聽得心癢癢的,也想來一次這樣的冒險(xiǎn)。
他告訴我:冒險(xiǎn)經(jīng)歷的開端是兩個(gè)警察,你就等著他們來吧。
1
陌生警察
這一天我正在睡覺,突然咣咣有人砸我房門。
我打開門一看,一高一矮兩個(gè)陌生警察!我的冒險(xiǎn)之旅要開場了。
“你們是ClassLoader吧?” 我想起了Account.class告訴我,會有個(gè)叫ClassLoader的警察來裝載。
“什么ClassLoader? 我們Python不玩Java那一套!” 兇神惡煞的矮個(gè)子警察遞上了工作證:“我是Python編譯器,現(xiàn)在奉命對你的住處進(jìn)行檢查,有沒有私藏pyc文件?”
“pyc? 什么pyc?” 我感覺情節(jié)發(fā)展和Account.class說得明顯不符。
“別裝了你!” 他四處查看,沒一會兒,在一個(gè)叫做_pycache_的角落里拉出來一個(gè)叫做user.pyc的家伙,“敢說你沒有私藏文件?”
我真是驚呆了,我確實(shí)是user.py,這個(gè)pyc是什么時(shí)候藏在這里的。
“讓我檢查檢查,” Python編譯器拿著放大鏡開始查看pyc這個(gè)家伙的二進(jìn)制數(shù)據(jù),“嗯,Magic Number是3394,是我們Python3.7編譯出來的,不過從修改時(shí)間戳看,實(shí)在是太老了。”
Python編譯器剛說完,抽出手槍,砰的一聲,就把這個(gè)pyc該干掉了, 他把頭轉(zhuǎn)向我:“現(xiàn)在,我對你重新編譯。”
可憐的pyc,連個(gè)臺詞都來不及說,就消失在空氣中了。
“有個(gè)叫order.py 的文件 import了你,現(xiàn)在我們奉命帶你去內(nèi)存編譯。” Python編譯器冷冰冰地說到。
我很驚奇:“我們Python不是解釋執(zhí)行嗎,怎么還要編譯?”
“真是無知,我們Python有虛擬機(jī),執(zhí)行的是字節(jié)碼,是先編譯,再解釋執(zhí)行!走,去內(nèi)存編譯。”
兩個(gè)警察不允許我?guī)魏螙|西,便把我推上車,我們一起奔向內(nèi)存。
2
打探消息
我覺得前途未卜,不會編譯完以后把我也干掉吧?不能坐以待斃,一定得多了解信息。
“警察大哥,你們是怎么找到我的?” 我小心地問那個(gè)高個(gè)警察。
高個(gè)兒警察還算和藹,揮了揮手中的一個(gè)本子:“我是Python解釋器,我們會根據(jù)本子上記錄的Python模塊搜索規(guī)則來查找,你看,先從程序運(yùn)行的當(dāng)前目錄找,然后從PYTHONPATH找,然后是python的安裝設(shè)置相關(guān)的默認(rèn)路徑。”
“瞧瞧,” 他指著本子說,“你就在C:\users\andy\temp\python\這個(gè)目錄下。”
我心說這和Java的ClassPath差不多。
“原來如此,那為什么把那個(gè)pyc給槍斃了?” 我心里緊張,下意識地看了一眼開車的Python編譯器。
“編譯一次挺花費(fèi)時(shí)間的,所以就把字節(jié)碼緩存到了pyc文件中,如果你的源碼沒有變化,下次就不用編譯,直接執(zhí)行了。否則,那個(gè)pyc文件就沒用了。”
我長出一口氣,看來我的源碼有改動(dòng)!
“咱們怎么不用ClassLoader呢,我聽說Java都是這么干的。”
“說來話長,” 高個(gè)兒警察很有耐心,“他們Java最早的時(shí)候有個(gè)非常先進(jìn)的理念,代碼可以從網(wǎng)絡(luò)下載,在本地的JVM的執(zhí)行, 但是你怎么知道網(wǎng)上的那些代碼有沒有危害?所以就搞了一個(gè)沙箱機(jī)制,ClassLoader也分了層,Java的核心類(如java.lang.String)只能由最上層的ClassLoader來裝載,防止別有用心的人寫個(gè)同名的核心類搞破壞。”
我點(diǎn)頭:“奧,我們Python沒有這樣的需求,拿到源文件,編譯后解釋執(zhí)行,也就不需要復(fù)雜的Class Loader了。”
3
編譯
說話間,車子就開到了內(nèi)存。
Python編譯器下車,把我的代碼通通搬到內(nèi)存,然后是一系列讓人眼花繚亂的詞法分析,語言分析, 形成抽象語法樹,從抽象語法樹中形成字節(jié)碼,此處略去3000字不表。
終于,他在內(nèi)存中把我變成了二進(jìn)制的字節(jié)碼。
“這是什么鬼? ”
Python編譯器說:“這就是pyc啊,就是PyCodeObject,編譯一次累死人,我把這個(gè)PyCodeObject的對象保存到pyc文件中,下一次就不用編譯了。”
“我給你舉個(gè)例子,”高個(gè)的Python解釋器接口道,“在你的user.py中有這么一段代碼
def add(a,b):
c = a + b
print(c)
編譯成PyCodeObject以后大概是這個(gè)樣子:
(注:這里展示的只是一個(gè)片段,實(shí)際的PyCodeObject經(jīng)常是一個(gè)復(fù)雜的嵌套接結(jié)構(gòu))
局部常量表中記錄的是局部變量a,b,c 。
符號表中記錄了程序引用的符號,如print等。
字節(jié)碼就是真正的指令了,這些指令會引用常量表和符號表。”
只是展示一個(gè)片段就這么復(fù)雜了,我懶得去看這么多的細(xì)節(jié),心里想著按照Account.class的劇本,接下來就要去方法區(qū)了。
可是高個(gè)子的Python解釋器說:“我們這兒沒有方法區(qū),Python的對象和數(shù)據(jù)結(jié)構(gòu)都是保存在一個(gè)Heap中的,user.py,這是你的地址,你帶著PyCodeObject到那里去吧,一會兒就有線程聯(lián)系你了。”
4
執(zhí)行
去Heap區(qū)的路上,我看到一隊(duì)全副武裝的士兵不停地在巡邏,時(shí)不時(shí)把一些對象拉出來,塞到車?yán)?,不用說,這些都是可怕的清理者。
我仔細(xì)觀察了一下,每個(gè)對象的頭上都有一個(gè)引用計(jì)數(shù),如果被使用,計(jì)數(shù)就會增加,不用就會減少,如果變成零,對不起,那就危險(xiǎn)了。
按照地址找到了格子間,我倆剛坐下來,桌子上的視頻電話就響了。
畫面中,我看到一個(gè)編號為0x7954的線程坐在一個(gè)明亮的CPU車間里,他的面前是一個(gè)工作臺,工作臺上有一個(gè)深桶(后來知道這叫做棧)和一排小格子,還有一個(gè)引人注目的大鎖,上面寫著“GIL”。
這個(gè)線程對我說:“我是線程0x7954,我們的老板Python解釋器讓我調(diào)用你的add函數(shù),請把第一條指令給我說一下。”
我說:“c = a +b ”
“聽不懂,你得給我說字節(jié)碼。”
我恍然大悟,趕緊從PyCodeObject中的字節(jié)碼區(qū)域?qū)ふ遥?ldquo;LOAD_FAST 0 (a)”
0x7594從編號為0的格子中找到了數(shù)字10, 也就是add函數(shù)的參數(shù)a 的值,放入棧中
然后0x7594說:“下一條指令。”
“LOAD_FAST 1 (b)”
于是數(shù)字20被放入了棧中:
然后是:BINARY_ADD, 這應(yīng)該是個(gè)加法操作。
0x7954迅速地把10,20都取出來,做了加法,把結(jié)果30放入棧中。
最后是 :STORE_FAST 2 (c)
于是0x7954取出30,放到了編號為2的格子中
看到這里, 我就明白了Account.class曾經(jīng)說過JVM是個(gè)基于棧的虛擬機(jī), 看來Python VM也是如此啊。
不過既然都是虛擬機(jī),為什么這里執(zhí)行兩個(gè)整數(shù)的加法操作(BINARY_ADD)會這么慢呢?
電話那頭的0x7954似乎看透了我的心思:“我最煩這個(gè)BINARY_ADD指令了,Python是動(dòng)態(tài)類型語言,運(yùn)行期才知道具體類型,比如這段代碼
s1 = "hello"
s2 = "world"
s = s1 + s2
編譯后,底層的指令也是BINARY_ADD, 所以在執(zhí)行這個(gè)指令的時(shí)候,還需要做類型判斷,如果操作數(shù)是整數(shù),就相加;如果操作數(shù)是字符串,就做連接;如果一個(gè)是整數(shù),一個(gè)是字符串,還得做轉(zhuǎn)型,我容易嗎我!”
看來靜態(tài)類型也有好處,可以直接編譯成對應(yīng)的字節(jié)碼,整數(shù)相加就是iadd,字符串連接是其他字節(jié)碼,在運(yùn)行時(shí)就不用判斷參數(shù)類型了。
5
GIL
執(zhí)行的時(shí)間長了,我對這些字節(jié)碼熟得都能背下來了,這里實(shí)在是無聊。
0x7954執(zhí)行完一條STORE_FAST指令以后,居然停了下來,我心中大喜,Account.class告訴過我,一旦停下來,那就是程序員要調(diào)試了,他們的一秒是我們的十多天,將會有個(gè)漫長的假期。
但是沒有什么調(diào)試, 0x7954從工作臺上抱起GIL這個(gè)大鎖離開了CPU車間。
他對我說:“對不起,剛才Python解釋器說我已經(jīng)運(yùn)行了100個(gè)ticks,必須得放棄這個(gè)GIL的鎖,讓別的線程使用CPU車間了。”
我說:“不對啊,你這里有4個(gè)CPU車間(CPU core),你為什么不去別的車間執(zhí)行?”
“沒辦法,這是老大規(guī)定的,不管有多少個(gè)CPU車間,只有搶到GIL鎖的哪個(gè)線程才能運(yùn)行。”
“這么多線程在等待GIL,這么多CPU車間空著,一核有難,多核圍觀,浪費(fèi)啊,浪費(fèi)!” 我不由得痛心疾首。
不知道等了多久,0x7954又獲得了GIL鎖,進(jìn)入CPU車間執(zhí)行。
我注意到一個(gè)特點(diǎn),字節(jié)碼中對print函數(shù)的調(diào)用特別特別多。
程序員們怎么不調(diào)試呢?快樂假期怎么還不來呢?
0x7954說:“碼農(nóng)有兩類
1. 調(diào)試派,出了問題喜歡調(diào)試
2. 輸出派,不喜歡單步調(diào)試,喜歡通過print來輸出信息
3. 思考派,出了問題先在腦子中分析定位,然后再調(diào)試。
我看咱們這位Python程序員屬于第二種。”
這個(gè)程序員“去年”還調(diào)試Java呢,怎么到了Python這里就變成輸出派了?我很疑惑。
6
尾聲
代碼終于執(zhí)行完了,整個(gè)世界都消失了,我又回到了硬盤,正如Account.class所說,像做了一場夢一樣。
user.pyc熱情地給我打招呼:“大哥回來了,你可千萬別再改動(dòng)了,你一改動(dòng)我就完蛋。”
我說:“我也不想改,一改我也活不成, 但是我也控制不了程序員啊......”
話還沒說完,就感覺頭上遭遇了一記暴擊,我知道程序員動(dòng)了我的源碼,也許是修改了一個(gè)Bug,我知道自己要被新版本覆蓋了。
user.pyc喃喃自語:“完了,這么快就改了.....”
這時(shí)候門外又響起了敲門聲......
【本文為51CTO專欄作者“劉欣”的原創(chuàng)稿件,轉(zhuǎn)載請通過作者微信公眾號coderising獲取授權(quán)】