No.js中V8堆外内存管理和字符编码解码的实现

对于基于 V8 的 JS 运行时来说,堆外内存的管理是非常重要的一部分,因为 gc 的原因,V8 自己管理堆内存大小是有限制的,我们不能什么数据都往 V8 的堆里存储,比如我们想一下读取一个 1G 的文件,如果存到 V8 的堆,一下子就满了,所以我们需要定义堆外内存并进行管理。本文介绍 No.js 里目前支持的简单堆内存管理机制和字符编码解码的实现。

创新互联建站是一家网站设计公司,集创意、互联网应用、软件技术为一体的创意网站建设服务商,主营产品:响应式网站开发品牌网站建设网络营销推广。我们专注企业品牌在网站中的整体树立,网络互动的体验,以及在手机等移动端的优质呈现。做网站、成都网站制作、移动互联产品、网络运营、VI设计、云产品.运维为核心业务。为用户提供一站式解决方案,我们深知市场的竞争激烈,认真对待每位客户,为客户提供赏析悦目的作品,网站的价值服务。

1 字符串的使用

数据的读写,在底层都是一个个字节,那么我们在 JS 层定义的字符串,C++ 层是怎么获取的呢?比如我们在 JS 里调用自定义 log 函数打印日志。

 
 
 
  1. log("hello"); 

