`
RednaxelaFX
  • 浏览: 3052450 次
  • 性别: Icon_minigender_1
  • 来自: 海外
社区版块
存档分类
最新评论

字符串的一般封装方式的内存布局 (0): 拿在手上的是什么

阅读更多
(Disclaimer:未经许可请勿转载。如需转载请先与我联系。
作者:RednaxelaFX -> rednaxelafx.iteye.com)

字符串的一般封装方式的内存布局系列:
(0): 拿在手上的是什么
(1): 元数据与字符串内容,整体还是分离?

原本我写这个是作为一个讨论JavaScript String的内存布局的回帖的一部分,不过越写越长觉得跑题有点多所以干脆抽出来单独写一系列笔记好了。下面的讨论有原帖背景影响:
* JavaScript String分配在栈上还是在堆上?
* Lua的string是copy-on-write的吗?
请留意讨论的背景。

引用
字符串不就是一坨内存么?这坨内存不是就得在栈上或者堆上么?

嗯…不是。且不说还有全局数据区而字符串也可以存在那里,字符串它不一定只是“一坨内存”。下面展开来看看。

关于字符串的一般封装方式的内存布局

就不说char*这种裸字符串,只说面向对象的字符串封装。
进一步限制范围,这里只讨论“扁平”(flat)的字符串,也就是字符串内容按顺序紧密排布在内存中的数据结构,而不讨论像rope那样用链式结构来拼接字符串的数据结构。

回顾上面提到需要完全动态分配内存的条件,其中一个是无法事先判断数据大小。通用的string类型字符串内容的长度不固定,因而整个string的大小无法事先确定,无论是无论可变还是不可变字符串。这就意味着string整体看至少在某些情况下得有一部分(变长的部分)要使用动态内存分配,也就是“分配在堆上”。

string类型的封装可以在几个维度上表现出差异:
0、“拿在手上”的是什么?
1、字符串元数据与字符串内容打包为整体存放,还是分离存放;
2、不同字符串实例是否共享字符串内容;
3、字符串是否显式记录长度;
4、字符串是否有'\0'结尾(null-terminated),字符串内容是否允许存'\0'(embedded null);
5、外部指针或引用指向字符串的什么位置;
6、字符串的存储容量(capacity)是否可以大于字符串内容的长度(length);
7、是否有对齐要求,结尾是否有padding。

0、拿在手上的是什么?

假设有
mystringtype s = "foobar";
mystringtype s1 = s;

那拿在手上的“s”与“s1”也要占存储空间,它里面到底装着什么?
按照离“真实数据”的距离从近到远,可以有下面几种情况:
a) 直接是字符串内容?
b) 是指向字符串实体的指针?
c) 是指向字符串实体的“指针的指针”?
d) 是一个代表某个字符串的token?


a) 直接是字符串内容

比较少见,但并不是不存在。有些C++标准库实现的std::basic_string采用了SSO(short string optimization),可以把短字符串(7个wchar_t或者15个char之类的)直接塞在std::string结构体里;长度大于阈值的字符串就还是把字符串内容分配在堆上。此类实现中,
std::string s("foobar");
std::string s1 = s;

里面的s就会直接持有"foobar"内容,而不是“指向字符串实体的指针”。

例如VS2012/VC11的实现就是此类。把VC11的std::string极度简化,它的数据部分如下:
class string {
  enum { _BUF_SIZE = 16 };
  union _Bxty {
    // storage for small buffer or pointer to larger one
    char  _Buf[_BUF_SIZE];
    char* _Ptr;
  } _Bx;
  size_t _Mysize; // current length of string
  size_t _Myres;  // current storage reserved for string
};

可以看到它的第一个实例成员_Bx是个大小为16字节的union,里面既可以装下长度小于_BUF_SIZE的字符串内容,也可以装下一个指针(当字符串长度不小于_BUF_SIZE时)。这种称为SSO的技巧可以让小字符串直接内嵌在std::string实例结构内,此时不需要额外在堆上分配buffer所以减少了堆空间开销,也提高了数据的局部性。当然也有代价,也就是每次要访问字符串内容都得先根据_Myres与_BUF_SIZE的比较来判断当前处于"short string"还是"long string"模式,增加了一点代码的复杂度,不过总体看由于提高了数据局部性,倒未必增加了时间开销。

对"foobar"的例子,在32位x86上VC11的std::string在内存里可以是这样:
0x0042FE54  66 6f 6f 62 61 72 00 00 b9 21 a2 00 68 f7 0c 95
0x0042FE64  06 00 00 00 0f 00 00 00 

s: 0x0042FE54 (24字节)
 (+0) [ _Bx._Buf = 0x66 ('f') 0x6F ('o') 0x6F ('o') 0x62 ('b') 0x61 ('a') 0x72 ('r') 0x00 ('\0') ... ]
(+16) [ _Mysize  = 0x00000006 ]
(+20) [ _Myres   = 0x0000000F ]

64位x86上则可以是这样:
0x000000000024F8E8  66 6f 6f 62 61 72 00 00 69 2f d5 a1 1d d9 ce 01
0x000000000024F8F8  06 00 00 00 00 00 00 00 0f 00 00 00 00 00 00 00

s: 0x000000000024F8E8 (32字节)
 (+0) [ _Bx._Buf = 0x66 ('f') 0x6F ('o') 0x6F ('o') 0x62 ('b') 0x61 ('a') 0x72 ('r') 0x00 ('\0') ... ]
(+16) [ _Mysize  = 0x0000000000000006 ]
(+24) [ _Myres   = 0x000000000000000F ]

头16字节就是_Bx成员的范围,该例中头6字节是"foobar"的内容,接着是'\0'(null-terminate),剩余部分是未使用数据(并不保证清零);然后是_Mysize = 6与_Myres = 15。

到s1 = s的时候,s1就完整拷贝了s的内容,然后s1里就也内嵌着一份"foobar"了,两者没有共享数据。

b) 是指向字符串实体的指针

许多高级语言虚拟机的实现都会选用这种方案。它们会限制对对象的访问,不允许直接访问对象的内容,而是必须通过引用来间接访问。这样就至少有一层间接。当这个间接层通过“直接指针”来实现时,这种管理内存的方式叫做pointer-based memory management。

例子中的“s”“s1”是引用,引用自身的值是个指针;“s”“s1”两个引用指向同一个String实例。例如说由CLR实现的.NET和由HotSpot VM实现的Java都是这样。后面还会有相关例子所以现在先不展开写。
s:              string object:
[ pointer ] --> [ "foobar" ]
             /
s1:         /
[ pointer ]


c) 是指向字符串实体的“指针的指针”

比上一种情况更多一层或多层间接。多出来的间接层通常叫做handle(句柄),相应的内存管理方式叫做handle-based。

句柄的常见实现方式是“指针的指针”(pointer-to-pointer),也就是比直接指针多一层间接:
s:             handle table:   string object:
[ handle ] --> [ pointer ] --> [ "foobar" ]
            /
s1:        /
[ handle ]

像Sun JDK 1.0.2里的JVM就是这样的。

使用句柄的好处是实现起来可以偷懒。假如有内存管理器需要移动对象(例如说mark-compact或者copying GC),那就得修正所有相关指针。但遍历所有相关指针需要费点功夫,想偷懒的话就可以像这样增加一个间接层,不允许外界直接拥有指向对象的指针,而是让外界持有句柄,句柄可以是指向“句柄表”(handle table)的指针,而句柄表里的元素才真的持有指向对象的指针。要修正指针的时候只要遍历句柄表来修正即可。

用句柄的坏处就是时间和空间开销都较大。合适的使用场景有两种:1、想偷懒;2、想隐藏信息。

d) 是一个代表某个字符串的token

这算是上一种情况的进一步特例。所谓“句柄”不一定要是“指针的指针”,也可以是更加间接的东西,例如说如果“句柄表”是一个数组,那“句柄”可以只是下标而不是指针;如果“句柄表”是一个稀疏数组(可能用哈希表来实现),那“句柄”可能也只是个稀疏数组的下标(可能用哈希表的键来实现)。这样的句柄有时候也叫做token、ID之类的。

Ruby 1.8.7的Symbol就是这种特殊句柄的实际应用。
Ruby的Symbol跟String都可用来表示字符串信息,区别在于:
* Symbol是驻留(interned)的,String不是。驻留的意味着相同内容的“Symbol对象实例”只会有一份;
* Symbol不可变,String可以可变(也可以是frozen string,那就不可变)。

Symbol在Ruby里是如此特别,在表示Ruby的值的VALUE类型里都有针对Symbol的特化。
下面的例子连续使用了3个Symbol,赋值给局部变量s:
s = :rednaxelafx
s = :rednaxelapx
s = :rednaxelagx

假定这3个Symbol都是之前没出现过的,那么它们3个就会按顺序被接连intern起来。

局部变量s的类型从C的角度看是VALUE。三次赋值后s的内容(VALUE的值)可能分别是:
(例子在Mac OS X 10.7.5/x86-64/Ruby 1.8.7上运行)
0x00000000005F390E
0x00000000005F410E
0x00000000005F490E

看不出来有什么联系?换成二进制来看:
ID                                                    | ID_LOCAL | SYMBOL_FLAG
00000000000000000000000000000000000000000101111100111 | 001      | 00001110
00000000000000000000000000000000000000000101111101000 | 001      | 00001110
00000000000000000000000000000000000000000101111101001 | 001      | 00001110

Ruby 1.8.7的VALUE是一种tagged pointer类型:最低8位是用来标识值的特殊类型的标记(tag),其中用来标记Symbol的SYMBOL_FLAG值为0x0e;
当VALUE的标记是SYMBOL_FLAG时,紧挨着标记的3位用来表示Symbol的作用域(scope),其中用来标记局部标识符的ID_LOCAL的值为0x01;
再上面的高位是与Symbol一一对应的唯一值,是个整数ID。

把ID的部分单独抽出来看,可以看到例子里s的ID分别是
3047
3048
3049

是逐个递增上去的整数序列。这个ID与作用域标记一同构成了Ruby里用于表示Symbol的token,可以看作特殊形式的句柄。

这样,Symbol其实没有真正的“对象实例”,至少没有整体存在于堆上的对象实例。整个Symbol系统由3部分组成:
* 与Symbol一一对应的ID值,通常嵌在标记为SYMBOL_FLAG的VALUE里。这个ID除去作用域标记外的部分由一个全局计数器生成而来。而Symbol#object_id其实返回的也是由这个ID算出来的值。参考rb_intern()的实现;
* 一个全局的symbol table,是个哈希表,记录着ID到实际字符串内容的映射关系;
* 存有实际字符串信息的char数组。

知道Symbol#object_id与底层ID之间的映射关系后可以写出这样的小程序:
def id_with_scope(object_id)
  # sizeof(RVALUE) = 40 on 64-bit platform for Ruby 1.8.7
  ((object_id << 1) - (4 << 2)) / 40
end
ID_LOCAL = 1
ID_SHIFT = 3
def to_id(sym)
  return nil unless sym.is_a? Symbol
  id_with_scope(sym.object_id) >> ID_SHIFT
end

(只对Ruby 1.8系列在64位上正确。其它版本/平台的细节稍有不同,但原理一样。)
然后算出某个Symbol对应的ID值:
>> to_id :rednaxelafx
=> 3047
>> to_id :rednaxelapx
=> 3048
>> to_id :rednaxelagx
=> 3049


RubiniusSymbol也是用相似方式实现的。

从驻留的角度看,Ruby的Symbol跟Lua的string相似:两者的所有实例都是驻留的。但前者的引用的值(VALUE)有特殊表现形式,是一个整数ID;而后者还是用普通指针来实现引用的值(Value)。驻留、实例的特殊性,与是否使用指针来表现引用,是两个正交的问题。
分享到:
评论
2 楼 RednaxelaFX 2013-11-14  
xxd82329 写道
印象中Matz的C Ruby貌似也采取了您所说的“VS2012/VC11的实现就是此类”Union优化吧?

嗯,这里是出处。

http://patshaughnessy.net/2012/1/4/never-create-ruby-strings-longer-than-23-characters

说得没错,多谢补充 ^_^
描述MRI里类似优化的还有另一篇 http://patshaughnessy.net/2013/2/8/ruby-mri-source-code-idioms-3-embedded-objects
1 楼 xxd82329 2013-11-14  
好吧,没有人抢R大的沙发。

印象中Matz的C Ruby貌似也采取了您所说的“VS2012/VC11的实现就是此类”Union优化吧?

嗯,这里是出处。

http://patshaughnessy.net/2012/1/4/never-create-ruby-strings-longer-than-23-characters

相关推荐

    EasyiOS_iOS开发类的各种封装

    EasyiOS可能包含了一些常见的类别封装,如NSString的扩展,提供了更多字符串处理的方法;NSArray和NSDictionary的安全操作,防止因为nil值引发的崩溃;UIImage的便捷加载和处理方法等。 2. **分类(Extension)封装...

    wordOffice.zip

    封装类可能提供了方法,允许开发者传入字符串和坐标位置,就能在模板上定位并添加文本。这可以用于生成报告的标题、正文或者动态数据。 2. **插入图片**:插入图片功能在报表中常用于展示数据可视化结果或是图表。...

    C# 扫码枪代码 包括USB和串口两种方式

    在这个特定的项目中,我们关注的是如何利用C#与扫码枪进行交互,涵盖了通过USB和串口两种常见接口的连接方式。以下将详细介绍这些知识点。 1. **C#基本编程**:C#是一种面向对象的语言,由微软开发,适用于.NET ...

    封装的简易方便的时间选择器、选项选择器!LYLOptionPicker

    2. 数据源设置:开发者可以通过设置数据源数组,将选项数据绑定到选择器上,数组可以包含字符串或者其他自定义对象。 3. 动画效果:选择器在滚动时提供平滑的动画效果,提升用户体验。 4. 高度自适应:根据选项数量...

    java ppt及案例

    字符串在Java中是不可变的,它们有多种操作方法,如连接、查找、替换等。了解字符编码(如Unicode)和字符串操作对于处理文本数据至关重要。 5. **异常和垃圾收集**:Java使用异常处理来捕获和处理程序运行时的错误...

    C ++ 需要注意的问题 FAQ

    - **兼容性问题**:虽然理论上可行,但在实践中可能会遇到各种问题,如内存对齐问题。 - **推荐做法**:尽量使用 C++ 的标准库和智能指针来管理内存。 ##### 21. **我为什么必须使用一个造型来转换 *void?** - ...

    Android小型题库

    - **解析:** 字符串资源通常存储在`res/values/strings.xml`文件中。因此,正确答案是B: `strings.xml`。 **2. Activity加载顺序** - **知识点:** 在Android应用中,`AndroidManifest.xml`文件定义了应用的结构和...

    professional Cpp

    - **字符串类(string)**:详细介绍`std::string`类的用法,包括构造、复制、赋值和操作字符串的方法。 - **格式化输出**:使用流(如`std::stringstream`)进行字符串格式化输出。 - **正则表达式**:简要介绍C++...

    OC Control SDCycleScrollView(图片轮播器).zip

    5. 多种加载方式:支持本地图片、网络图片的加载,同时支持URL字符串或UIImage对象。 6. 自定义视图:不仅可以展示图片,还可以扩展为展示其他类型的视图,如广告卡片、视频等。 四、使用步骤 1. 引入库:在项目中...

    PCB的布局和布线ppt

    放置字符串和坐标用于标记和记录,尺寸标注则有助于确保制造精度。 在布线前,需要进行布线设置,例如设定布线规则、选择工作层和布线策略。自动布线工具可以快速完成大部分布线工作,但通常需要手动调整以满足特定...

    Android自定义控件视频下载链接

    1. attrs.xml:在res/values目录下创建attrs.xml文件,定义自定义属性,如颜色、尺寸、字符串等。 2. 使用`TypedArray`:在`onCreateView()`或`onInitializeView()`中获取自定义属性值,以便在初始化时使用。 三、...

    IOS面试题2018总结188题

    - **性能**:Swift 在某些方面比 Objective-C 表现更好,尤其是在编译速度和运行效率上。 #### 2. KVC(Key Value Coding)与KVO(Key Value Observing) - **KVC**:允许对象通过键来获取和设置值,而不必了解对象...

    深信服科技公司校园招聘笔试题

    **题目:** 给定以下结构体,请计算其大小并解释为什么在不同的平台上可能会得到不同的结果。 ```c++ struct { short a; long b; char c; } d; ``` **解析:** 结构体的大小取决于成员变量的类型以及编译器对内存...

    Android_TextView.rar_android

    在布局XML文件中,我们可以使用标签来创建TextView,并通过属性设置其文字内容、字体大小、颜色、对齐方式等。 2. **数据绑定**:在描述中提到的数据封装,可能指的是使用数据绑定机制。Android提供了Data Binding...

    2021-2022计算机二级等级考试试题及答案No.11992.docx

    - **应用场景**:在处理字符串数据时,正确使用字符串比较方法可以避免逻辑错误。 #### 25. Word中的表格操作 - **知识点**:Word中可通过拖动标尺上的“移动表格列”调整表格列宽(选项正确)。 - **应用场景**:...

    495个C语言问题

    `,则在局部作用域内可能会出现问题,因为字符串文字的生命周期仅限于其被创建的时间点。 12. **动态分配内存的初始化** - 使用`malloc()`分配的内存需要显式初始化,因为它不保证被清零。尝试通过`malloc()`直接...

    java2实验实用模板代码

    - 实验2:实例成员与类成员,探讨实例变量和静态变量的区别,理解它们在内存中的存储方式。 - 实验3:使用PACKAGE语句与IMPORT语句,解释包的概念,学习如何管理与导入类库。 4. **继承与接口** - 实验1:继承,...

    Protel 99 SE电子教案 第6章 PCB图设计常用操作功能.pptx

    工具栏中包含了绘制导线、放置焊盘、过孔、字符串、位置坐标、尺寸标注等基本元素的功能。 6.1.1 绘制导线:可以使用【Place】/【Interactive Routing】菜单命令或直接点击放置工具栏上的相应按钮来绘制导线,这在...

Global site tag (gtag.js) - Google Analytics