Node.js C++ addon生成JsObject如何达到与纯JS同等的运行速度
性能慢的核心原因
你当前C++代码性能远低于纯JS,90%以上的开销来自不必要的重复冗余操作,V8隐藏类的影响是次要因素,具体原因如下:
- 循环内重复获取body数组:每次循环都执行
node.Get("body").As<Napi::Array>(),该操作需要遍历对象属性哈希表,200万次循环就产生了200万次多余的哈希查找开销,而纯JS实现里已经把body数组的引用提到了循环外,只获取一次。 - 循环内多余的字符串转换:你每次都调用
body.Length()计算长度,再把数字转成std::string,再转成Napi字符串作为数组的键,而纯JS的Array.push直接使用内部索引操作,不需要任何字符串转换和属性查找,200万次字符串的创建、销毁开销非常高。 - 跨层调用开销:Napi所有接口都是V8暴露的跨层调用,每一次
New、Set操作都有C和JS上下文切换的成本,而纯JS的对象操作会被V8 JIT编译为直接操作内存的机器码,没有跨层开销,本身就比C侧调用V8接口创建JS对象更快。
和V8隐藏类的关系
有一定关联,但不是当前性能问题的主因。纯JS里反复创建结构完全相同的对象,V8会自动复用同一个隐藏类,属性访问直接走内存偏移量寻址,速度极快。你当前C++代码里创建的stmt对象结构固定,理论上也能复用隐藏类,但前面的冗余操作开销已经完全盖过了隐藏类带来的收益。
优化方案
按下面的点修改代码,性能可以降到300ms以内,和纯JS实现的差距非常小:
- 把固定属性名、body数组引用全部提到循环外,避免重复创建和查找
- 数组直接用数字索引赋值,不要转成字符串键
- 提前预分配数组空间,避免运行时数组动态扩容的开销
- 进阶可以用对象模板预定义对象结构,提前固定隐藏类,进一步降低对象创建开销
优化后代码
#include <napi.h> Napi::Value GetBigObject(const Napi::CallbackInfo& info) { Napi::Env m_env = info.Env(); // 提前创建固定属性名,避免循环内重复生成 Napi::String foo_key = Napi::String::New(m_env, "foo"); Napi::String body_key = Napi::String::New(m_env, "body"); Napi::Object node = Napi::Object::New(m_env); node.Set(foo_key, Napi::Number::New(m_env, 1)); // 直接创建body数组并存住引用,不需要后面反复获取 Napi::Array body = Napi::Array::New(m_env); node.Set(body_key, body); const int loop_count = 2000000; // 预分配数组空间,避免动态扩容 body.Reserve(loop_count); for (uint32_t i = 0; i < loop_count; i++) { Napi::Object stmt = Napi::Object::New(m_env); stmt.Set(foo_key, Napi::Number::New(m_env, 1)); // 直接用数字索引设置数组元素,不需要转字符串 body.Set(i, stmt); } return node; } Napi::Object Init(Napi::Env env, Napi::Object exports) { exports.Set(Napi::String::New(env, "getBigObject"), Napi::Function::New(env, GetBigObject)); return exports; } NODE_API_MODULE(addon, Init)
如果还需要进一步优化,可以在模块初始化时提前创建对象模板,预定义stmt的结构,避免运行时隐藏类迁移,性能还能提升10%~20%:
// 静态存储对象模板的持久引用 static Napi::ObjectReference stmtTplRef; Napi::Value GetBigObject(const Napi::CallbackInfo& info) { Napi::Env m_env = info.Env(); Napi::String body_key = Napi::String::New(m_env, "body"); Napi::Object node = Napi::Object::New(m_env); node.Set(Napi::String::New(m_env, "foo"), Napi::Number::New(m_env, 1)); Napi::Array body = Napi::Array::New(m_env); node.Set(body_key, body); const int loop_count = 2000000; body.Reserve(loop_count); for (uint32_t i = 0; i < loop_count; i++) { // 直接用预定义的模板创建对象,不需要再Set foo属性 Napi::Object stmt = stmtTplRef.NewInstance(); body.Set(i, stmt); } return node; } Napi::Object Init(Napi::Env env, Napi::Object exports) { // 模块初始化时创建对象模板,固定结构 Napi::ObjectTemplate stmtTpl = Napi::ObjectTemplate::New(env); stmtTpl.Set("foo", Napi::Number::New(env, 1)); stmtTplRef = Napi::Persistent(stmtTpl); exports.Set(Napi::String::New(env, "getBigObject"), Napi::Function::New(env, GetBigObject)); return exports; } NODE_API_MODULE(addon, Init)
内容的提问来源于stack exchange,提问作者Sugar
相关产品推荐
相关产品推荐

