Skip to content

Buffer探究

Buffer的内存分配机制

buffer对应于 V8 堆内存之外的一块原始内存:

Buffer是一个典型的javascriptC++结合的模块,与性能有关的用C++来实现,javascript 负责衔接和提供接口。Buffer所占的内存不是V8堆内存,是独立于V8堆内存之外的内存,通过C++层面实现内存申请(可以说真正的内存是C++层面提供的)、javascript 分配内存(可以说JavaScript层面只是使用它)。Buffer在分配内存最终是使用ArrayBuffer对象作为载体。简单点而言, 就是Buffer模块使用v8::ArrayBuffer分配一片内存,通过TypedArray中的v8::Uint8Array来去写数据。

内存分配的8K机制

  • 分配小内存

说道Buffer的内存分配就不得不说Buffer8KB的问题,对应buffer.js源码里面的处理如下:

JavaScript
Buffer.poolSize = 8 * 1024;

function allocate(size)
{
    if(size <= 0 )
        return new FastBuffer();
    if(size < Buffer.poolSize >>> 1 )
        if(size > poolSize - poolOffset)
            createPool();
        var b = allocPool.slice(poolOffset,poolOffset + size);
        poolOffset += size;
        alignPool();
        return b
    } else {
        return createUnsafeBuffer(size);
    }
}

源码直接看来就是以8KB作为界限,如果写入的数据大于8KB一半的话直接则直接去分配内存,如果小于4KB的话则从当前分配池里面判断是否够空间放下当前存储的数据,如果不够则重新去申请8KB的内存空间,把数据存储到新申请的空间里面,如果足够写入则直接写入数据到内存空间里面,下图为其内存分配策略。

An image

看内存分配策略图,如果当前存储了2KB的数据,后面要存储5KB大小数据的时候分配池判断所需内存空间大于4KB,则会去重新申请内存空间来存储5KB数据并且分配池的当前偏移指针也是指向新申请的内存空间,这时候就之前剩余的6KB(8KB-2KB)内存空间就会被搁置。至于为什么会用8KB作为存储单元分配,为什么大于8KB按照大内存分配策略,在下面Buffer内存分配机制优点有说明。

  • 分配大内存

还是看上面那张内存分配图,如果需要超过8KBBuffer对象,将会直接分配一个SlowBuffer对象作为基础单元,这个基础单元将会被这个大Buffer对象独占。

JavaScript
// Big buffer,just alloc one
this.parent = new SlowBuffer(this.length);
this.offset = 0;

这里的SlowBUffer类实在C++中定义的,虽然引用buffer模块可以访问到它,但是不推荐直接操作它,而是用Buffer替代。这里内部parent属性指向的SlowBuffer对象来自Node自身C++中的定义,是C++层面的Buffer对象,所用内存不在V8的堆中

  • 内存分配的限制

此外,Buffer单次的内存分配也有限制,而这个限制根据不同操作系统而不同,而这个限制可以看到node_buffer.h里面

C
    static const unsigned int kMaxLength =
    sizeof(int32_t) == sizeof(intptr_t) ? 0x3fffffff : 0x7fffffff;

对于32位的操作系统单次可最大分配的内存为1G,对于64位或者更高的为2G。

buffer内存分配机制优点

Buffer真正的内存实在NodeC++层面提供的,JavaScript层面只是使用它。当进行小而频繁的Buffer操作时,采用的是8KB为一个单元的机制进行预先申请和事后分配,使得Javascript到操作系统之间不必有过多的内存申请方面的系统调用。对于大块的Buffer而言(大于8KB),则直接使用C++层面提供的内存,则无需细腻的分配操作。

Buffer与stream

stream的流动为什么要使用二进制Buffer

根据最初代码的打印结果,stream中流动的数据就是Buffer类型,也就是二进制

原因一:

node官方使用二进制作为数据流动肯定是考虑过很多,stream主要的设计目的——是为了优化IO操作文件IO网络IO),对应后端无论是文件IO还是网络IO,其中包含的数据格式都是未知的,有可能是字符串,音频,视频,网络包等等,即使就是字符串,它的编码格式也是未知的,可能ASC编码,也可能utf-8编码,对于这些未知的情况,还不如直接使用最通用的格式二进制.

原因二:

Buffer对于http请求也会带来性能提升。

举一个例子:

JavaScript
const http = require('http');
const fs = require('fs');
const path = require('path');

const server = http.createServer(function (req, res) {
    const fileName = path.resolve(__dirname, 'buffer-test.txt');
    fs.readFile(fileName, function (err, data) {
        res.end(data)   // 测试1 :直接返回二进制数据
        // res.end(data.toString())  // 测试2 :返回字符串数据
    });
});
server.listen(8000);

将代码中的buffer-test文件大小增加到50KB左右,然后使用ab工具测试一下性能,你会发现无论是从吞吐量(Requests per second)还是连接时间上,返回二进制格式比返回字符串格式效率提高很多。为何字符串格式效率低?—— 因为网络请求的数据本来就是二进制格式传输,虽然代码中写的是 response 返回字符串,最终还得再转换为二进制进行传输,多了一步操作,效率当然低了。

Buffer在stream数据流转充当的角色

我们可以把整个流(stream)Buffer的配合过程看作公交站。在一些公交站,公交车在没有装满乘客前是不会发车的,或者在特定的时刻才会发车。当然,乘客也可能在不同的时间,人流量大小也会有所不同,有人多的时候,有人少的时候,乘客公交车站都无法控制人流量。

不论何时,早到的乘客都必须等待,直到公交车接到指令可以发车。当乘客到站,发现公交车已经装满,或者已经开走,他就必须等待下一班车次。

总之,这里总会有一个等待的地方,这个等待的区域就是Node.js中的BufferNode.js不能控制数据什么时候传输过来,传输速度,就好像公交车站无法控制人流量一样。他只能决定什么时候发送数据(公交车发车)。如果时间还不到,那么Node.js就会把数据放入Buffer等待区域中,一个在RAM中的地址,直到把他们发送出去进行处理。

注意点:

Buffer虽好也不要瞎用,BufferString两者都可以存储字符串类型的数据,但是,StringBuffer不同,在内存分配上面,String直接使用v8堆存储,不用经过c++堆外分配内存,并且Google也对String进行优化,在实际的拼接测速对比中,StringBuffer快。但是Buffer的出现是为了处理二进制以及其他非Unicode编码的数据,所以在处理非utf8数据的时候需要使用到Buffer来处理。