浅析node的buffer模块(一创建)

2013-04-19 18:55

浅析node的buffer模块(一创建)

by snoopyxdy

at 2013-04-19 10:55:25

original http://snoopyxdy.blog.163.com/blog/static/60117440201331683752285

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::Handle<v8::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<Value> Buffer::New(const Arguments &args) {
  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<Object> wrapper, size_t length) : ObjectWrap() {
  Wrap(wrapper);

  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::Persistent<v8::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]


测试环境:4CPU Linux 2.6.8 x64 8G Men

测试1:
生成两种buffer,对比速度:
A、1024*4
B、1024*4+1
代码:

var time = 10*10000; //10万次
console.time('1024*4')
for(var i=0;i<time;i++)
var x = new Buffer(1024*4);
console.timeEnd('1024*4')

console.time('1024*4+1')
for(var j=0;j<time;j++)
var y = new Buffer(1024*4+1);
console.timeEnd('1024*4+1')


测试结果:

1024*4: 337ms
1024*4+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(time*max)
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,然后记录每块使用的偏移这样比生成很多小buffer要快很多。