原生引擎跨语言调用优化

前言

在 Cocos Creator 3.6.0 版本的原生实现上,我们提高了原生(CPP)层级,主要体现在节点树(Scene、Node)、资产(Asset 及其子类)、材质系统(Material、Pass、ProgramLib)、3D 渲染器 (包含 Model、SubModel)、2D 渲染器(Batch2D、RenderEntity)都在 CPP 中实现,并通过绑定技术暴露给 JS 层使用。

众所周知,在 Cocos Creator 3.x 中,开发者只能使用 TS 脚本开发游戏业务逻辑。尽管我们将更多的引擎代码在 CPP 中实现,但是开发者并无法直接使用 CPP 语言进行游戏业务逻辑的开发,因此我们通过脚本引擎封装层(Script Engine Wrapper)的 API,简称 SE API,将这些 CPP 层实现的类型绑定并暴露给 JS 中,暴露到 JS 中的接口保持跟 Web 环境中一致。

原生层级的上移最直接的好处就是:在原生平台上引擎代码的执行性能得到提升,特别是不支持 JIT 的平台(比如:iOS)尤为明显。但在 3.6.0 正式版发布前,我们也面临了一系列由于原生层级提升带来的副作用,最主要的方面就是 JSB 调用(JS <-> CPP 语言之间的交互)次数比之前版本多了不少,而这直接导致原生层级提升带来的收益被抵消甚至性能相比之前版本( 3.5 )变得更差。本文将介绍一些降低 JSB 调用的优化方式,若开发者自己的 CPP 代码中也存在类似的 JSB 调用过多的问题,希望本文也能提供一些优化思路。

共享内存

对 Node 中需要频繁同步的属性,我们使用 CPP 与 JS 共享内存的方式,避免 JSB 调用。为了更方便地共享 CPP 的内存到 JS 中,我们封装了 bindings::NativeMemorySharedToScriptActor 辅助类。

bindings/utils/BindingUtils.h

  1. namespace cc::bindings {
  2. class NativeMemorySharedToScriptActor final {
  3. public:
  4. NativeMemorySharedToScriptActor() = default;
  5. ~NativeMemorySharedToScriptActor();
  6. void initialize(void *ptr, uint32_t byteLength);
  7. void destroy();
  8. inline se::Object *getSharedArrayBufferObject() const { return _sharedArrayBufferObject; }
  9. private:
  10. se::Object *_sharedArrayBufferObject{nullptr};
  11. CC_DISALLOW_COPY_MOVE_ASSIGN(NativeMemorySharedToScriptActor)
  12. };
  13. } // namespace cc::bindings

bindings/utils/BindingUtils.h

  1. #include "bindings/utils/BindingUtils.h"
  2. #include "bindings/jswrapper/SeApi.h"
  3. namespace cc::bindings {
  4. NativeMemorySharedToScriptActor::~NativeMemorySharedToScriptActor() {
  5. destroy();
  6. }
  7. void NativeMemorySharedToScriptActor::initialize(void* ptr, uint32_t byteLength) {
  8. CC_ASSERT_NULL(_sharedArrayBufferObject);
  9. // The callback of freeing buffer is empty since the memory is managed in native,
  10. // the external array buffer just holds a reference to the memory.
  11. _sharedArrayBufferObject = se::Object::createExternalArrayBufferObject(ptr, byteLength, [](void* /*contents*/, size_t /*byteLength*/, void* /*userData*/) {});
  12. // Root 此对象,防止对象被 GC,当 Actor 对象销毁的时候会调用 destroy 函数,其内部会 unroot ArrayBuffer 对象
  13. _sharedArrayBufferObject->root();
  14. }
  15. void NativeMemorySharedToScriptActor::destroy() {
  16. if (_sharedArrayBufferObject != nullptr) {
  17. _sharedArrayBufferObject->unroot();
  18. _sharedArrayBufferObject->decRef();
  19. _sharedArrayBufferObject = nullptr;
  20. }
  21. }
  22. } // namespace cc::bindings

bindings/jswrapper/v8/Object.h

  1. using BufferContentsFreeFunc = void (*)(void *contents, size_t byteLength, void *userData);
  2. static Object *createExternalArrayBufferObject(void *contents, size_t byteLength, BufferContentsFreeFunc freeFunc, void *freeUserData = nullptr);

bindings/jswrapper/v8/Object.cpp

  1. /* static */
  2. Object *Object::createExternalArrayBufferObject(void *contents, size_t byteLength, BufferContentsFreeFunc freeFunc, void *freeUserData /* = nullptr*/) {
  3. Object *obj = nullptr;
  4. std::shared_ptr<v8::BackingStore> backingStore = v8::ArrayBuffer::NewBackingStore(contents, byteLength, freeFunc, freeUserData);
  5. v8::Local<v8::ArrayBuffer> jsobj = v8::ArrayBuffer::New(__isolate, backingStore);
  6. if (!jsobj.IsEmpty()) {
  7. obj = Object::_createJSObject(nullptr, jsobj);
  8. }
  9. return obj;
  10. }

