您好,登錄后才能下訂單哦!
這篇文章主要介紹“Python虛擬機中字節的實現原理是什么”的相關知識,小編通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強,希望這篇“Python虛擬機中字節的實現原理是什么”文章能幫助大家解決問題。
typedef struct { PyObject_VAR_HEAD Py_hash_t ob_shash; char ob_sval[1]; /* Invariants: * ob_sval contains space for 'ob_size+1' elements. * ob_sval[ob_size] == 0. * ob_shash is the hash of the string or -1 if not computed yet. */ } PyBytesObject; typedef struct { PyObject ob_base; Py_ssize_t ob_size; /* Number of items in variable part */ } PyVarObject; typedef struct _object { Py_ssize_t ob_refcnt; struct _typeobject *ob_type; } PyObject;
上面的數據結構用圖示如下所示:
現在我們來解釋一下上面的數據結構各個字段的含義:
ob_refcnt,這個還是對象的引用計數的個數,主要是在垃圾回收的時候有用。
ob_type,這個是對象的數據類型。
ob_size,表示這個對象當中字節的個數。
ob_shash,對象的哈希值,如果還沒有計算,哈希值為 -1 。
ob_sval,一個數據存儲一個字節的數據,需要注意的是 ob_sval[size] 一定等于 '\0' ,表示字符串的結尾。
可能你會有疑問上面的結構體當中并沒有后面的那么多字節啊,數組只有一個字節的數據啊,這是因為在 cpython 的實現當中除了申請 PyBytesObject 大的小內存空間之外,還會在這個基礎之上申請連續的額外的內存空間用于保存數據,在后續的源碼分析當中可以看到這一點。
下面我們舉幾個例子來說明一下上面的布局:
上面是空和字符串 abc 的字節表示。
下面是在 cpython 當中通過字節數創建 PyBytesObject 對象的函數。下面的函數的主要功能是創建一個能夠存儲 size 個字節大小的數據的 PyBytesObject 對象,下面的函數最重要的一個步驟就是申請內存空間。
static PyObject * _PyBytes_FromSize(Py_ssize_t size, int use_calloc) { PyBytesObject *op; assert(size >= 0); if (size == 0 && (op = nullstring) != NULL) { #ifdef COUNT_ALLOCS null_strings++; #endif Py_INCREF(op); return (PyObject *)op; } if ((size_t)size > (size_t)PY_SSIZE_T_MAX - PyBytesObject_SIZE) { PyErr_SetString(PyExc_OverflowError, "byte string is too large"); return NULL; } /* Inline PyObject_NewVar */ // PyBytesObject_SIZE + size 就是實際申請的內存空間的大小 PyBytesObject_SIZE 就是表示 PyBytesObject 各個字段占用的實際的內存空間大小 if (use_calloc) op = (PyBytesObject *)PyObject_Calloc(1, PyBytesObject_SIZE + size); else op = (PyBytesObject *)PyObject_Malloc(PyBytesObject_SIZE + size); if (op == NULL) return PyErr_NoMemory(); // 將對象的 ob_size 字段賦值成 size (void)PyObject_INIT_VAR(op, &PyBytes_Type, size); // 由于對象的哈希值還沒有進行計算 因此現將哈希值賦值成 -1 op->ob_shash = -1; if (!use_calloc) op->ob_sval[size] = '\0'; /* empty byte string singleton */ if (size == 0) { nullstring = op; Py_INCREF(op); } return (PyObject *) op; }
我們可以使用一個寫例子來看一下實際的 PyBytesObject 內存空間的大小。
>>> import sys >>> a = b"hello world" >>> sys.getsizeof(a) 44 >>>
上面的 44 = 32 + 11 + 1 。
其中 32 是 PyBytesObject 4 個字段所占用的內存空間,ob_refcnt、ob_type、ob_size和 ob_shash 各占 8 個字節。11 是表示字符串 "hello world" 占用 11 個字節,最后一個字節是 '\0' 。
這個函數主要是返回 PyBytesObject 對象的字節長度,也就是直接返回 ob_size 的值。
static Py_ssize_t bytes_length(PyBytesObject *a) { // (((PyVarObject*)(ob))->ob_size) return Py_SIZE(a); }
在 python 當中執行下面的代碼就會執行字節拼接函數:
>>> b"abc" + b"edf"
下方就是具體的執行字節拼接的函數:
/* This is also used by PyBytes_Concat() */ static PyObject * bytes_concat(PyObject *a, PyObject *b) { Py_buffer va, vb; PyObject *result = NULL; va.len = -1; vb.len = -1; // Py_buffer 當中有一個指針字段 buf 可以用戶保存 PyBytesObject 當中字節數據的首地址 // PyObject_GetBuffer 函數的主要作用是將 對象 a 當中的字節數組賦值給 va 當中的 buf if (PyObject_GetBuffer(a, &va, PyBUF_SIMPLE) != 0 || PyObject_GetBuffer(b, &vb, PyBUF_SIMPLE) != 0) { PyErr_Format(PyExc_TypeError, "can't concat %.100s to %.100s", Py_TYPE(b)->tp_name, Py_TYPE(a)->tp_name); goto done; } /* Optimize end cases */ if (va.len == 0 && PyBytes_CheckExact(b)) { result = b; Py_INCREF(result); goto done; } if (vb.len == 0 && PyBytes_CheckExact(a)) { result = a; Py_INCREF(result); goto done; } if (va.len > PY_SSIZE_T_MAX - vb.len) { PyErr_NoMemory(); goto done; } result = PyBytes_FromStringAndSize(NULL, va.len + vb.len); // 下方就是將對象 a b 當中的字節數據拷貝到新的 if (result != NULL) { // PyBytes_AS_STRING 宏定義在下方當中 主要就是使用 PyBytesObject 對象當中的 // ob_sval 字段 也就是將 buf 數據(也就是 a 或者 b 當中的字節數據)拷貝到 ob_sval當中 memcpy(PyBytes_AS_STRING(result), va.buf, va.len); memcpy(PyBytes_AS_STRING(result) + va.len, vb.buf, vb.len); } done: if (va.len != -1) PyBuffer_Release(&va); if (vb.len != -1) PyBuffer_Release(&vb); return result; }
#define PyBytes_AS_STRING(op) (assert(PyBytes_Check(op)), \ (((PyBytesObject *)(op))->ob_sval))
我們修改一個這個函數,在其中加入一條打印語句,然后重新編譯 python 執行結果如下所示:
Python 3.9.0b1 (default, Mar 23 2023, 08:35:33) [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)] on linux Type "help", "copyright", "credits" or "license" for more information. >>> b"abc" + b"edf" In concat function: abc <> edf b'abcedf' >>>
在上面的拼接函數當中會拷貝原來的兩個字節對象,因此需要謹慎使用,一旦發生非常多的拷貝的話是非常耗費內存的。因此需要警惕使用循環內的內存拼接。比如對于 [b"a", b"b", b"c"] 來說,如果使用循環拼接的話,那么會將 b"a" 拷貝兩次。
>>> res = b"" >>> for item in [b"a", b"b", b"c"]: ... res += item ... >>> res b'abc' >>>
因為 b"a", b"b" 在拼接的時候會將他們分別拷貝一次,在進行 b"ab",b"c" 拼接的時候又會將 ab 和 c 拷貝一次,那么具體的拷貝情況如下所示:
"a" 拷貝了一次。
"b" 拷貝了一次。
"ab" 拷貝了一次。
"c" 拷貝了一次。
但是實際上我們的需求是只需要對 [b"a", b"b", b"c"] 當中的數據各拷貝一次,如果我們要實現這一點可以使用 b"".join([b"a", b"b", b"c"]),直接將 [b"a", b"b", b"c"] 作為參數傳遞,然后各自只拷貝一次,具體的實現代碼如下所示,在這個例子當中 sep 就是空串 b"",iterable 就是 [b"a", b"b", b"c"] 。
Py_LOCAL_INLINE(PyObject *) STRINGLIB(bytes_join)(PyObject *sep, PyObject *iterable) { char *sepstr = STRINGLIB_STR(sep); const Py_ssize_t seplen = STRINGLIB_LEN(sep); PyObject *res = NULL; char *p; Py_ssize_t seqlen = 0; Py_ssize_t sz = 0; Py_ssize_t i, nbufs; PyObject *seq, *item; Py_buffer *buffers = NULL; #define NB_STATIC_BUFFERS 10 Py_buffer static_buffers[NB_STATIC_BUFFERS]; seq = PySequence_Fast(iterable, "can only join an iterable"); if (seq == NULL) { return NULL; } seqlen = PySequence_Fast_GET_SIZE(seq); if (seqlen == 0) { Py_DECREF(seq); return STRINGLIB_NEW(NULL, 0); } #ifndef STRINGLIB_MUTABLE if (seqlen == 1) { item = PySequence_Fast_GET_ITEM(seq, 0); if (STRINGLIB_CHECK_EXACT(item)) { Py_INCREF(item); Py_DECREF(seq); return item; } } #endif if (seqlen > NB_STATIC_BUFFERS) { buffers = PyMem_NEW(Py_buffer, seqlen); if (buffers == NULL) { Py_DECREF(seq); PyErr_NoMemory(); return NULL; } } else { buffers = static_buffers; } /* Here is the general case. Do a pre-pass to figure out the total * amount of space we'll need (sz), and see whether all arguments are * bytes-like. */ for (i = 0, nbufs = 0; i < seqlen; i++) { Py_ssize_t itemlen; item = PySequence_Fast_GET_ITEM(seq, i); if (PyBytes_CheckExact(item)) { /* Fast path. */ Py_INCREF(item); buffers[i].obj = item; buffers[i].buf = PyBytes_AS_STRING(item); buffers[i].len = PyBytes_GET_SIZE(item); } else if (PyObject_GetBuffer(item, &buffers[i], PyBUF_SIMPLE) != 0) { PyErr_Format(PyExc_TypeError, "sequence item %zd: expected a bytes-like object, " "%.80s found", i, Py_TYPE(item)->tp_name); goto error; } nbufs = i + 1; /* for error cleanup */ itemlen = buffers[i].len; if (itemlen > PY_SSIZE_T_MAX - sz) { PyErr_SetString(PyExc_OverflowError, "join() result is too long"); goto error; } sz += itemlen; if (i != 0) { if (seplen > PY_SSIZE_T_MAX - sz) { PyErr_SetString(PyExc_OverflowError, "join() result is too long"); goto error; } sz += seplen; } if (seqlen != PySequence_Fast_GET_SIZE(seq)) { PyErr_SetString(PyExc_RuntimeError, "sequence changed size during iteration"); goto error; } } /* Allocate result space. */ res = STRINGLIB_NEW(NULL, sz); if (res == NULL) goto error; /* Catenate everything. */ p = STRINGLIB_STR(res); if (!seplen) { /* fast path */ for (i = 0; i < nbufs; i++) { Py_ssize_t n = buffers[i].len; char *q = buffers[i].buf; Py_MEMCPY(p, q, n); p += n; } goto done; } // 具體的實現邏輯就是在這里 for (i = 0; i < nbufs; i++) { Py_ssize_t n; char *q; if (i) { // 首先現將 sepstr 拷貝到新的數組里面但是在我們舉的例子當中是空串 b"" Py_MEMCPY(p, sepstr, seplen); p += seplen; } n = buffers[i].len; q = buffers[i].buf; // 然后將列表當中第 i 個 bytes 的數據拷貝到 p 當中 這樣就是實現了我們所需要的效果 Py_MEMCPY(p, q, n); p += n; } goto done; error: res = NULL; done: Py_DECREF(seq); for (i = 0; i < nbufs; i++) PyBuffer_Release(&buffers[i]); if (buffers != static_buffers) PyMem_FREE(buffers); return res; }
在 cpython 的內部實現當中給單字節的字符做了一個小的緩沖池:
static PyBytesObject *characters[UCHAR_MAX + 1]; // UCHAR_MAX 在 64 位系統當中等于 255
當創建的 bytes 只有一個字符的時候就可以檢查是否 characters 當中已經存在了,如果存在就直接返回這個已經創建好的 PyBytesObject 對象,否則再進行創建。新創建的 PyBytesObject 對象如果長度等于 1 的話也會被加入到這個數組當中。下面是 PyBytesObject 的另外一個創建函數:
PyObject * PyBytes_FromStringAndSize(const char *str, Py_ssize_t size) { PyBytesObject *op; if (size < 0) { PyErr_SetString(PyExc_SystemError, "Negative size passed to PyBytes_FromStringAndSize"); return NULL; } // 如果創建長度等于 1 而且對象在 characters 當中存在的話那么就直接返回 if (size == 1 && str != NULL && (op = characters[*str & UCHAR_MAX]) != NULL) { #ifdef COUNT_ALLOCS one_strings++; #endif Py_INCREF(op); return (PyObject *)op; } op = (PyBytesObject *)_PyBytes_FromSize(size, 0); if (op == NULL) return NULL; if (str == NULL) return (PyObject *) op; Py_MEMCPY(op->ob_sval, str, size); /* share short strings */ // 如果創建的對象的長度等于 1 那么久將這個對象保存到 characters 當中 if (size == 1) { characters[*str & UCHAR_MAX] = op; Py_INCREF(op); } return (PyObject *) op; }
我們可以使用下面的代碼進行驗證:
>>> a = b"a" >>> b =b"a" >>> a == b True >>> a is b True >>> a = b"aa" >>> b = b"aa" >>> a == b True >>> a is b False
從上面的代碼可以知道,確實當我們創建的 bytes 的長度等于 1 的時候對象確實是同一個對象。
關于“Python虛擬機中字節的實現原理是什么”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識,可以關注億速云行業資訊頻道,小編每天都會為大家更新不同的知識點。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。