為什么說(shuō)python很慢

我們之前一定聽有人說(shuō)過(guò),python的執(zhí)行速度比其他語(yǔ)言慢。

我們通常的解釋是:python是一個(gè)動(dòng)態(tài)的解釋型語(yǔ)言;python中的值不是存儲(chǔ)在緩存區(qū)而是分散的存儲(chǔ)在對(duì)象中。通過(guò)使用Numpy和Scipy等相關(guān)可以進(jìn)行矢量化操作的工具并調(diào)用編譯后的代碼來(lái)繞過(guò)這個(gè)問題來(lái)避開這個(gè)問題。

然而,“動(dòng)態(tài)類型 - 解釋 - 緩沖 - 矢量化編譯”這些詞的解釋太過(guò)籠統(tǒng)。這些解釋不能解釋python運(yùn)行過(guò)慢的底層性的深層次原因。最佳無(wú)意間在網(wǎng)上看到這樣一邊帖子《Why Python is Slow: Looking Under the Hood》便翻譯一下,貼出來(lái)供大家參考。

為什么python比較慢?python比Fortran和C語(yǔ)言慢的原因

1.1. python是動(dòng)態(tài)性語(yǔ)言不是靜態(tài)性語(yǔ)言

這是說(shuō)在python程序執(zhí)行的時(shí)候,編譯器不知道變量的類型。圖1.展示了C語(yǔ)言中的變量與python中變量的區(qū)別。在C中編譯器知道變量在定義時(shí)的類型,而python中執(zhí)行的時(shí)候只知道它是一個(gè)對(duì)象。

圖1

因此,如果您在C中編寫以下內(nèi)容:

/ * C代碼* /

int? a? =? 1 ;

int? b? =? 2 ;

int? c? =? a? +? b ;

C編譯器從一開始就知道a并且b是整數(shù):它們根本不可能是其他任何東西!有了這些知識(shí),它可以調(diào)用添加兩個(gè)整數(shù)的例程,返回另一個(gè)整數(shù),它在內(nèi)存中只是一個(gè)簡(jiǎn)單的值。在C中執(zhí)行的流程大概如下:

###C加法:

1. 分配<int>1給a;

2.?分配<int>2給b;

3. 調(diào)用二進(jìn)制加法binary_add(a, b)<int,int>(a, b);

4. 將結(jié)構(gòu)分配給c變量

python中等效的代碼如下:

# python code

a = 1

b = 2

c = a + b

這里解釋器只知道1和2是對(duì)象,但不知道它們是什么類型的對(duì)象。 因此解釋器必須檢查每個(gè)變量的PyObject_HEAD以找到類型信息,然后為這兩種類型調(diào)用適當(dāng)?shù)那蠛屠獭?最后,它必須創(chuàng)建并初始化一個(gè)新的Python對(duì)象來(lái)保存返回值。 執(zhí)行流程大致如下:

###Python 加法

1.?分配1給a

? ? ? ? (1)設(shè)置a->PyObject_HEAD->typecode為整數(shù)

? ? ? ? (2)設(shè)置Set?a->val = 1

2.?分配2給b

? ? ? ? (1)設(shè)置?b->PyObject_HEAD->typecode?為整數(shù)

? ? ? ? (2)設(shè)置?b->val = 2

3. 調(diào)用二進(jìn)制加法binary_add(a, b)

? ? ? ? (1)找到類型代碼 a->PyObject_HEAD

? ? ? ? (2)a是整數(shù),值為a->val

? ? ? ? (3)找到類型代碼 b->PyObject_HEAD

? ? ? ? (4)b是整數(shù),值為b->val

? ? ? ? (5)調(diào)用二進(jìn)制加法?binary_add(a->val, b->val)

? ? ? ? (6)結(jié)果是result,是一個(gè)整數(shù)。

4. 創(chuàng)建一個(gè)新的對(duì)象c

? ? ? ? (1)設(shè)置?c->PyObject_HEAD->typecode?為整數(shù)

? ? ? ? (2)將?c->val?分配給結(jié)果

動(dòng)態(tài)類型意味著任何操作都需要更多的步驟。這是Python在數(shù)值數(shù)據(jù)操作方面比C慢的主要原因。