分析以上代码可知,其实 NativeMemorySharedToScriptActor 最终是调用 v8::ArrayBuffer::NewBackingStore 和 v8::ArrayBuffer::New 函数创建了一个 External 类型的 ArrayBuffer,取名为 External 的原因是其内存不在 V8 内部分配和管理,即其内存完全交由 V8 的上层管理,当 ArrayBuffer 对象被 GC 的后,freeFunc 回调函数会被触发。由于在 Node 中需要共享的内存是 Node 中的若干连续属性,内存的创建和释放完全交由 Node 自己维护,而 CPP Node 实例的销毁也是由 GC 控制的,因此 NativeMemorySharedToScriptActor::initialize 内部调用 se::Object::createExternalArrayBufferObject 的时候,传递了一个空实现的回调函数。

Node.h

  1. class Node : public CCObject {
  2. ......
  3. inline se::Object *_getSharedArrayBufferObject() const { return _sharedMemoryActor.getSharedArrayBufferObject(); } // NOLINT
  4. ......
  5. bindings::NativeMemorySharedToScriptActor _sharedMemoryActor;
  6. ......
  7. // Shared memory with JS
  8. // NOTE: TypeArray created in node.jsb.ts _ctor should have the same memory layout
  9. uint32_t _eventMask{0}; // Uint32: 0
  10. uint32_t _layer{static_cast<uint32_t>(Layers::LayerList::DEFAULT)}; // Uint32: 1
  11. uint32_t _transformFlags{0}; // Uint32: 2
  12. index_t _siblingIndex{0}; // Int32: 0
  13. uint8_t _activeInHierarchy{0}; // Uint8: 0
  14. uint8_t _active{1}; // Uint8: 1
  15. uint8_t _isStatic{0}; // Uint8: 2
  16. uint8_t _padding{0}; // Uint8: 3
  17. ......
  18. };

Node.cpp

  1. Node::Node(const ccstd::string &name) {
  2. #define NODE_SHARED_MEMORY_BYTE_LENGTH (20)
  3. static_assert(offsetof(Node, _padding) + sizeof(_padding) - offsetof(Node, _eventMask) == NODE_SHARED_MEMORY_BYTE_LENGTH, "Wrong shared memory size");
  4. _sharedMemoryActor.initialize(&_eventMask, NODE_SHARED_MEMORY_BYTE_LENGTH);
  5. #undef NODE_SHARED_MEMORY_BYTE_LENGTH
  6. _id = idGenerator.getNewId();
  7. if (name.empty()) {
  8. _name.append("New Node");
  9. } else {
  10. _name = name;
  11. }
  12. }

Node 构造函数中调用 _sharedMemoryActor.initialize(&_eventMask, NODE_SHARED_MEMORY_BYTE_LENGTH); 将 _eventMask 属性开始的 20 字节设置为共享内存。

node.jsb.ts

注意:所有的 .jsb.ts 结尾的文件最终在打包的时候会替换对应不带 .jsb 的文件,比如这里 node.jsb.ts 会替换掉 node.ts,具体可以查看引擎根目录下的 cc.config.json 文件,有对应的 overrides 字段:“cocos/scene-graph/node.ts”: “cocos/scene-graph/node.jsb.ts”,

  1. // JS 的 _ctor 回调函数会在 JS 的 var node = new Node(); 流程的最后阶段被触发,即 CPP Node 对象创建之后,因此 _getSharedArrayBufferObject 绑定函数返回的 ArrayBuffer 一定存在。
  2. nodeProto._ctor = function (name?: string) {
  3. ......
  4. // 通过 _getSharedArrayBufferObject 绑定方法,取得 CPP 共享给 JS 的 ArrayBuffer 对象
  5. const sharedArrayBuffer = this._getSharedArrayBufferObject();
  6. // Uint32Array with 3 elements, offset from the start: eventMask, layer, dirtyFlags
  7. this._sharedUint32Arr = new Uint32Array(sharedArrayBuffer, 0, 3);
  8. // Int32Array with 1 element, offset from 12th bytes: siblingIndex
  9. this._sharedInt32Arr = new Int32Array(sharedArrayBuffer, 12, 1);
  10. // Uint8Array with 3 elements, offset from 16th bytes: activeInHierarchy, active, static
  11. this._sharedUint8Arr = new Uint8Array(sharedArrayBuffer, 16, 3);
  12. //
  13. this._sharedUint32Arr[1] = Layers.Enum.DEFAULT; // this._sharedUint32Arr[1] is layer
  14. ......
  15. };

