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