1.2. python是解釋性語(yǔ)言而不是編譯性語(yǔ)言

解釋型語(yǔ)言與編譯型語(yǔ)言它們本身的區(qū)別也會(huì)造成程序在執(zhí)行的時(shí)候的速度差異。一個(gè)智能化的編譯器可以預(yù)測(cè)并針對(duì)重復(fù)和不需要的操作進(jìn)行優(yōu)化。這也會(huì)提升程序執(zhí)行的速度。

1.3. python的對(duì)象模型會(huì)導(dǎo)致訪問內(nèi)存效率低下

在上面的例子中,相對(duì)于C語(yǔ)言,在python中對(duì)整數(shù)進(jìn)行操作會(huì)有一個(gè)額外的類型信息層。當(dāng)有很多的整數(shù)并且希望進(jìn)行某種批操作時(shí),在python中往往會(huì)使用一個(gè)list,而在C中會(huì)使用某個(gè)基于緩存區(qū)的數(shù)組。

在Numpy數(shù)組的最簡(jiǎn)單的形式是一個(gè)圍繞著C中的數(shù)組建的一個(gè)python對(duì)象。也就是說(shuō)Numpy有一個(gè)指針指向連續(xù)緩存區(qū)數(shù)據(jù)的值,而在python中,python列表有一個(gè)只想緩存區(qū)的指針,每個(gè)指針都指向一個(gè)python緩存對(duì)象,而且每個(gè)對(duì)象都綁定一個(gè)數(shù)據(jù)(本例中是整數(shù))。這兩種情況的原理圖如圖2:

圖2

從圖2中可以很明顯的看出,當(dāng)對(duì)數(shù)據(jù)進(jìn)行操作時(shí)(例如排序、計(jì)算、查找等),無(wú)論是在存續(xù)成本還是訪問成本上,Numpy都比python更加的高效。

1.4. 為什么我們還要使用python

既然用pytho處理數(shù)據(jù)那么低效,那么為什么我們還要使用python呢?主要是因?yàn)?,python是動(dòng)態(tài)的語(yǔ)言,它比C更加的容易上手使用,而且用法更加的靈活和兼容,這可以極大的節(jié)省開發(fā)時(shí)間。而且,python是開源的,跨平臺(tái),具有很強(qiáng)的移植性。在那些真正需要運(yùn)用C或Fortran進(jìn)行優(yōu)化的場(chǎng)合中,python都有強(qiáng)大的API或庫(kù)進(jìn)行支持。這就是為什么Python在許多科學(xué)社區(qū)中的使用一直在不斷增長(zhǎng)。所以,Python最終成為使用代碼進(jìn)行科學(xué)研究的總體任務(wù)的極其有效的語(yǔ)言。

上面已經(jīng)談到了python的一些特有的內(nèi)部結(jié)構(gòu),但文章并不想止步于此,下面對(duì)python進(jìn)行一些黑客攻擊,這個(gè)過(guò)程很具有啟發(fā)性。

接下來(lái)的部分將使用黑客攻擊來(lái)暴露一些python對(duì)象來(lái)證明上述信息的正確性。請(qǐng)注意,以下所有內(nèi)容均使用Python 3.4編寫。早期的python版本python對(duì)象的內(nèi)部結(jié)構(gòu)不同,隨后的版本其內(nèi)部的對(duì)象結(jié)構(gòu)也會(huì)發(fā)生調(diào)整,所以注明python的版本很重要。請(qǐng)確保使用正確的版本!此外,下面的大多數(shù)代碼假定為64位CPU。如果您使用的是32位平臺(tái),則必須調(diào)整下面的某些C類型以解釋這種差異。

2.1. 深入研究python整型

Python中的整數(shù)易于創(chuàng)建和使用:

然而,這個(gè)界面的簡(jiǎn)單性卻隱藏了底層的復(fù)雜性,上面的敘述中簡(jiǎn)要討論了python整數(shù)在內(nèi)存中的布局。在這里,我們將使用Python的內(nèi)置ctypes模塊從Python解釋器本身檢查Python的整數(shù)類型。首先我們需要準(zhǔn)確的指導(dǎo)在C的API級(jí)別上python的整型是什么樣子的。