采用共享内存的方式,也意味着我们无法通过 JSB 绑定函数的方式把要设置的值从 JS 传递到 CPP 中。因此我们需要在 .jsb.ts 中定义对应的 getter / setter 函数,其内部通过直接操作 TypedArray 的方式修改共享内存。

  1. Object.defineProperty(nodeProto, 'activeInHierarchy', {
  2. configurable: true,
  3. enumerable: true,
  4. get (): Readonly<Boolean> {
  5. return this._sharedUint8Arr[0] != 0; // Uint8, 0: activeInHierarchy
  6. },
  7. set (v) {
  8. this._sharedUint8Arr[0] = (v ? 1 : 0); // Uint8, 0: activeInHierarchy
  9. },
  10. });
  11. Object.defineProperty(nodeProto, '_activeInHierarchy', {
  12. configurable: true,
  13. enumerable: true,
  14. get (): Readonly<Boolean> {
  15. return this._sharedUint8Arr[0] != 0; // Uint8, 0: activeInHierarchy
  16. },
  17. set (v) {
  18. this._sharedUint8Arr[0] = (v ? 1 : 0); // Uint8, 0: activeInHierarchy
  19. },
  20. });
  21. Object.defineProperty(nodeProto, 'layer', {
  22. configurable: true,
  23. enumerable: true,
  24. get () {
  25. return this._sharedUint32Arr[1]; // Uint32, 1: layer
  26. },
  27. set (v) {
  28. this._sharedUint32Arr[1] = v; // Uint32, 1: layer
  29. if (this._uiProps && this._uiProps.uiComp) {
  30. this._uiProps.uiComp.setNodeDirty();
  31. this._uiProps.uiComp.markForUpdateRenderData();
  32. }
  33. this.emit(NodeEventType.LAYER_CHANGED, v);
  34. },
  35. });
  36. Object.defineProperty(nodeProto, '_layer', {
  37. configurable: true,
  38. enumerable: true,
  39. get () {
  40. return this._sharedUint32Arr[1]; // Uint32, 1: layer
  41. },
  42. set (v) {
  43. this._sharedUint32Arr[1] = v; // Uint32, 1: layer
  44. },
  45. });
  46. Object.defineProperty(nodeProto, '_eventMask', {
  47. configurable: true,
  48. enumerable: true,
  49. get () {
  50. return this._sharedUint32Arr[0]; // Uint32, 0: eventMask
  51. },
  52. set (v) {
  53. this._sharedUint32Arr[0] = v; // Uint32, 0: eventMask
  54. },
  55. });
  56. Object.defineProperty(nodeProto, '_siblingIndex', {
  57. configurable: true,
  58. enumerable: true,
  59. get () {
  60. return this._sharedInt32Arr[0]; // Int32, 0: siblingIndex
  61. },
  62. set (v) {
  63. this._sharedInt32Arr[0] = v; // Int32, 0: siblingIndex
  64. },
  65. });
  66. nodeProto.getSiblingIndex = function getSiblingIndex() {
  67. return this._sharedInt32Arr[0]; // Int32, 0: siblingIndex
  68. };
  69. Object.defineProperty(nodeProto, '_transformFlags', {
  70. configurable: true,
  71. enumerable: true,
  72. get () {
  73. return this._sharedUint32Arr[2]; // Uint32, 2: _transformFlags
  74. },
  75. set (v) {
  76. this._sharedUint32Arr[2] = v; // Uint32, 2: _transformFlags
  77. },
  78. });
  79. Object.defineProperty(nodeProto, '_active', {
  80. configurable: true,
  81. enumerable: true,
  82. get (): Readonly<Boolean> {
  83. return this._sharedUint8Arr[1] != 0; // Uint8, 1: active
  84. },
  85. set (v) {
  86. this._sharedUint8Arr[1] = (v ? 1 : 0); // Uint8, 1: active
  87. },
  88. });
  89. Object.defineProperty(nodeProto, 'active', {
  90. configurable: true,
  91. enumerable: true,
  92. get (): Readonly<Boolean> {
  93. return this._sharedUint8Arr[1] != 0; // Uint8, 1: active
  94. },
  95. set (v) {
  96. this.setActive(!!v);
  97. },
  98. });
  99. Object.defineProperty(nodeProto, '_static', {
  100. configurable: true,
  101. enumerable: true,
  102. get (): Readonly<Boolean> {
  103. return this._sharedUint8Arr[2] != 0;
  104. },
  105. set (v) {
  106. this._sharedUint8Arr[2] = (v ? 1 : 0);
  107. },
  108. });

性能对比

原生引擎跨语言调用优化 - 图1

避免接口传参

如果 JSB 函数调用中包含参数,V8 内部需要对参数的合理性做校验,这些校验工作也会影响调用性能。针对 Node 中可能会高频调用的 JSB 函数,我们通过复用一个全局的 Float32Array 来避免浮点类型的参数传递。