我们来看看 JS 运行时中 log 函数的实现。

 
 
 
  1. void No::Console::log(V8_ARGS) { 
  2.     V8_ISOLATE 
  3.     String::Utf8Value str(isolate, args[0]); 
  4.     Log(*str); 

最终在 C++ 里可以通过 V8 提供的 String::Utf8Value 从 args 中获得 JS 层的字符串,然后调用系统函数把它打印到屏幕就行。但是这种形式使用的内容是 V8 的堆内存。那么如果我们需要操作一个非常大的字符串,那怎么办呢?这时候就需要使用 V8 提供的堆外内存机制 ArrayBuffer。

2 ArrayBuffer 的实现

我们看看这个类关于内存申请的一些实现细节。当我们在 JS 里执行以下代码时

 
 
 
  1. new ArrayBuffer(1) 

来看看 V8 的实现。

 
 
 
  1. BUILTIN(ArrayBufferConstructor) { 
  2.   // [[Construct]]  args 为 JS 层的参数 
  3.   Handle new_target = Handle::cast(args.new_target()); 
  4.   // JS 层定义的长度,即 ArrayBuffer 的第一个参数 
  5.   Handle length = args.atOrUndefined(isolate, 1); 
  6.  
  7.   return ConstructBuffer(isolate,  
  8.                          target,  
  9.                          new_target,  
  10.                          number_length, // = length 
  11.                          number_max_length,  // 空 
  12.                          InitializedFlag::kZeroInitialized); 
  13. 接着看 ConstructBuffer 。

     
     
     
    1. Object ConstructBuffer(Isolate* isolate, Handle target, 
    2.                        Handle new_target, Handle length, 
    3.                        Handle max_length, InitializedFlag initialized) { 
    4.       // resizable = ResizableFlag::kNotResizable 
    5.       ResizableFlag resizable = max_length.is_null() ? ResizableFlag::kNotResizable : ResizableFlag::kResizable; 
    6.       // 申请一个 JSArrayBuffer 对象,不包括存储数据的内存                                               
    7.       Handle result; 
    8.       ASSIGN_RETURN_FAILURE_ON_EXCEPTION( 
    9.           isolate, result, 
    10.           JSObject::New(target, new_target, Handle::null())); 
    11.       auto array_buffer = Handle::cast(result); 
    12.      
    13.       size_t byte_length; 
    14.       size_t max_byte_length = 0; 
    15.       // byte_length:需要申请的字节数,由 length Object 解析得到,并且校验申请的大小是否超过阈值 
    16.       if (!TryNumberToSize(*length, &byte_length) || 
    17.           byte_length > JSArrayBuffer::kMaxByteLength) { 
    18.           // ... 
    19.       } 
    20.       std::unique_ptr backing_store; 
    21.       // 申请存储数据的内存 
    22.       backing_store = BackingStore::Allocate(isolate, byte_length, shared, initialized); 
    23.       max_byte_length = byte_length; 
    24.       // 保存ArrayBuffer 存储数据的内存 
    25.       array_buffer->Attach(std::move(backing_store)); 
    26.       array_buffer->set_max_byte_length(max_byte_length); 
    27. 以上代码首先申请了一个 JSArrayBuffer 对象,但是申请的对象中不包括存储数据的内存,接着通过 BackingStore::Allocate 申请存储数据的内存,并且保存到 JSArrayBuffer 中。我们接着看 BackingStore::Allocate 的内存分配逻辑。

       
       
       
      1. std::unique_ptr BackingStore::Allocate( 
      2.     Isolate* isolate, size_t byte_length, SharedFlag shared, 
      3.     InitializedFlag initialized) { 
      4.   void* buffer_start = nullptr; 
      5.   // ArrayBuffer 的内存分配器,初始化 V8 的时候可以设置 
      6.   auto allocator = isolate->array_buffer_allocator(); 
      7.   if (byte_length != 0) { 
      8.     auto allocate_buffer = [allocator, initialized](size_t byte_length) { 
      9.       void* buffer_start = allocator->Allocate(byte_length); 
      10.       return buffer_start; 
      11.     }; 
      12.     // 执行 allocate_buffer 分配内存 
      13.     buffer_start = isolate->heap()->AllocateExternalBackingStore(allocate_buffer, byte_length); 
      14.   } 
      15.   / 分配一个 BackingStore 对象管理上面申请的内存 
      16.   auto result = new BackingStore(...); 
      17.   return std::unique_ptr(result); 

      我们看到最终通过 allocator->Allocate 分配内存,allocator 是在初始化 V8 的时候设置的,比如 No.js 设置的 ArrayBuffer::Allocator::NewDefaultAllocator()。

       
       
       
      1. v8::ArrayBuffer::Allocator* v8::ArrayBuffer::Allocator::NewDefaultAllocator() { 
      2.   return new ArrayBufferAllocator(); 

      我们看看 ArrayBufferAllocator。

       
       
       
      1. class ArrayBufferAllocator : public v8::ArrayBuffer::Allocator { 
      2.  public: 
      3.   void* Allocate(size_t length) override { 
      4.     return page_allocator_->AllocatePages(nullptr, RoundUp(length, page_size_), 
      5.                                           page_size_, 
      6.                                           PageAllocator::kReadWrite); 
      7.   } 
      8.  private: 
      9.   PageAllocator* page_allocator_ = internal::GetPlatformDataCagePageAllocator(); 
      10.   const size_t page_size_ = page_allocator_->AllocatePageSize(); 
      11. }; 

      最终调用 page_allocator_ 去分配内存,从 page_allocator_ 的值 GetPlatformDataCagePageAllocator 我们可以看到这里是调用系统相关的函数去申请内存,比如 Linux 下的 mmap。至此我们看到了 ArrayBuffer 的内存由来,

      3 ArrayBuffer 应用

      有了 ArrayBuffer,我们就可以在 V8 堆之外申请内存了,我们看看 No.js 里怎么使用。

       
       
       
      1. http.createServer({host: '127.0.0.1', port: 8888}, (req, res) => { 
      2.     // HTTP 响应的 body 
      3.     const body  = `...`; 
      4.     // HTTP 响应报文 
      5.     const response = `...`; 
      6.     // 申请堆外内存 
      7.     const responseBuffer = new ArrayBuffer(response.length); 
      8.     // 把响应内容写入堆外内存 
      9.     const bytes = new Uint8Array(responseBuffer); 
      10.     for (let i = 0; i < response.length; i++) { 
      11.         bytes[i] = response[i].charCodeAt(0); 
      12.     } 
      13.     // 发送给客户端 
      14.     res.write(responseBuffer); 
      15. }); 

      接着我们看看 write 的实现。

       
       
       
      1. // 拿到 JS 的 ArrayBuffer 
      2. Local arrayBuffer = args[1].As(); 
      3. std::shared_ptr backing = arrayBuffer->GetBackingStore();// 申请一个写请求struct io_request *io_req = (struct io_request *)malloc(sizeof(*io_req));memset(io_req, 0, sizeof(*io_req));// 拿到底层存储数据的内存,保存到 request 中等待发送 
      4. io_req->buf = backing->Data(); 
      5. io_req->len = backing->ByteLength(); 

      JS 层设置数据,然后在 C++ 层拿到存储数据的内存发送出去,这个看起来可以满足需求,但是似乎还不够,首先每次都要自己申请一个 ArrayBuffer 和 Uint8Array 比较麻烦,而且还需要自己设置 Uint8Array 的内容,最重要的是 Uint8Array 只能保存单字节的数据,如果我们要发送非单字节的字符就会出现问题了。比如 “??“ 在 JS 里长度是 2,底层占四个字节。

       
       
       
      1. '????'.length => 2 

      所以还需要封装一个模块处理这些问题。

      4 Buffer

      类似 Node.js,No.js 也提供 Buffer 模块处理 V8 堆外内存,但是 No.js 没有 Node.js 实现的功能那么多。下面我们看看如何实现。

       
       
       
      1. class Buffer { 
      2.     bytes = null; 
      3.     memory = null; 
      4.     constructor({ length }) { 
      5.         this.memory = new ArrayBuffer(length); 
      6.         this.bytes = new Uint8Array(this.memory); 
      7.         this.byteLength = length; 
      8.     } 
      9.  
      10.     static from(str) { 
      11.         const chars = toUTF8(str); 
      12.         const buffer = new Buffer({length: chars.length}); 
      13.         for (let i = 0; i < buffer.byteLength; i++) { 
      14.             buffer.bytes[i] = chars[i]; 
      15.         } 
      16.         return buffer; 
      17.     } 
      18.  
      19.     static toString(bytes) { 
      20.         return fromUTF8(bytes); 
      21.     } 

      使用的方式和 Node.js 一样。

       
       
       
      1. Buffer.from("你好") 

      字符串通过 Buffer 类实现,Buffer 封装了 ArrayBuffer 和 Uint8Array,不过更重要的是实现了 UTF-8 编码和解码,这样应用层就可以传任何字符串,Buffer 会转成对应的 UTF-8 编码(一系列二进制数据),处理完后再通过底层传输就可以。看一下 UTF-8 编码解码的实现。

       
       
       
      1. function toUTF8(str) { 
      2.     // 通过 ... 解决多字节字符问题 
      3.     const chars = [...str]; 
      4.     const bytes = []; 
      5.     for (let i = 0; i < chars.length; i++) { 
      6.         const char = chars[i]; 
      7.         const code = char.codePointAt(0); 
      8.         if (code > 0 && code < 0x7F) { 
      9.             bytes.push(code) 
      10.         } else if (code > 0x80 && code < 0x7FF) { 
      11.             bytes.push((code >> 6) & 0x1f | 0xC0); 
      12.             bytes.push(code & 0x3f | 0x80);   
      13.         } else if ((code > 0x800 && code < 0xFFFF) || (code > 0xE000 && code < 0xFFFF)) { 
      14.             bytes.push((code >> 12) & 0x0f | 0xE0); 
      15.             bytes.push((code >> 6) & 0x3f | 0x80); 
      16.             bytes.push(code & 0x3f | 0x80);  
      17.         } else if (code > 0x10000 && code < 0x10FFFF) { 
      18.             bytes.push((code >> 18) & 0x07 | 0xF0); 
      19.             bytes.push((code >> 12) & 0x3f | 0x80); 
      20.             bytes.push((code >> 6) & 0x3f | 0x80); 
      21.             bytes.push(code & 0x3f | 0x80);  
      22.         }  
      23.     } 
      24.     return bytes; 

      toUTF8 把字符的 Unicode 码变成 UTF-8 编码,具体实现就是根据 UTF-8 的规则,但是有一个地方需要注意的是,不能简单遍历 JS 字符串。比如 “??“ 在遍历的时候情况如下

       
       
       
      1. '????'[0] => '\uD842''????'[1] => '\uDFB7' 

      所以需要处理一下使得每个字符变得一个独立的元素,再获得它的 unicode 码进行处理。

       
       
       
      1. const chars = [...str]; 

      接着看看 解码。

       
       
       
      1. // 计算二进制数最左边有多少个连续的 1 
      2. function countByte(byte) { 
      3.     let bytelen = 0; 
      4.     while(byte & 0x80) { 
      5.         bytelen++; 
      6.         byte = (byte << 1) & 0xFF; 
      7.     } 
      8.     return bytelen || 1;} 
      9.  
      10. function fromUTF8(bytes) { 
      11.     let i = 0; 
      12.     const chars = []; 
      13.     while(i < bytes.length) { 
      14.         const byteLen = countByte(bytes[i]); 
      15.         switch(byteLen) { 
      16.             case 1: 
      17.                 chars.push(String.fromCodePoint(bytes[i])); 
      18.                 i += 1; 
      19.                 break; 
      20.             case 2: 
      21.                 chars.push(String.fromCodePoint( (bytes[i] & 0x1F) << 6 | (bytes[i + 1] & 0x3F) )); 
      22.                 i += 2; 
      23.                 break; 
      24.             case 3: 
      25.                 chars.push(String.fromCodePoint( (bytes[i] & 0x0F) << 12 | (bytes[i + 1] & 0x3F) << 6| (bytes[i + 2] & 0x3F) )); 
      26.                 i += 3; 
      27.                 break; 
      28.             case 4: 
      29.                 chars.push(String.fromCodePoint( (bytes[i] & 0x07) << 18 | (bytes[i + 1] & 0x3F) << 12 | (bytes[i + 2] & 0x3F) << 6 | (bytes[i + 3] & 0x3F) )); 
      30.                 i += 4; 
      31.                 break; 
      32.             default: 
      33.                 throw new Error('invalid byte'); 
      34.         } 
      35.     } 
      36.     return chars.join(''); 

      解码的原理是首先计算单字节的最左边有多少个 1,这个表示后续的多少个字节组成一个字符。计算完后就把一个或多个字节按照 UTF-8 规则拼出 unicode 码,然后使用 fromCodePoint 转成对应字符。最后看看使用例子。

       
       
       
      1. http.createServer({host: '127.0.0.1', port: 8888}, (req, res) => { 
      2.     const body  = ` 
      3.          
      4.          
      5.             你好! 
      6.          
      7.         `; 
      8.     res.setHeaders({ 
      9.         "Content-Type": "text/html; charset=UTF-8" 
      10.     }); 
      11.     res.end(body); 
      12. }); 

      5 总结

       

      目前初步实现了堆外内存管理和编码解码的功能,这样应用层就不需要面对麻烦的堆外内存管理和数据设置问题。另外 V8 堆外内存我们平时可能关注的不是很多,但是却是一个重要的部分。

      文章名称:No.js中V8堆外内存管理和字符编码解码的实现
      当前URL:http://www.shufengxianlan.com/qtweb/news44/88794.html

      成都网站建设公司_创新互联,为您提供静态网站商城网站做网站python网站设计网站策划

      广告

      声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联