buffer是nodejs中存储长字符串以及二进制数据的存储介质,buffer我们在使用过程中到底要注意哪些问题?最近结合node的源码简单了解了一下buffer的工作机制。
关于buffer大家可能都听说过8KB的故事,至于8KB的内容我的另外一篇文章有比较详细的介绍,包括一个典型的内存泄露的例子:
buffer.concat引出的bug
打开0.10.4的源码,在lib目录下找到buffer.js,我们先概览一下整个文件的组成:
1、两个工具函数,clamp和toHex
2、SlowBuffer类,并且这个类的一些接口继承自buffer类
3、buffer类,定义并实现了node api文档上的接口函数
本文只讨论buffer实例的创建,读取和写入将留到下两章讨论。
一、创建buffer实例
我们从创建一个buffer开始,看看暴露在接口之后的node是如何实现buffer功能的。
1、比如我们创建一个1KB的buffer,var buf = new Buffer(1024);
2、buffer类会根据传入的字符串或大小数字或字符数组的大小来分配新的buffer池或者使用旧的,字符串或大小数字或字符数组的大小以下简称buf大小
2.1、如果buf大小大于8KB,则buffer类将返回一个slowbuffer实例给buf存储
2.2、如果buf大小小于8KB并且还小于当前buffer池内剩余的空间,则将此buf实例存入当前buffer池,和其他buffer实例共享这个8KB的内存池。
2.3、如果buf大小不大于0,则将zerobuffer实例返回给buf,也就是说所有0大小的buffer实例都是一个。
3、如果传入的参数不是数字,也就是说是字符串或者字符数组,在创建buf实例时会将内容写入刚才分配的buffer内存中。
3.1、如果是字符串,则调用如下代码:之后我们再讨论this.write方法
if (type === ‘string’) {
// We are a string
this.length = this.write(subject, 0, encoding); //将字符串写入
}
3.2、如果传入的参数是buffer实例,则将copy这份buffer实例内容:如果传入的buffer实例是与其他buffer共享内存存储的话,则要根据偏移量进行copy,如果是独享的则不用,偏移量设置为0。之后再讨论buffer.copy的方法
else if (Buffer.isBuffer(subject)) {
if (subject.parent)
subject.parent.copy(this.parent,
this.offset,
subject.offset,
this.length + subject.offset);
else
subject.copy(this.parent, this.offset, 0, this.length);
}
3.3、如果是字符数组,则循环将buf实例的parent实例的偏移之后的内容刷入字符数组的内容,代码如下:
else if (isArrayIsh(subject)) {
for (var i = 0; i < this.length; i++)
this.parent[i + this.offset] = subject[i];
}
我们看一下isArrayIsh工具函数
function isArrayIsh(subject) {
return Array.isArray(subject) ||
subject && typeof subject === ‘object’ &&
typeof subject.length === ‘number’;
}
这个isArrayIsh接受2种数组
1、[‘a’,’b’,’c’,’d’]
2、{1:’a’,2:’b’,3:’c’,4:’d’,length:4}
4.最后buffer类将调用C++接口,把数据刷入内存,其实是利用v8接口建立起内存地址和js对象之间的引用。
SlowBuffer.makeFastBuffer(this.parent, this, this.offset, this.length);
目前为止,我们创建了一个新的buf实例,但是具体它是如何被创建和存储的呢?我们主要看如下代码:
当创建的buffer超过8KB时,buffer.js用如下代码创建一个buffer实例,
this.parent = new SlowBuffer(this.length);
当小于8KB时则使用如下代码,创建一个空的8KB内存空间
function allocPool() {
pool = new SlowBuffer(Buffer.poolSize);
pool.used = 0;
}
buffer类代码:
if (!pool || pool.length - pool.used < this.length) allocPool();
this.parent = pool;
可见,buffer实例的parent属性保存着slowbuffer实例,可以调用c++封装暴露出的接口。
我们先看一下C++为slowerbuffer定义了多少接口:
static void Initialize(v8::Handlev8::Object target); //初始化函数
// copy free
NODE_SET_PROTOTYPE_METHOD(constructor_template, “binarySlice”, Buffer::BinarySlice);
NODE_SET_PROTOTYPE_METHOD(constructor_template, “asciiSlice”, Buffer::AsciiSlice);
NODE_SET_PROTOTYPE_METHOD(constructor_template, “base64Slice”, Buffer::Base64Slice);
NODE_SET_PROTOTYPE_METHOD(constructor_template, “ucs2Slice”, Buffer::Ucs2Slice);
NODE_SET_PROTOTYPE_METHOD(constructor_template, “hexSlice”, Buffer::HexSlice);
// TODO NODE_SET_PROTOTYPE_METHOD(t, “utf16Slice”, Utf16Slice);
// copy
NODE_SET_PROTOTYPE_METHOD(constructor_template, “utf8Slice”, Buffer::Utf8Slice);
NODE_SET_PROTOTYPE_METHOD(constructor_template, “utf8Write”, Buffer::Utf8Write);
NODE_SET_PROTOTYPE_METHOD(constructor_template, “asciiWrite”, Buffer::AsciiWrite);
NODE_SET_PROTOTYPE_METHOD(constructor_template, “binaryWrite”, Buffer::BinaryWrite);
NODE_SET_PROTOTYPE_METHOD(constructor_template, “base64Write”, Buffer::Base64Write);
NODE_SET_PROTOTYPE_METHOD(constructor_template, “ucs2Write”, Buffer::Ucs2Write);
NODE_SET_PROTOTYPE_METHOD(constructor_template, “hexWrite”, Buffer::HexWrite);
NODE_SET_PROTOTYPE_METHOD(constructor_template, “readFloatLE”, Buffer::ReadFloatLE);
NODE_SET_PROTOTYPE_METHOD(constructor_template, “readFloatBE”, Buffer::ReadFloatBE);
NODE_SET_PROTOTYPE_METHOD(constructor_template, “readDoubleLE”, Buffer::ReadDoubleLE);
NODE_SET_PROTOTYPE_METHOD(constructor_template, “readDoubleBE”, Buffer::ReadDoubleBE);
NODE_SET_PROTOTYPE_METHOD(constructor_template, “writeFloatLE”, Buffer::WriteFloatLE);
NODE_SET_PROTOTYPE_METHOD(constructor_template, “writeFloatBE”, Buffer::WriteFloatBE);
NODE_SET_PROTOTYPE_METHOD(constructor_template, “writeDoubleLE”, Buffer::WriteDoubleLE);
NODE_SET_PROTOTYPE_METHOD(constructor_template, “writeDoubleBE”, Buffer::WriteDoubleBE);
NODE_SET_PROTOTYPE_METHOD(constructor_template, “fill”, Buffer::Fill);
NODE_SET_PROTOTYPE_METHOD(constructor_template, “copy”, Buffer::Copy);
NODE_SET_METHOD(constructor_template->GetFunction(),
“byteLength”,
Buffer::ByteLength);
NODE_SET_METHOD(constructor_template->GetFunction(),
“makeFastBuffer”,
Buffer::MakeFastBuffer);
大致定义了以上这么多接口可以供node调用,同时对buffer.cc还定义了setFastBufferConstructor函数,不过在buffer.js中没有用到它,是用来设置fast_buffer_constructor静态变量的,主要用于判断node的对象是否为Buffer类的实例
当node调用 var buf = new Buffer()
会将poolsize发给buffer::new这个方法:
Handle
if (!args.IsConstructCall()) { //如果不是构造函数调用,如果不是,则使用构造函数调用
return FromConstructorTemplate(constructor_template, args);
}
HandleScope scope;
if (!args[0]->IsUint32()) return ThrowTypeError(“Bad argument”);
size_t length = args[0]->Uint32Value();
if (length > Buffer::kMaxLength) {
return ThrowRangeError(“length > kMaxLength”);
}
new Buffer(args.This(), length);
return args.This();
}
对参数做了一些合法性验证之后,将实例化Buffer类,执行Buffer的构造函数:
Buffer::Buffer(Handle
length_ = 0;
callback_ = NULL;
handle_.SetWrapperClassId(BUFFER_CLASS_ID);
//定义包装的对象ID,检查堆的运行情况,初始化时会去定义这个堆的id和回调函数
Replace(NULL, length, NULL, NULL);
}
Buffer类构造函数初始化了两个类成员,然后设定了 SetWrapperClassId ,最后调用replace函数申请内存空间
// if replace doesn’t have a callback, data must be copied
// const_cast in Buffer::New requires this
void Buffer::Replace(char data, size_t length,
free_callback callback, void hint) {
HandleScope scope;
if (callback_) {//非初始化执行
callback_(data_, callback_hint_);
} else if (length_) {
delete [] data_;
V8::AdjustAmountOfExternalAllocatedMemory(
-static_cast<intptr_t>(sizeof(Buffer) + length_));
}
length_ = length;
callback_ = callback;
callback_hint_ = hint;
if (callback_) { //初始化不执行
data_ = data;
} else if (length_) { //初始化执行
data_ = new char[length_]; //将data_指针指向char[length]
if (data) //参数传递了
memcpy(data_, data, length_); //从data内存指针拷贝length长度的字节到data_指针指向的内存中
V8::AdjustAmountOfExternalAllocatedMemory(sizeof(Buffer) + length_);
//调用V8调整外部内存大小的
//文档上说注册更多的外部内存会让V8的GC更加活跃
//当然从这点我们就可以发现,slowbuffer的创建确实会有消耗
} else {
data_ = NULL;
}
handle_->SetIndexedPropertiesToExternalArrayData(data_,
kExternalUnsignedByteArray,
length_);
//SetIndexedPropertiesToExternalArrayData表示将js对象的内存地址通过V8做好关联,当js对象失去对这个地址的访问
//v8引起将delete这个data_ 指针。
handle_->Set(length_symbol, Integer::NewFromUnsigned(length_));
//关于这个handle_是在node_object_wrap.h文件中的ObjectWrap类定义的
//v8::Persistentv8::Object handle_; // ro 至于 Persistent 和 handle 的区别,cnode上有一篇文章介绍的很详细,
//简单点说就是:handle是栈,Persistent是堆
//最后这行表示设置这个对象的属性length
//length_symbol 表示length ,见代码:
// length_symbol = NODE_PSYMBOL(“length”);
}
另外v8手册已经废弃了V8::AdjustAmountOfExternalAllocatedMemory,转而使用Isolate类
static intptr_t v8::V8::AdjustAmountOfExternalAllocatedMemory ( intptr_t change_in_bytes ) [static]
Deprecated. Use Isolate::AdjustAmountOfExternalAllocatedMemory instead.
测试环境:4CPU Linux 2.6.8 x64 8G Men
测试1:
生成两种buffer,对比速度:
A、10244
B、10244+1
代码:
var time = 1010000; //10万次
console.time(‘10244’)
for(var i=0;i<time;i++)
var x = new Buffer(10244);
console.timeEnd(‘10244’)
console.time(‘10244+1’)
for(var j=0;j<time;j++)
var y = new Buffer(10244+1);
console.timeEnd(‘1024*4+1’)
测试结果:
10244: 337ms
10244+1: 615ms
虽然只有1字节的改变,但是生成的速度却将近相差1倍。
当然这也算8KB的一个注意点,但是从中我们不难发现,重新申请一份额外的内存空间的消耗是挺大的。
测试2:
我们如何避免频繁的生成slowbuffer将是性能上的一个重点
比如我们有10万个长度在1至2048之间不等的字符串我们需要保存,并且我们需要快速的读取其中的任意一个字符串出来。
测试代码:
var time = 10*10000;
var str = ‘1’;
var max = 2048;
console.time(‘many buffer’)
var ary1=[]
for(var i=0;i<time;i++){
var tempi = Math.ceil(Math.random()*max)
var tempstr = str
while(tempi–){
tempstr += str
}
ary1.push(new Buffer(tempstr))
}
console.timeEnd(‘many buffer’)
console.time(‘one buffer’)
var ary_offset=[];
var ary_len=[];
var tempbuf = new Buffer(timemax)
var offset = 0;
for(var i=0;i<time;i++){
var len;
var tempi = len = Math.ceil(Math.random()max)
var tempstr = str
while(tempi–){
tempstr += str
}
var end = offset+len
tempbuf.fill(tempstr, offset, end)
ary_offset.push(offset)
ary_len.push(end)
offset = end
}
console.timeEnd(‘one buffer’)
console.time(‘many buffer read’)
for(var x=0;x<100000;x++){
ary1[x].toString(‘utf-8’)
}
console.timeEnd(‘many buffer read’)
console.time(‘one buffer read’)
for(var y=0;y<100000;y++){
tempbuf.toString(‘utf-8’, ary_offset[y], ary_len[y])
}
console.timeEnd(‘one buffer read’)
测试结果:
many buffer: 4622ms
one buffer: 2942ms
many buffer read: 92ms
one buffer read: 91ms
结果表明这两种方法生成的速度相差比较大,但是遍历读取速度相当,可是消耗的内存第二种更大一些。
对于内存的消耗和执行的时间取舍我们要根据实际情况来取舍了。
总结一下:
1、8KB的内存使用注意情况,不多说了,看我上面给出的链接有详细说明,第一个例子也说明了8KB的问题
2、可能创建buffer对于8KB的性能问题更突出一些,但是我们还是应当尽量避免大数量的创建buffer对象,
如果真的有必要创建很多buffer对象,不如创建一个大的buffer,然后记录每