CPython中的實(shí)際x變量存儲(chǔ)在CPython源代碼(包含Include / longintrepr.h)中定義的結(jié)構(gòu)中。

struct _longobject {

? ? PyObject_VAR_HEAD

? ? digit ob_digit[1];

};

PyObject_VAR_HEAD是一個(gè)宏,它使用以下結(jié)構(gòu)啟動(dòng)對(duì)象,該結(jié)構(gòu)在Include / object.h中定義:

typedef struct {

? ? PyObject? ob_base ;

? ? Py_ssize_t? ob_size ;? / *變量部分中的項(xiàng)目數(shù)* /

}? PyVarObject ;

...并包含一個(gè)PyObject元素,該元素也在Include / object.h中定義:

typedef struct _object {

? ? _PyObject_HEAD_EXTRA

? ? Py_ssize_t ob_refcnt;

? ? struct _typeobject *ob_type;

} PyObject;

這里_PyObject_HEAD_EXTRA是一個(gè)宏,它通常不在Python構(gòu)建中使用。

把所有這些放在一起,typedef /?macros不進(jìn)行模糊處理,我們的整數(shù)對(duì)象就就可以解決如下結(jié)構(gòu):

struct _longobject {

? ? long ob_refcnt;

? ? PyTypeObject *ob_type;

? ? size_t ob_size;

? ? long ob_digit[1];

};

ob_refcnt變量是對(duì)象的引用計(jì)數(shù),ob_type變量是指向包含對(duì)象的所有類型信息和方法定義的結(jié)構(gòu)的指針,ob_digit保存實(shí)際的數(shù)值。

有了這些知識(shí),我們將使用ctypes模塊開始查看實(shí)際的對(duì)象結(jié)構(gòu),并提取上面的一些信息。

首先,我們?cè)贑中定義一個(gè)python。

現(xiàn)在讓我們看看一些數(shù)字的內(nèi)部表示,比如說(shuō)42。我們將使用在cPython中ID函數(shù)給出對(duì)象的內(nèi)存的位置:

ob_digit屬性指向內(nèi)存中的正確位置!

但是refcount怎么辦?只創(chuàng)建了一個(gè)值,為什么引入比這個(gè)值大得多的數(shù)?

事實(shí)上python中使用了很多的小整數(shù)。如果PyObject為這些整數(shù)中的每一個(gè)創(chuàng)建了一個(gè)新的,那將占用大量?jī)?nèi)存。因此,Python將公共整數(shù)值實(shí)現(xiàn)為單例:即,內(nèi)存中只存在這些數(shù)字的一個(gè)副本。換句話說(shuō),每次在此范圍內(nèi)創(chuàng)建新的Python整數(shù)時(shí),您只需使用該值創(chuàng)建對(duì)單例的引用:

這兩個(gè)變量都是指向同一內(nèi)存地址的指針。當(dāng)你得到更大的整數(shù)(在Python 3.4中大于255)時(shí),這不再是True:

只要啟動(dòng)Python解釋器,就會(huì)創(chuàng)建許多整數(shù)對(duì)象;看看有多少引用可能是很有趣的:

我們看到零被引用幾千次,并且正如您所預(yù)期的那樣,引用的頻率通常隨著整數(shù)值的增加而減小。為了進(jìn)一步確保這樣做符合我們的預(yù)期,讓我們確保該ob_digit字段保持正確的值:

如果你更深入一點(diǎn),你可能會(huì)注意到這不適用于大于256的數(shù)字:事實(shí)證明,一些位位移操作是在Objects / longobject.c中執(zhí)行的,它們改變了內(nèi)存中表示大整數(shù)的方式。我不能說(shuō)我完全理解為什么會(huì)發(fā)生這種情況,但我認(rèn)為這與Python有效處理超過(guò)long int數(shù)據(jù)類型溢出限制的整數(shù)的能力有關(guān),正如我們?cè)谶@里看到的:

這些數(shù)太長(zhǎng)了而不能變?yōu)閘ong整型,long整型只能儲(chǔ)存64位。