scene-graph/utils.jsb.ts

  1. import { IMat4Like, Mat4 } from '../core/math';
  2. declare const jsb: any;
  3. // For optimize getPosition, getRotation, getScale
  4. export const _tempFloatArray = new Float32Array(jsb.createExternalArrayBuffer(20 * 4));
  5. export const fillMat4WithTempFloatArray = function fillMat4WithTempFloatArray (out: IMat4Like) {
  6. Mat4.set(out,
  7. _tempFloatArray[0], _tempFloatArray[1], _tempFloatArray[2], _tempFloatArray[3],
  8. _tempFloatArray[4], _tempFloatArray[5], _tempFloatArray[6], _tempFloatArray[7],
  9. _tempFloatArray[8], _tempFloatArray[9], _tempFloatArray[10], _tempFloatArray[11],
  10. _tempFloatArray[12], _tempFloatArray[13], _tempFloatArray[14], _tempFloatArray[15]
  11. );
  12. };
  13. //

以上定义了一个全局的 _tempFloatArray ,用于存储 number 或者 number 的复合类型(Vec3/Vec4/Mat4等)参数。

node.jsb.ts

  1. // ......
  2. // 将 FloatArray 设置给 CPP 层
  3. Node._setTempFloatArray(_tempFloatArray.buffer);
  4. // ......
  5. // 在 JS 中重新实现带有参数 setPosition 函数
  6. nodeProto.setPosition = function setPosition (val: Readonly<Vec3> | number, y?: number, z?: number) {
  7. if (y === undefined && z === undefined) {
  8. // 当 y 和 z 都是 undefined 的时候,表示第一个参数为 Vec3 类型的对象,_tempFloatArray 的第一个元素表示参数个数
  9. _tempFloatArray[0] = 3;
  10. const pos = val as Vec3;
  11. // 将新的 pos 赋值到 FloatArray 数组和 this._lpos 缓存中
  12. this._lpos.x = _tempFloatArray[1] = pos.x;
  13. this._lpos.y = _tempFloatArray[2] = pos.y;
  14. this._lpos.z = _tempFloatArray[3] = pos.z;
  15. } else if (z === undefined) {
  16. // 如果 z 为 undefined,那么只有 x 与 y 2 个参数
  17. _tempFloatArray[0] = 2;
  18. this._lpos.x = _tempFloatArray[1] = val as number;
  19. this._lpos.y = _tempFloatArray[2] = y as number;
  20. } else {
  21. _tempFloatArray[0] = 3;
  22. this._lpos.x = _tempFloatArray[1] = val as number;
  23. this._lpos.y = _tempFloatArray[2] = y as number;
  24. this._lpos.z = _tempFloatArray[3] = z as number;
  25. }
  26. this._setPosition(); // 此为原生绑定出来的无参的函数
  27. };

jsb_scene_manual.cpp

  1. namespace {
  2. /**
  3. * 对共享的全局 FloatArray 操作的辅助类
  4. */
  5. class TempFloatArray final {
  6. public:
  7. TempFloatArray() = default;
  8. ~TempFloatArray() = default;
  9. inline void setData(float *data) { _data = data; }
  10. ......
  11. inline const float &operator[](size_t index) const { return _data[index]; }
  12. inline float &operator[](size_t index) { return _data[index]; }
  13. private:
  14. float *_data{nullptr};
  15. CC_DISALLOW_ASSIGN(TempFloatArray)
  16. };
  17. TempFloatArray tempFloatArray;
  18. } // namespace
  1. static bool js_scene_Node_setTempFloatArray(se::State &s) // NOLINT(readability-identifier-naming)
  2. {
  3. const auto &args = s.args();
  4. size_t argc = args.size();
  5. CC_UNUSED bool ok = true;
  6. if (argc == 1) {
  7. uint8_t *buffer = nullptr;
  8. args[0].toObject()->getArrayBufferData(&buffer, nullptr);
  9. // 初始化 TempFloatArray 关联的数据
  10. tempFloatArray.setData(reinterpret_cast<float *>(buffer));
  11. return true;
  12. }
  13. SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", (int)argc, 1);
  14. return false;
  15. }
  16. SE_BIND_FUNC(js_scene_Node_setTempFloatArray)
  1. bool register_all_scene_manual(se::Object *obj) // NOLINT(readability-identifier-naming)
  2. {
  3. ......
  4. __jsb_cc_Node_proto->defineFunction("_setPosition", _SE(js_scene_Node_setPosition));
  5. __jsb_cc_Node_proto->defineFunction("_setScale", _SE(js_scene_Node_setScale));
  6. __jsb_cc_Node_proto->defineFunction("_setRotation", _SE(js_scene_Node_setRotation));
  7. __jsb_cc_Node_proto->defineFunction("_setRotationFromEuler", _SE(js_scene_Node_setRotationFromEuler));
  8. __jsb_cc_Node_proto->defineFunction("_rotateForJS", _SE(js_scene_Node_rotateForJS));
  9. ......
  10. }
  1. // 无参的 node._setPosition() 绑定
  2. static bool js_scene_Node_setPosition(void *s) // NOLINT(readability-identifier-naming)
  3. {
  4. auto *cobj = reinterpret_cast<cc::Node *>(s);
  5. auto argc = static_cast<size_t>(tempFloatArray[0]);
  6. if (argc == 2) {
  7. // 从 tempFloatArray 中获取参数
  8. cobj->setPositionInternal(tempFloatArray[1], tempFloatArray[2], true);
  9. } else {
  10. cobj->setPositionInternal(tempFloatArray[1], tempFloatArray[2], tempFloatArray[3], true);
  11. }
  12. return true;
  13. }
  14. SE_BIND_FUNC_FAST(js_scene_Node_setPosition) // 注意,这里使用了新的 SE_BIND_FUNC_FAST 宏

