您好,欢迎访问一九零五行业门户网

什么是实现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_tbytes_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 linuxtype "help", "copyright", "credits" or "license" for more information.>>> b"abc" + b"edf"in concat function: abc <> edfb'abcedf'>>>
在上面的拼接函数当中会拷贝原来的两个字节对象,因此需要谨慎使用,一旦发生非常多的拷贝的话是非常耗费内存的。因此需要警惕使用循环内的内存拼接。比如对于 [b"a", b"b", b"c"] 来说,如果使用循环拼接的话,那么会将 b"a" 拷贝两次。
>>> res = b"">>> for item in [b"a", b"b", b"c"]:... res += item...>>> resb'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 == btrue>>> a is btrue>>> a = b"aa">>> b = b"aa">>> a == btrue>>> a is bfalse
从上面的代码可以知道,确实当我们创建的 bytes 的长度等于 1 的时候对象确实是同一个对象。
以上就是什么是实现python虚拟机字节的方法?的详细内容。
其它类似信息

推荐信息