2.2?深入研究python列表

讓我們將上述想法應(yīng)用于更復(fù)雜的類型:Python列表。類似于整數(shù),我們?cè)?a target="_blank" rel="nofollow">Include / listobject.h中找到列表對(duì)象的定義:

typedef struct {

? ? PyObject_VAR_HEAD

? ? PyObject **ob_item;

? ? Py_ssize_t allocated;

} PyListObject;

同樣,我們可以擴(kuò)展宏并對(duì)類型進(jìn)行去混淆,以查看結(jié)構(gòu)實(shí)際上是以下內(nèi)容:

typedef struct {

? ? long ob_refcnt;

? ? PyTypeObject *ob_type;

? ? Py_ssize_t ob_size;

? ? PyObject **ob_item;

? ? long allocated;

} PyListObject;

這里PyObject **ob_item是指向列表內(nèi)容的對(duì)象,該ob_size值告訴我們列表中有多少項(xiàng)。

我們來(lái)試試吧:

為了確保我們已經(jīng)正確完成了任務(wù),讓我們創(chuàng)建一些額外的列表引用,并查看它如何影響引用計(jì)數(shù):

現(xiàn)在讓我們看看如何找到列表中的實(shí)際元素。如上所述,元素通過(guò)連續(xù)的PyObject指針數(shù)組存儲(chǔ)。使用CyType,我們實(shí)際上可以創(chuàng)建一個(gè)復(fù)合結(jié)構(gòu),它由以前的IntStruct對(duì)象組成:

現(xiàn)在讓我們來(lái)看看每個(gè)項(xiàng)目中的值:

我們已經(jīng)恢復(fù)了列表中的PyObject整數(shù)! 您可能希望花一點(diǎn)時(shí)間回顧上面列表內(nèi)存布局的原理圖,并確保您了解這些ctypes操作如何映射到該圖表。

2.3?深入研究Numpy數(shù)組

現(xiàn)在,為了比較,讓我們對(duì)numpy數(shù)組進(jìn)行相同的檢查。我將跳過(guò)NumPy C-API數(shù)組定義的詳細(xì)介紹;?如果你想看看它,你可以在中找到它https://github.com/numpy/numpy/blob/maintenance/1.8.x/numpy/core/include/numpy/ndarraytypes.h#L646請(qǐng)注意,我在這里使用Numpy 1.8版;?這些內(nèi)部可能在不同版本之間發(fā)生了變化。

讓我們從創(chuàng)建一個(gè)表示Numpy數(shù)組本身的結(jié)構(gòu)開始。這應(yīng)該開始看起來(lái)很熟悉…

我們還將添加一些定制屬性來(lái)訪問Python版本的shape和stride:

現(xiàn)在讓我們?cè)囈辉嚕?/p>

我們看到我們已經(jīng)提取出正確的shape信息。讓我們確保引用計(jì)數(shù)是正確的:

現(xiàn)在,我們可以完成提取數(shù)據(jù)緩沖區(qū)的復(fù)雜部分。為了簡(jiǎn)單起見,我們將忽略大步并假設(shè)它是一個(gè)C連續(xù)數(shù)組;這可以用一點(diǎn)工作來(lái)概括。

該data變量現(xiàn)在是Numpy數(shù)組中定義的連續(xù)內(nèi)存塊的視圖!為了表明這一點(diǎn),我們將更改數(shù)組中的值...

...并觀察數(shù)據(jù)視圖也會(huì)發(fā)生變化。兩者x并data都指向的存儲(chǔ)器中的相同的連續(xù)塊。

比較Python列表和Numpy ndarray的內(nèi)部結(jié)構(gòu),對(duì)于表示相同類型的數(shù)據(jù),很明顯Numpy的數(shù)組要比列表簡(jiǎn)單得多。



2.4 修改整數(shù)的值

受這篇Reddit帖子的啟發(fā),我們可以修改整數(shù)對(duì)象的數(shù)值!如果我們使用一個(gè)普通的數(shù)字,比如0或1,我們很可能會(huì)崩潰我們的Python內(nèi)核。 但是如果我們用不那么重要的數(shù)字來(lái)做,我們就可以僥幸逃脫,至少暫時(shí)的。