Node.h

  1. inline void setPositionInternal(float x, float y, bool calledFromJS) { setPositionInternal(x, y, _localPosition.z, calledFromJS); }
  2. void setPositionInternal(float x, float y, float z, bool calledFromJS);

bindings/jswrapper/v8/HelperMacros.h

  1. #define SE_BIND_FUNC(funcName) \
  2. void funcName##Registry(const v8::FunctionCallbackInfo<v8::Value> &_v8args) { \
  3. JsbInvokeScope(#funcName); \
  4. jsbFunctionWrapper(_v8args, funcName, #funcName); \
  5. }
  6. #define SE_BIND_FUNC_FAST(funcName) \
  7. void funcName##Registry(const v8::FunctionCallbackInfo<v8::Value> &_v8args) { \
  8. auto *thisObject = static_cast<se::Object *>(_v8args.This()->GetAlignedPointerFromInternalField(0)); \
  9. auto *nativeObject = thisObject != nullptr ? thisObject->getPrivateData() : nullptr; \
  10. funcName(nativeObject); \
  11. }

bindings/jswrapper/v8/HelperMacros.cpp

  1. // SE_BIND_FUNC 宏会调用 jsbFunctionWrapper 函数,其内部做了更多的事情
  2. SE_HOT void jsbFunctionWrapper(const v8::FunctionCallbackInfo<v8::Value> &v8args, se_function_ptr func, const char *funcName) {
  3. bool ret = false;
  4. v8::Isolate *isolate = v8args.GetIsolate();
  5. v8::HandleScope scope(isolate);
  6. bool needDeleteValueArray{false};
  7. se::ValueArray &args = se::gValueArrayPool.get(v8args.Length(), needDeleteValueArray);
  8. se::CallbackDepthGuard depthGuard{args, se::gValueArrayPool._depth, needDeleteValueArray};
  9. se::internal::jsToSeArgs(v8args, args);
  10. se::Object *thisObject = se::internal::getPrivate(isolate, v8args.This());
  11. se::State state(thisObject, args);
  12. ret = func(state);
  13. if (!ret) {
  14. SE_LOGE("[ERROR] Failed to invoke %s\n", funcName);
  15. }
  16. se::internal::setReturnValue(state.rval(), v8args);
  17. }

SE_BIND_FUNC_FAST 宏的内部实现非常快,因为其内部非常简单,拿到 private data 后立马出发回调函数,没有任何 se 相关的 API 调用。这避免了 se::Value 与 jsvalue 之间的转换,也避免了 V8 内部对参数进行校验。大家可以对比一下标准的 SE_BIND_FUNC 宏的内部实现。

另外,你可能会好奇,为什么直接不通过「共享内存」章节的方式来共享 position、rotation、scale 信息,而要通过 _setPosition()_setRotation()_setScale() 这种无参的 JSB 方法调用。原因是,这里的这个 JSB 调用无法被优化掉,我们来看 Node::setPosition 的实现:

  1. class Node : public CCObject {
  2. ......
  3. inline void setPosition(const Vec3 &pos) { setPosition(pos.x, pos.y, pos.z); }
  4. inline void setPosition(float x, float y) { setPosition(x, y, _localPosition.z); }
  5. inline void setPosition(float x, float y, float z) { setPositionInternal(x, y, z, false); }
  6. inline void setPositionInternal(float x, float y, bool calledFromJS) { setPositionInternal(x, y, _localPosition.z, calledFromJS); }
  7. void setPositionInternal(float x, float y, float z, bool calledFromJS);
  8. ......
  9. inline uint32_t getChangedFlags() const {
  10. return _hasChangedFlagsVersion == globalFlagChangeVersion ? _hasChangedFlags : 0;
  11. }
  12. inline void setChangedFlags(uint32_t value) {
  13. _hasChangedFlagsVersion = globalFlagChangeVersion;
  14. _hasChangedFlags = value;
  15. }
  16. ......
  17. };
  1. void Node::setPositionInternal(float x, float y, float z, bool calledFromJS) {
  2. _localPosition.set(x, y, z);
  3. invalidateChildren(TransformBit::POSITION);
  4. if (_eventMask & TRANSFORM_ON) {
  5. emit<TransformChanged>(TransformBit::POSITION);
  6. }
  7. if (!calledFromJS) {
  8. notifyLocalPositionUpdated();
  9. }
  10. }

