详解字符串在Python内部是如何省内存的
起步
Python3起,str就采用了Unicode编码(注意这里并不是utf8编码,尽管.py文件默认编码是utf8)。每个标准Unicode字符占用4个字节。这对于内存来说,无疑是一种浪费。
Unicode是表示了一种字符集,而为了传输方便,衍生出里如utf8,utf16等编码方案来节省存储空间。Python内部存储字符串也采用了类似的形式。
三种内部表示Unicode字符串
为了减少内存的消耗,Python使用了三种不同单位长度来表示字符串:
- 每个字符1个字节(Latin-1)
- 每个字符2个字节(UCS-2)
- 每个字符4个字节(UCS-4)
源码中定义字符串结构体:
#Include/unicodeobject.h typedefuint32_tPy_UCS4; typedefuint16_tPy_UCS2; typedefuint8_tPy_UCS1; #Include/cpython/unicodeobject.h typedefstruct{ PyCompactUnicodeObject_base; union{ void*any; Py_UCS1*latin1; Py_UCS2*ucs2; Py_UCS4*ucs4; }data;/*Canonical,smallest-formUnicodebuffer*/ }PyUnicodeObject;
如果字符串中所有字符都在ascii码范围内,那么就可以用占用1个字节的Latin-1编码进行存储。而如果字符串中存在了需要占用两个字节(比如中文字符),那么整个字符串就将采用占用2个字节UCS-2编码进行存储。
这点可以通过sys.getsizeof函数外部窥探来验证这个结论:
如图,存储'zh'所需的存储空间比'z'多1个字节,h在这里占了1个字节;
存储'z中'所需的存储空间比'中'多了2个字节,z在这里占了2个字节。
大多数的自然语言采用2字节的编码就够了。但如果有一个1G的ascii文本加载到内存后,在文本中插入了一个emoji表情,那么字符串所需的空间将扩大到4倍,是不是很惊喜。
为什么内部不采用utf8进行编码
最受欢迎的Unicode编码方案,Python内部却不使用它,为什么?
这里就得说下utf8编码带来的缺点。这种编码方案每个字符的占用字节长度是变化的,这就导致了无法按所以随机访问单个字符,例如string[n](使用utf8编码)则需要先统计前n个字符占用的字节长度。所以由O(1)变成了O(n),这更无法让人接受。
因此Python内部采用了定长的方式存储字符串。
字符串驻留机制
另一个节省内存的方式就是将一些短小的字符串做成池,当程序要创建字符串对象前检查池中是否有满足的字符串。在内部中,仅包含下划线(_)、字母和数字的长度不高过20的字符串才能驻留。驻留是在代码编译期间进行的,代码中的如下会进行驻留检查:
- 空字符串''及所有;
- 变量名;
- 参数名;
- 字符串常量(代码中定义的所有字符串);
- 字典键;
- 属性名称;
驻留机制节省大量的重复字符串内存。在内部,字符串驻留池由一个全局的dict维护,该字段将字符串用作键:
voidPyUnicode_InternInPlace(PyObject**p) { PyObject*s=*p; PyObject*t; if(s==NULL||!PyUnicode_Check(s)) return; //对PyUnicodeObjec进行类型和状态检查 if(!PyUnicode_CheckExact(s)) return; if(PyUnicode_CHECK_INTERNED(s)) return; //创建intern机制的dict if(interned==NULL){ interned=PyDict_New(); if(interned==NULL){ PyErr_Clear();/*Don'tleaveanexception*/ return; } } //对象是否存在于inter中 t=PyDict_SetDefault(interned,s,s); //存在,调整引用计数 if(t!=s){ Py_INCREF(t); Py_SETREF(*p,t); return; } /*Thetworeferencesininternedarenotcountedbyrefcnt. Thedeallocatorwilltakecareofthis*/ Py_REFCNT(s)-=2; _PyUnicode_STATE(s).interned=SSTATE_INTERNED_MORTAL; }
变量interned就是全局存放字符串池的字典的变量名interned=PyDict_New(),为了让intern机制中的字符串不被回收,设置字典时PyDict_SetDefault(interned,s,s);将字符串作为键同时也作为值进行设置,这样对于字符串对象的引用计数就会进行两次+1操作,这样存于字典中的对象在程序结束前永远不会为0,这也是y_REFCNT(s)-=2;将计数减2的原因。
从函数参数中可以看到其实字符串对象还是被创建了,内部其实始终会为字符串创建对象,但经过inter机制检查后,临时创建的字符串会因引用计数为0而被销毁,临时变量在内存中昙花一现然后迅速消失。
字符串缓冲池
除了字符串驻留池,Python还会保存所有ascii码内的单个字符:
staticPyObject*unicode_latin1[256]={NULL};
如果字符串其实是一个字符,那么优先从缓冲池中获取:
[unicodeobjec.c] PyObject*PyUnicode_DecodeUTF8Stateful(constchar*s, Py_ssize_tsize, constchar*errors, Py_ssize_t*consumed) { ... /*ASCIIisequivalenttothefirst128ordinalsinUnicode.*/ if(size==1&&(unsignedchar)s[0]<128){ returnget_latin1_char((unsignedchar)s[0]); } ... }
然后再经过intern机制后被保存到intern池中,这样驻留池中和缓冲池中,两者都是指向同一个字符串对象了。
严格来说,这个单字符缓冲池并不是省内存的方案,因为从中取出的对象几乎都会保存到缓冲池中,这个方案是为了减少字符串对象的创建。
总结
本文介绍了两种是节省内存的方案。一个字符串的每个字符在占用空间大小是相同的,取决于字符串中的最大字符。
短字符串会放到一个全局的字典中,该字典中的字符串成了单例模式,从而节省内存。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。