請(qǐng)注意,這是一個(gè)非常非常糟糕的主意。 特別是,如果你在IPython筆記本中運(yùn)行它,你可能會(huì)破壞IPython內(nèi)核的運(yùn)行能力(因?yàn)槟阍谶\(yùn)行時(shí)搞亂了變量)。 盡管如此,我們還是會(huì)用指出:

但是現(xiàn)在請(qǐng)注意,我們不能以簡(jiǎn)單的方式返回值,因?yàn)镻ython中不再存在真正的113的值!

恢復(fù)的一種方法是直接操作字節(jié)。所以在運(yùn)行Python 3.4的小端64位系統(tǒng)上,以下工作可以做:

2.5 列表內(nèi)容的就地修改

上面我們對(duì)Numpy數(shù)組中的值進(jìn)行了就地修改。這很容易,因?yàn)镹umpy數(shù)組只是一個(gè)數(shù)據(jù)緩沖區(qū)。但是我們可以為列表做同樣的事情嗎?這會(huì)變得有點(diǎn)棘手,因?yàn)榱斜泶鎯?chǔ)對(duì)值的引用而不是值本身。為了不讓Python本身崩潰,你需要非常小心地跟蹤這些引用計(jì)數(shù)。可以用一下方式完成:

就像我說(shuō)的,你永遠(yuǎn)都不應(yīng)該用這個(gè),老實(shí)說(shuō),我想不出任何你想用的理由。但它讓您了解了解釋器在修改列表內(nèi)容時(shí)必須執(zhí)行的操作類型。與上面的Numpy示例相比,您將看到為什么Python列表比Python數(shù)組開銷更大的一個(gè)原因。

2.6?Meta Goes Meta:自包裝Python對(duì)象

使用上述方法,我們可以開始變得更加陌生。所述Structure類ctypes本身是一個(gè)Python對(duì)象,其中可以看到模塊/ _ctypes / ctypes.h。正如我們包裝整數(shù)和列表一樣,我們可以按如下方式自行包裝結(jié)構(gòu):

現(xiàn)在我們將嘗試制作一個(gè)包裹自己的結(jié)構(gòu)。我們不能直接這樣做,因?yàn)槲覀儾恢纼?nèi)存中的哪個(gè)地址將創(chuàng)建新結(jié)構(gòu)。但我們可以做的是創(chuàng)建第二個(gè)包裝第一個(gè)結(jié)構(gòu),并使用它來(lái)就地修改其內(nèi)容!

我們首先制作一個(gè)臨時(shí)的元結(jié)構(gòu)并將其包裝起來(lái):

現(xiàn)在我們添加第三個(gè)結(jié)構(gòu),并使用它來(lái)就地調(diào)整第二個(gè)內(nèi)存值:

我們現(xiàn)在有一個(gè)自包裝的Python結(jié)構(gòu)!再說(shuō)一次,我想不出你有什么理由想要這樣做。并且請(qǐng)記住,在Python中有關(guān)于這種類型的自引用的開創(chuàng)性 - 由于它的動(dòng)態(tài)類型,在不直接破解內(nèi)存的情況下執(zhí)行這樣的操作是非常簡(jiǎn)單的:

3.?結(jié)論

Python很慢。正如我們所看到的那樣,其中一個(gè)重要原因就是引擎蓋下的類型間接,這使得開發(fā)人員可以快速,輕松,有趣地使用Python。正如我們所見,Python本身提供的工具可以用來(lái)攻擊Python對(duì)象本身。

我希望通過(guò)對(duì)各種物體之間的差異的探索以及CPython本身內(nèi)部的一些自由搗蛋來(lái)更清楚地說(shuō)明這一點(diǎn)。這個(gè)練習(xí)對(duì)我來(lái)說(shuō)非常有啟發(fā)性,我希望它也適合你...快樂的黑客攻擊!


這篇博客文章完全是在IPython Notebook中編寫的。完整的筆記本可以在查看下載?,或在這里靜態(tài) 查看

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容