setPosition 不止对 _localPostion 赋值,其还需要触发 invalidateChildren 函数的调用,invalidateChildren 是个递归函数,其内部还会遍历所有子节点,进而修改一些其它属性,比如 _transformFlags, _hasChangedFlagsVersion, _hasChangedFlags。因此,我们无法通过第一章节中「共享内存」的方式来优化 setPosition。

性能对比

原生引擎跨语言调用优化 - 图2

缓存属性

在 JS 层中缓存属性,避免 getter 访问 c++ 接口,也能减少 JSB 调用。

node.jsb.ts

  1. Object.defineProperty(nodeProto, 'position', {
  2. configurable: true,
  3. enumerable: true,
  4. get (): Readonly<Vec3> {
  5. return this._lpos;
  6. },
  7. set (v: Readonly<Vec3>) {
  8. this.setPosition(v as Vec3);
  9. },
  10. });
  11. nodeProto.getPosition = function getPosition (out?: Vec3): Vec3 {
  12. if (out) {
  13. return Vec3.set(out, this._lpos.x, this._lpos.y, this._lpos.z);
  14. }
  15. return Vec3.copy(new Vec3(), this._lpos);
  16. };
  17. nodeProto._ctor = function (name?: string) {
  18. ......
  19. this._lpos = new Vec3();
  20. this._lrot = new Quat();
  21. this._lscale = new Vec3(1, 1, 1);
  22. this._euler = new Vec3();
  23. .......
  24. };

性能对比

原生引擎跨语言调用优化 - 图3

节点同步

用户的逻辑代码中,也经常会这样使用:

  1. const children = node.children;
  2. for (let i = 0; i < children.length; ++i) {
  3. const child = children[i];
  4. // do something with child
  5. }

.children getter 可能会被频繁调用,这也导致 JSB 调用过多。children 的 getter 绑定比较特殊,因为它是一个 JS array,在 CPP 中,它对应的是 ccstd::vector<Node*> _children;类型,因此 JS Array 与 CPP 的 std::vector 并没有很好的方式进行数据同步。如果每次调用 .children getter 都是 JSB 的方式返回的话,那么每次调用都会通过 se::Object::createArrayObject 生成一个临时的 JS Array ,并通过 nativevalue_to_se 依次转换 CPP 的 child 为 JS 的 child 并赋值到 JS Array 中,这算是比较重度的转换开销,而且会生成需要被 GC 的临时数组,给 GC 造成一定的额外压力。

为了解决此问题,我们在 JS 层中缓存了 _children 属性,当 CPP 层中有更改 _children 时候,通过监听 ChildAdded, ChildRemoved 事件的方式通知 JS 层中的 _children 更新内容。

以下以 node.addChild(child); 为例阐述:

  1. class Node : public CCObject {
  2. ......
  3. inline void addChild(Node *node) { node->setParent(this); }
  4. inline void removeChild(Node *node) const {
  5. auto idx = getIdxOfChild(_children, node);
  6. if (idx != -1) {
  7. node->setParent(nullptr);
  8. }
  9. }
  10. inline void removeFromParent() {
  11. if (_parent) {
  12. _parent->removeChild(this);
  13. }
  14. }
  15. void removeAllChildren();
  16. ......
  17. };
  1. void Node::setParent(Node *parent, bool isKeepWorld /* = false */) {
  2. ......
  3. onSetParent(oldParent, isKeepWorld);
  4. emit<ParentChanged>(oldParent);
  5. if (oldParent) {
  6. if (!(oldParent->_objFlags & Flags::DESTROYING)) {
  7. index_t removeAt = getIdxOfChild(oldParent->_children, this);
  8. if (removeAt < 0) {
  9. return;
  10. }
  11. // 将 child 从旧的父节点中移除
  12. oldParent->_children.erase(oldParent->_children.begin() + removeAt);
  13. oldParent->updateSiblingIndex();
  14. oldParent->emit<ChildRemoved>(this);
  15. }
  16. }
  17. if (newParent) {
  18. ......
  19. // 将 child 添加到新的父节点中
  20. newParent->_children.emplace_back(this);
  21. _siblingIndex = static_cast<index_t>(newParent->_children.size() - 1);
  22. newParent->emit<ChildAdded>(this);
  23. }
  24. onHierarchyChanged(oldParent);
  25. }

