前言:上一篇介绍了通过快照加速Node.js启动的方法。除了快照,V8还提供了另一种加速代码执行的技术,那就是代码缓存。第一次通过V8执行JS时,V8需要实时解析编译JS代码。这需要一定的时间。代码缓存可以保存这个过程的一些信息。下次执行时,会传递缓存的信息。可以加快JS代码的执行速度。本文介绍如何在Node.js中使用代码缓存技术来加速Node.js的启动。首先看下Node.js的编译配置。'actions':[{'action_name':'node_js2c','process_outputs_as_sources':1,'inputs':['tools/js2c.py','<@(library_files)','<@(deps_files)','config.gypi'],'outputs':['<(SHARED_INTERMEDIATE_DIR)/node_javascript.cc',],'action':['<(python)','tools/js2c.py','--directory','lib','--target','<@(_outputs)','config.gypi','<@(deps_files)',],},]通过这个配置,编译Node.js时,会执行js2c.py并将输入写入node_javascript.cc文件。让我们看看生成了什么。其中定义了一个函数。在这个函数中,source_字段中不断的加入了一系列的内容。key是Node.js中原生的JS模块信息,value是模块的内容。我们只看一个模块assert/strict。constdata=[39,117,115,101,32,115,116,114,105,99,116,39,59,10,10,109,111,100,117,108,101,46,101,120,112,111,114,116,115,32,61,32,114,101,113,117,105,114,101,40,39,97,115,115,101,114,116,39,41,46,115,116,114,105,99,116,59,10];console.log(Buffer.from(data).toString('utf-8'))输出以下内容。'使用严格';module.exports=require('assert').strict;Node.js通过js2c.py脚本,将原生JS模块的内容写入到文件中,编译成Node.js可执行文件,这样运行时就不需要从硬盘读取相应的文件了Node.js启动。否则在启动或运行时动态加载原生JS模块会花费更多的时间,因为内存的速度比硬盘快很多。这是Node.js做的第一个优化。接下来看代码缓存,因为代码缓存是在这个基础上实现的。先看编译配置。['node_use_node_code_cache=="true"',{'dependencies':['mkcodecache',],'actions':[{'action_name':'run_mkcodecache','process_outputs_as_sources':1,'inputs':['<(mkcodecache_exec)',],'outputs':['<(SHARED_INTERMEDIATE_DIR)/node_code_cache.cc',],'action':['<@(_inputs)','<@(_outputs)',],},],},{'sources':['src/node_code_cache_stub.cc'],}],编译Node.js时如果node_use_node_code_cache为true,会生成代码缓存。如果我们不需要,可以关闭,具体执行./configure--without-node-code-cache。如果我们关闭代码缓存,这部分的Node.js实现是空的,具体在node_code_cache_stub.cc中。constboolhas_code_cache=false;voidNativeModuleEnv::InitializeCodeCache(){}没有任何意义。如果我们启用代码缓存,将执行mkcodecache.cc以生成代码缓存。intmain(intargc,char*argv[]){argv=uv_setup_args(argc,argv);std::ofstream输出;out.open(argv[1],std::ios::out|std::ios::binary);node::per_process::enabled_debug_list.Parse(nullptr);std::unique_ptrplatform=v8::platform::NewDefaultPlatform();v8::V8::InitializePlatform(platform.get());v8::V8::初始化();隔离::CreateParamscreate_params;create_params.array_buffer_allocator_shared.reset(ArrayBuffer::Allocator::NewDefaultAllocator());隔离*isolate=Isolate::New(create_params);{Isolate::Scopeisolate_scope(isolate);v8::HandleScope处理范围(隔离);v8::Localcontext=v8::Context::New(isolate);v8::上下文::范围context_scope(context);std::string缓存=CodeCacheBuilder::Generate(上下文);出<<缓存;关闭();}隔离->处置();v8::V8::ShutdownPlatform();return0;}首先打开文件,然后是V8的常用初始化总编辑,最后通过生成生成代码打包存储。std::stringCodeCacheBuilder::Generate(Localcontext){NativeModuleLoader*loader=NativeModuleLoader::GetInstance();std::vectorids=loader->GetModuleIds();std::map数据;for(constauto&id:ids){if(loader->CanBeRequired(id.c_str())){NativeModuleLoader::Result结果;USE(loader->CompileAsModule(context,id.c_str(),&result));ScriptCompiler::CachedData*cached_data=loader->GetCodeCache(id.c_str());data.emplace(id,cached_data);}}returnGenerateCodeCache(data);}首先新建一个NativeModuleLoader。NativeModuleLoader::NativeModuleLoader():config_(GetConfig()){LoadJavaScriptSource();}当初始化NativeModuleLoader时,会执行LoadJavaScriptSource。该函数是python脚本生成的node_javascript.cc文件中的函数。初始化完成后,NativeModuleLoader对象的source_字段保存了原生JS模块的代码。然后遍历这些原生的JS模块,通过CompileAsModule进行编译。MaybeLocalNativeModuleLoader::CompileAsModule(Localcontext,constchar*id,NativeModuleLoader::Result*result){Isolate*isolate=context->GetIsolate();}std::vector>parameters={FIXED_ONE_BYTE_STRING(隔离,“导出”),FIXED_ONE_BYTE_STRING(隔离,“要求”),FIXED_ONE_BYTE_STRING(隔离,“模块”),FIXED_ONE_BYTE_STRING(隔离,“处理”),FIXED_ONE_BYTE_STRING(隔离,“internalBinding”),FIXED_ONE_BYTE_STRING(隔离,“primordials”)};returnLookupAndCompile(context,id,¶meters,result);}接着看LookupAndCompileMaybeLocalNativeModuleLoader::LookupAndCompile(Localcontext,constchar*id,std::vector>*parameters,NativeModuleLoader::Result*result){隔离*isolate=context->GetIsolate();EscapableHandleScope范围(隔离);本地来源;//根据key从source_字段中查找模块内容if(!LoadBuiltinModuleSource(isolate,id).ToLocal(&source)){return{};}std::stringfilename_s=std::string("node:")+id;Localfilename=OneByteString(isolate,filename_s.c_str(),filename_s.size());ScriptOrigin原点(隔离,文件名,0、0,真);ScriptCompiler::CachedData*cached_data=nullptr;{Mutex::ScopedLock锁(code_cache_mutex_);//判断是否有代码缓存autocache_it=code_cache_.find(id);if(cache_it!=code_cache_.end()){cached_data=cache_it->second.release();代码缓存_。擦除(缓存它);}}constboolhas_cache=cached_data!=nullptr;ScriptCompiler::CompileOptions选项=has_cache?脚本编译器::kConsumeCodeCache:脚本编译器::kEagerCompile;//如果有代码缓存,传入ScriptCompiler::Sourcescript_source(source,origin,cached_data);//编译MaybeLocalmaybe_fun=ScriptCompiler::CompileFunctionInContext(context,&script_source,parameters->size(),parameters->data(),0,nullptr,options);本地有趣;如果(!maybe_fun.ToLocal(&fun)){returnMaybeLocal();}*result=(has_cache&&!script_source.GetCachedData()->rejected)?结果::kWithCache:结果::kWithoutCache;//生成代码缓存并保存,最后写入文件,下次使用std::unique_ptrnew_cached_data(ScriptCompiler::CreateCodeCacheForFunction(fun));{Mutex::ScopedLock锁(code_cache_mutex_);code_cache_.emplace(id,std::move(new_cached_data));}returnscope.Escape(fun);}首先第一次执行时,即编译Node.js时,LookupAndCompile会生成一个代码缓存并写入到文件node_code_cache.cc中,编译成可执行文件。发布。在Node.js第一次执行的初始化阶段,会执行上面的函数,每个模块和对应的代码缓存都会保存在code_cache字段中。初始化完成后,稍后加载原生JS模块时,Node.js会再次执行LookupAndCompile,代码会立即被缓存。开启代码缓存后,我电脑上Node.js的启动时间大概是40毫秒。去除代码缓存的逻辑重新编译后,Node.js的启动时间约为60毫秒,速度有了很大的提升。总结:Node.js在编译时先将原生JS模块的代码写入文件,然后执行mkcodecache.cc编译原生JS模块并获取对应的代码缓存,然后写入文件,编译成节点。js的可执行文件,在Node.js初始化的时候会收集起来,以便后续加载原生JS模块时,可以使用这些代码缓存来加速代码执行。