调用完 addChild 后,CPP 层中的 Node::_children 被修改后,会触发 emit<ChildAdded> 事件,Node 初始化的时候会设置监听此事件,见 jsb_scene_manual.cpp 中的代码:

  1. // jsb_scene_manual.cpp
  2. static void registerOnChildAdded(cc::Node *node, se::Object *jsObject) {
  3. node->on<cc::Node::ChildAdded>(
  4. [jsObject](cc::Node * /*emitter*/, cc::Node *child) {
  5. se::AutoHandleScope hs;
  6. se::Value arg0;
  7. nativevalue_to_se(child, arg0);
  8. // 调用 JS 的私有函数 _onChildAdded
  9. se::ScriptEngine::getInstance()->callFunction(jsObject, "_onChildAdded", 1, &arg0);
  10. });
  11. }
  12. static bool js_scene_Node_registerOnChildAdded(se::State &s) // NOLINT(readability-identifier-naming)
  13. {
  14. auto *cobj = SE_THIS_OBJECT<cc::Node>(s);
  15. SE_PRECONDITION2(cobj, false, "Invalid Native Object");
  16. auto *jsObject = s.thisObject();
  17. registerOnChildAdded(cobj, jsObject);
  18. return true;
  19. }
  20. SE_BIND_FUNC(js_scene_Node_registerOnChildAdded) // NOLINT(readability-identifier-naming)
  21. bool register_all_scene_manual(se::Object *obj) // NOLINT(readability-identifier-naming)
  22. {
  23. ......
  24. __jsb_cc_Node_proto->defineFunction("_registerOnChildAdded", _SE(js_scene_Node_registerOnChildAdded));
  25. ......
  26. }

node.jsb.ts

  1. Object.defineProperty(nodeProto, 'children', {
  2. configurable: true,
  3. enumerable: true,
  4. get () {
  5. return this._children;
  6. },
  7. set (v) {
  8. this._children = v;
  9. },
  10. });
  11. nodeProto._onChildRemoved = function (child) {
  12. this.emit(NodeEventType.CHILD_REMOVED, child);
  13. };
  14. // 此函数会被 CPP 层 registerOnChildAdded 函数中调用
  15. nodeProto._onChildAdded = function (child) {
  16. this.emit(NodeEventType.CHILD_ADDED, child);
  17. };
  18. nodeProto.on = function (type, callback, target, useCapture: any = false) {
  19. switch (type) {
  20. ......
  21. case NodeEventType.CHILD_ADDED:
  22. if (!(this._registeredNodeEventTypeMask & REGISTERED_EVENT_MASK_CHILD_ADDED_CHANGED)) {
  23. this._registerOnChildAdded(); // 调用 JSB 方法注册监听
  24. this._registeredNodeEventTypeMask |= REGISTERED_EVENT_MASK_CHILD_ADDED_CHANGED;
  25. }
  26. break;
  27. ......
  28. default:
  29. break;
  30. }
  31. this._eventProcessor.on(type, callback, target, useCapture);
  32. };
  33. nodeProto._ctor = function (name?: string) {
  34. ......
  35. this._children = [];
  36. // 使用 on 接口监听 CHILD_ADDED 和 CHILD_REMOVED 事件
  37. this.on(NodeEventType.CHILD_ADDED, (child) => {
  38. // 同步 JS 层中的 this._children 数组
  39. this._children.push(child);
  40. });
  41. this.on(NodeEventType.CHILD_REMOVED, (child) => {
  42. const removeAt = this._children.indexOf(child);
  43. if (removeAt < 0) {
  44. errorID(1633);
  45. return;
  46. }
  47. this._children.splice(removeAt, 1);
  48. });
  49. ......
  50. };

性能对比

原生引擎跨语言调用优化 - 图4

参数数组对象池

截止 3.6.0,Cocos Creator 引擎还未实现高效的内存池,而引擎内部使用 se ( Script Engine Wrapper )在做 JS -> CPP 交互的时候,都需要生成临时的 se::ValueArray args(argCount);,se::ValueArray 类型是 ccstd::vector<se::Value>typedef, 因此会有大量临时内存的申请、释放,极大影响性能。在 3.6.0 之前的版本并没有暴露出此问题是因为原来的原生层级比较低,JSB 调用次数比较低,而 3.6.0 版本中,随着原生层级提升,JSB 调用变多,此问题变得愈发严重。

我们想到的解决方案是:使用对象池的方式复用 se::ValueArray。对象池(se::ValueArrayPool)的实现也比较简单,具体如下:

bindings/jswrapper/ValueArrayPool.h

  1. // 调用深度警卫类,用于使用完 se::ValueArray 后对其中的 se::Value 进行复位的操作
  2. // 如果 se::ValueArray 是 new 出来的,将会处理 delete 流程
  3. class CallbackDepthGuard final {
  4. public:
  5. CallbackDepthGuard(ValueArray &arr, uint32_t &depth, bool needDelete)
  6. : _arr(arr), _depth(depth), _needDelete(needDelete) {
  7. ++_depth;
  8. }
  9. ~CallbackDepthGuard() {
  10. --_depth;
  11. for (auto &e : _arr) {
  12. e.setUndefined();
  13. }
  14. if (_needDelete) {
  15. delete &_arr;
  16. }
  17. }
  18. private:
  19. ValueArray &_arr;
  20. uint32_t &_depth;
  21. const bool _needDelete{false};
  22. };
  23. class ValueArrayPool final {
  24. public:
  25. // 绑定函数的参数个数的上限为 20,如果有参数大于 20 个的情况,需要考虑重构函数参数
  26. static const uint32_t MAX_ARGS = 20;
  27. ValueArrayPool();
  28. ValueArray &get(uint32_t argc, bool &outNeedDelete);
  29. uint32_t _depth{0};
  30. private:
  31. void initPool(uint32_t index);
  32. ccstd::vector<ccstd::array<ValueArray, MAX_ARGS + 1>> _pools;
  33. };
  34. extern ValueArrayPool gValueArrayPool;

bindings/jswrapper/ValueArrayPool.cpp

  1. ValueArrayPool gValueArrayPool;
  2. // 定义最大深度为 5,如果超过此深度,则不使用对象池
  3. #define SE_DEFAULT_MAX_DEPTH (5)
  4. ValueArrayPool::ValueArrayPool() {
  5. _pools.resize(SE_DEFAULT_MAX_DEPTH);
  6. for (uint32_t i = 0; i < SE_DEFAULT_MAX_DEPTH; ++i) {
  7. initPool(i);
  8. }
  9. }
  10. ValueArray &ValueArrayPool::get(uint32_t argc, bool &outNeedDelete) {
  11. // 如果深度大于对象池的大小,直接 new ValueArray
  12. if (SE_UNLIKELY(_depth >= _pools.size())) {
  13. outNeedDelete = true;
  14. auto *ret = ccnew ValueArray();
  15. ret->resize(argc);
  16. return *ret;
  17. }
  18. outNeedDelete = false;
  19. CC_ASSERT_LE(argc, MAX_ARGS);
  20. // 从对象池中取出 ValueArray 对象
  21. auto &ret = _pools[_depth][argc];
  22. CC_ASSERT(ret.size() == argc);
  23. return ret;
  24. }
  25. // 初始化对象池
  26. void ValueArrayPool::initPool(uint32_t index) {
  27. auto &pool = _pools[index];
  28. uint32_t i = 0;
  29. for (auto &arr : pool) {
  30. arr.resize(i);
  31. ++i;
  32. }
  33. }

我们回头再来看下 jsbFunctionWrapper 的实现:

  1. SE_HOT void jsbFunctionWrapper(const v8::FunctionCallbackInfo<v8::Value> &v8args, se_function_ptr func, const char *funcName) {
  2. bool ret = false;
  3. v8::Isolate *isolate = v8args.GetIsolate();
  4. v8::HandleScope scope(isolate);
  5. /* 原来的代码
  6. se::ValueArray args;
  7. args.reserve(10);
  8. */
  9. // 新实现的优化代码------ 开始 -------
  10. bool needDeleteValueArray{false};
  11. // 从全局的 se::ValueArray 对象池中取出 se::ValueArray,注意:needDeleteValueArray 是输出参数
  12. se::ValueArray &args = se::gValueArrayPool.get(v8args.Length(), needDeleteValueArray);
  13. // 定义「调用深度警卫」变量 depthGuard,当此变量销毁的时候,会自动清理对象池
  14. se::CallbackDepthGuard depthGuard{args, se::gValueArrayPool._depth, needDeleteValueArray};
  15. // 新实现的优化代码------ 结束 -------
  16. se::internal::jsToSeArgs(v8args, args);
  17. se::Object *thisObject = se::internal::getPrivate(isolate, v8args.This());
  18. se::State state(thisObject, args);
  19. ret = func(state);
  20. if (!ret) {
  21. SE_LOGE("[ERROR] Failed to invoke %s\n", funcName);
  22. }
  23. se::internal::setReturnValue(state.rval(), v8args);
  24. }

性能对比

opt-5.jpg

总结

在 Cocos Creator 3.6.0 版本中,我们通过以下主要优化手段改进了原生引擎的性能:

  1. 在原生层(C++层)中实现引擎的核心模块,充分利用 C++ 代码的执行性能。
  2. 尽可能减少跨语言的交互(JS <-> CPP)频率,我们采用了以下五种方法进行优化:
    • 共享内存:通过减少内存拷贝的方式提高性能。
    • 避免接口传参:使用成员变量替代频繁的参数传递。
    • 缓存属性:将频繁访问的属性缓存起来,避免重复获取。
    • 节点同步:在节点操作时,通过事件监听机制同步节点的变化。
    • 参数数组对象池:使用对象池复用参数数组对象,减少内存分配和释放的开销。

通过这些优化措施,我们改进了 Cocos Creator 引擎的性能表现。这些优化技术可以显著降低跨语言交互的开销,提高了引擎的整体性能和响应速度。