【性能优化】cocoscreator 共享节点-动效复用方案

news2024/11/24 17:26:11

前言

 

迷雾散尽,露出了古朴庄严的森林。古老的铁杉,在头顶编成绿色穹顶。 阳光在树叶间破碎成金色顶棚。从树干间远眺,远处的森林渐渐隐去。
用几句话就能描述一片巨大的森林,但是在实时游戏中做这件事就完全是另外一件事了。 当屏幕上需要显示一整个森林时,图形程序员看到的是每秒需要送到GPU六十次的百万多边形。
我们讨论的是成千上万的树,每棵都由上千的多边形组成。 就算有足够的内存描述森林,渲染的过程中,CPU到GPU的部分也太过繁忙了
–Bob Nystrom《游戏设计模式》

以上是《游戏设计模式》这本书中“享元模式”章节提到的内容。文中还提到,当我们需要渲染一整片使用给相同纹理的树木时,可以只进行一次树木模型的数据传输,随后发送每棵树不同的位置、大小等数据,来实现森林的效果。

初次读到这个章节的时候,我觉得这真是一个了不起的优化,去除所有重复的工作,用最少的渲染数据,实现大量复用元素的渲染。
当时的我一想,这不就是渲染合批在做的事情吗?提交一次纹理数据,再多次提交顶点数据,就可以实现渲染大量相同的元素。
年轻的我以为故事到这里就结束了… 但是!前段时间看了上海站Cocos Star Meeting,乐府互娱大佬夏凯强分享的“共享节点”,发现这个故事其实还有得聊!

引擎已经帮我们实现了渲染合批的功能,当我们需要渲染大量元素(比如同一张图集下的图片)时,即使不做任何优化,性能效果也是非常优秀的。

能不能更进一步呢?如果我们需要的是一样的结果(比如渲染100个草丛),必须要这么多节点、这么多计算吗?
胆子大一点,把这些节点、组件全部省掉!相同元素只创建一份,杜绝重复劳动——这就是共享节点。

1. 开发环境

浏览器:Chrome
开发语言:JavaScript
引擎版本:CocosCreator 2.4.3

 注意:
乐府互娱基于原生环境实现了共享节点,但本文是基于H5的实现。

2. 研究过程

贴一下大佬的PPT(可在Cocos公众号后台发送“上海”获得,其他内容也很棒):


在他们的项目中,共享节点技术主要用来解决动效复用的问题。
在我看来,动效渲染时,会伴随大量的计算,如果有多个相同动效,会产生大量没必要的CPU消耗。虽然引擎提供了SHARED_CACHE等优化方式,但是似乎和我们的期望还是有一点点差距。

image750×422 57.8 KB


从这张PPT中,可以看出共享节点的主要实现逻辑:

  1. 相同的对象只创建1个
  2. 将该对象的渲染数据进行多次提交

image750×422 72.4 KB


最后的PPT涉及的是渲染流程的修改,在渲染子节点之后,对节点下的共享节点(特殊的子节点)进行渲染。

由于我是在线上观看的分享,加上运气不好,进去的时候这一段刚好完美错过了


看完PPT之后,也只是模糊地有了一个思路,本着不要脸的精神,我联系上了大佬(#狗头)!

得知他们的方案大概是:修改了节点的相关逻辑,使得共享节点可以有多个父节点,这样就实现了共享节点可以在多个位置进行渲染。感谢大佬!

3. 实现思路

3.1 思路梳理

综上,可以大概得出共享节点实现的逻辑(以多次渲染同一图片为例):

  1. 创建一个实际的节点,带有Sprite组件(称为共享节点)。
  2. 不再创建节点,而是将共享节点多次添加到父节点中
  3. 修改渲染流程,在渲染完子节点后,渲染共享节点(可能有多个)。
  4. 将共享节点自己的渲染数据再次进行提交。

3.2 思路简化

虽然大致的实现思路已经清晰了,但是为了方便实现… 我换了个方案(…

  1. 创建一个实际的节点,带有Sprite组件(称为共享节点)。
  2. 不再创建节点,而是创建一个复制节点(js对象),其中保存了位置、共享节点等信息。
  3. 将复制节点记录到父节点的_copyChidrens中。
  4. 修改渲染流程,在渲染完子节点后,渲染_copyChidrens(可能有多个)。
  5. 复制节点在渲染时,拷贝共享节点的渲染数据。
  6. 对渲染数据进行一点小加工,比如修改位置。
  7. 提交渲染数据!

注意,这里引入了“复制节点”这个概念。意义为:复制节点是共享节点的一个复制品~

4. 代码实现

为了实现共享节点,我们需要修改引擎相关代码。
本文是基于H5的实现,如果你需要像乐府一样支持原生端,则需要修改对应的C++代码。

4.1 拓展cc.Node

我们需要拓展内置节点功能,支持添加复制节点,并修改渲染流程,支持复制节点的渲染。

4.1.1 增加共享节点标记

// CCNode.js(cocos2d\core\CCNode.js)
properties: {
    _isShareNode: false,
    isShareNode: {
        get () {
            return this._isShareNode;
        },
        set (value) {
            this._isShareNode = value;
        }
    },
}

4.1.2 支持记录复制节点

// CCNode.js(cocos2d\core\CCNode.js)
let NodeDefines = {
    /**
     * 添加一个复制节点作为子节点
     * @param {CopyNode} node 复制节点
     */
    addCopyChildren (node) {
        let nodes = (this._copyChidrens || (this._copyChidrens = []));
        nodes.push(node);
        node.setParent(this);
    }
}

4.1.3 渲染复制节点

// render-flow.js(cocos2d\core\renderer\render-flow.js)
_proto._children = function (node) {
    let children = node._children;
    for (let i = 0, l = children.length; i < l; i++) {
      // ... 渲染子节点
    }
  
    // 修改开始-共享节点-渲染复制子节点
    if (node._copyChidrens) {
        for (let children of node._copyChidrens) {
            children.fillBuffers(batcher);
        }
    }
    // 修改结束-共享节点-渲染复制子节点
  
    this._next._func(node);
};

我们在子节点渲染的逻辑之后,渲染复制节点。
我们遍历节点的所有复制子节点(_copyChidrens),并调用fillBuffers函数进行渲染数据的填充

 注意:
在cocos渲染相关的流程中,若节点没有子节点,则不会进入_children渲染流,因此,作为复制节点的父节点,至少要有一个节点,即使是空节点也可以**。**

节点的修改比较简单,接着我们实现复制节点的逻辑。

4.2 实现复制节点(CopyNode)

不同渲染组件的实现逻辑会有不同,因此我们实现一个父类CopyNode,来负责通用的逻辑。

// CopyNode.js
const _nodeWorldPos = new cc.Vec2();
const _targetWorldPos = new cc.Vec2();

module.exports = class CopyNode {
    constructor(shareNode, x, y) {
        this.shareNode = shareNode;

        this._disX = 0;
        this._disY = 0;

        this.position = new cc.Vec2();
    }

    get x() { return this.position._x }
    get y() { return this.position._y }

    setParent(parent) {
        this._parent = parent;
    }

    setPosition(x, y) {
        if (!this._parent) {
            CC_DEBUG && cc.warn("复制节点:请在设置父节点后,再修改位置");
            return;
        }

        x === undefined && (x = this.position.x);
        y === undefined && (y = this.position.y);

        this.position.x = x;
        this.position.y = y;

        this._parent.convertToWorldSpaceAR(this.position, _nodeWorldPos);
        this.shareNode.parent.convertToWorldSpaceAR(this.shareNode.position, _targetWorldPos);

        let disX = _nodeWorldPos.x - _targetWorldPos.x;
        let disY = _nodeWorldPos.y - _targetWorldPos.y;
        this.calDis(disX, disY);
    }

    /**
     * 计算和共享节点的距离
     * 有特殊需求可重写本函数
     * @param {number} x x距离
     * @param {number} y y距离
     */
    calDis(x, y) {
        this._disX = x;
        this._disY = y;
    }
}

目前的需求只涉及修改位置,因此这里只实现了位置相关的设置和读取功能。
特殊的是,这里还额外计算了_disX和_disY。这是由于,复制节点的渲染数据来源于共享节点,修改位置时,如果知道复制节点和共享节点之间的距离,就可以直接进行相加得出结果,无需重新计算。

4.3 支持复制Sprite

乐府没有提到复用Sprite相关的内容,但是我有一些其他的想法,因此顺便研究了一下


Sprite实现起来比Spine简单,我们先从Sprite开始~

// CopySprite.js
module.exports = class CopySprite extends CopyNode {
    fillBuffers(renderer) {
        let renderComp = this.shareNode._renderComponent;
        renderComp._checkBacth(renderer, this.shareNode._cullingMask);

        // 获得共享节点的渲染数据
        let assembler = renderComp._assembler;
        let renderData = assembler._renderData;
        let vData = renderData.vDatas[0];
        let iData = renderData.iDatas[0];

        // 申请渲染数据空间、计算数据位移
        let buffer = assembler.getBuffer(renderer);
        let { verticesCount, floatsPerVert, indicesCount } = assembler;
        let positionOffset = assembler.getVfmt().element("a_position").offset;
        let offsetInfo = buffer.request(verticesCount, indicesCount);
        let vertexOffset = offsetInfo.byteOffset >> 2, vbuf = buffer._vData;

        // 复制vertices
        let vbufLen = vbuf.length;
        if (vData.length + vertexOffset > vbufLen) {
            vbuf.set(vData.subarray(0, vbufLen - vertexOffset), vertexOffset);
        } else {
            vbuf.set(vData, vertexOffset);
        }
      	// 修改位置
        for (let i = 0; i < verticesCount; i++) {
            let positionIndex = vertexOffset + i * floatsPerVert + positionOffset;

            if (positionIndex > vbufLen) {
                break;
            }

            vbuf[positionIndex] += this._disX;
            vbuf[positionIndex + 1] += this._disY;
        }

        // 填充indices
        let ibuf = buffer._iData, indiceOffset = offsetInfo.indiceOffset, vertexId = offsetInfo.vertexOffset;
        for (let i = 0, l = iData.length; i < l; i++) {
            ibuf[indiceOffset++] = vertexId + iData[i];
        }
    }
}

对于Sprite,我们可以仿照Assembler中的fillBuffers函数进行修改。
由于渲染数据都会存储在Assembler中,我们这里取出对应的数据,再重新填充一遍就好了。

复制完渲染数据后,我们可以对顶点数据进行修改,这里只修改了位置。如果你需要修改其他属性(比如颜色),也可以在这里进行处理。

从看到共享节点的第一眼,我就想把它用到TiledMap中!这满屏幕的物件,要是能少创建几个,肯定也有不错的提升!
最后我也确实把它塞到TiledMap中了,优化效果大概是25%(地图不同会导致数据不同)。实现起来不难,大家感兴趣可以试试。

在大部分的应用场景中,很少出现同一张图片渲染多次的情况,复制节点的优势很难凸显,加上复制节点的功能完全打不过真实的节点。估计很难用到其他地方。

4.4 支持复制Spine

与Sprite不同,Spine的渲染数据并不会被缓存,这不就不能CV了吗?这可不行!
根据渲染流程,所有最终的渲染数据都会被填充到buffer中,那我们胆子大一点,直接从buffer中取!

我们在共享节点渲染时,记录Spine渲染数据在buffer中的起始和结束位置,这样就可以在复制节点中通过buffer获得共享节点的渲染数据了

4.4.1 记录渲染数据位置

// spine-assembler.js(extensions\spine\spine-assembler.js)
fillBuffers (comp, renderer) {
    // ...省略部分代码

    // 修改开始-共享节点-记录渲染数据开始位置
    if (node.isShareNode) {
      this._bufferOffsetInfo = {
        startVertexOffset: _buffer.vertexOffset,
        startIndiceOffset: _buffer.indiceOffset,
      }
    }
    // 修改结束-共享节点-记录渲染数据开始位置
  
    if (comp.isAnimationCached()) {
        // Traverse input assembler.
        this.cacheTraverse(worldMat);
    } else {
        if (_vertexEffect) _vertexEffect.begin(comp._skeleton);
        this.realTimeTraverse(worldMat);
        if (_vertexEffect) _vertexEffect.end();
    }

    // 修改开始-共享节点-记录渲染数据结束位置
    if (node.isShareNode) {
      this._bufferOffsetInfo.endVertexOffset = _buffer.vertexOffset;
      this._bufferOffsetInfo.endIndiceOffset = _buffer.indiceOffset;
    }
    // 修改结束-共享节点-记录渲染数据结束位置
  
    // ...省略部分代码
}

4.4.2 实现复制逻辑

// CopySpine.js
const VFOneColor = cc.gfx.VertexFormat.XY_UV_Color;
const VFTwoColor = cc.gfx.VertexFormat.XY_UV_Two_Color;
const _mt4 = new cc.Mat4();

module.exports = class CopySpine extends CopyNode{
    fillBuffers(renderer) {
        let renderComp = this.shareNode._renderComponent;
        renderComp._checkBacth(renderer, this.shareNode._cullingMask);

        // 计算渲染数据数量
        let assembler = renderComp._assembler;
        let { startVertexOffset, startIndiceOffset, endVertexOffset, endIndiceOffset } = assembler._bufferOffsetInfo;
        let vertexCount = endVertexOffset - startVertexOffset;
        let indiceCount = endIndiceOffset - startIndiceOffset;

        // 准备填充复制节点的渲染数据
        let _useTint = renderComp.useTint || renderComp.isAnimationCached();
        let _vertexFormat = _useTint ? VFTwoColor : VFOneColor;
        let floatsPerVert = _vertexFormat._bytes >> 2;
        // 相关变量计算结束

        // 申请渲染数据空间、计算数据位移
        let _buffer = renderer.getBuffer('spine', _vertexFormat);
        let offsetInfo = _buffer.request(vertexCount, indiceCount);
        let { indiceOffset, vertexOffset } = offsetInfo;
        let vertexFloatOffset = offsetInfo.byteOffset >> 2;

        // 复制vertices
        let vbuf = _buffer._vData;
        let startVbufIndex = startVertexOffset * floatsPerVert;
        for (let i = 0, len = vertexCount * floatsPerVert; i < len; i++) {
            vbuf[vertexFloatOffset + i] = vbuf[startVbufIndex + i];
        }

        // 修改位置
        let positionOffset = _vertexFormat.element("a_position").offset;
        let positionIndex = vertexFloatOffset + positionOffset;
        while (positionIndex < vertexFloatOffset + vertexCount * floatsPerVert) {
            vbuf[positionIndex] += this._disX;
            vbuf[positionIndex + 1] += this._disY;

            positionIndex += floatsPerVert;
        }

        // 填充indices
        let ibuf = _buffer._iData;
        for (let i = 0; i < indiceCount; i++) {
            ibuf[indiceOffset + i] = ibuf[startIndiceOffset + i] - startVertexOffset + vertexOffset;
        }

        _buffer.adjust(vertexCount, indiceCount);
    }
  
    /**
     * 计算和共享节点的距离
     * 未开启合批时,会计算矩阵变换,需要将距离根据缩放进行逆换算。否则坐标错误。
     * @param {number} x x距离
     * @param {number} y y距离
     */
    calDis(x, y) {
        let shareNode = this.shareNode;

        if (!shareNode._renderComponent.enableBatch) {
            shareNode.getWorldMatrix(_mt4);

            x /= _mt4.m[0];
            y /= _mt4.m[5];
        }

        this._disX = x;
        this._disY = y;
    }
}

整体逻辑和Sprite类似,但我们从_buffer中复制渲染数据~

4.5 效果展示

到这里就实现了基本的复制节点效果了,我们已经可以用一个渲染节点来复制出无数个拷贝。是不是很简单!甚至有一点小激动。
你可以像这样使用复制节点:

this.nodeSprite.isShareNode = true;
let copyNode = new CopySprite(this.nodeSprite);
this.nodeContainer.addCopyChildren(copyNode);
copyNode.setPosition(-200, 0);

image960×641 27.3 KB


也可以多复制几个,做个基本的for循环就行(中间那个不合群的是共享节点):

但是!事情往往没有这么简单!

4.6 测试

4.6.1 翻车

当我把哥布林放到测试用例中…
这个框起来的小人本来应该出现在最右侧,突然… 变成了左侧…


对应的节点树:

当哥布林位于两个小人中间时,复制节点的位置错误
当哥布林位于两个小人后面时,复制节点的位置正确

4.6.2 翻车原因

乍一看以为是哥布林的原因,直接方向错误…
根本原因是:合批判定逻辑有缺陷,导致复制节点可以和最后一个渲染节点合批,但该节点又不是共享节点。
当哥布林位于小人后面时,显然无法进行合批,因此位置正确。

为什么合批判定出错,会导致位置错误呢?

渲染数据提交时,每个渲染批次会对应一个渲染模型(model),每个渲染模型会绑定一个节点。
节点的位置会变换为cc_matWorld属性(模型空间转世界空间矩阵),传递给effect的顶点处理器。

// base-renderer.js(cocos2d\renderer\core\base-renderer.js)
_draw (item) {
  // 省略了部分代码
	node.getWorldMatrix(_m4_tmp);
	device.setUniform('cc_matWorld', Mat4.toArray(_float16_pool.add(), _m4_tmp));
}

在effect中,cc_matWorld会和cc_matViewProj进行计算,得出顶点位置的换算矩阵。

// builtin-2d-spine.effect(resources\static\default-assets\resources\effects\builtin-2d-spine.effect)
// 省略了部分代码
CCProgram vs %{
  void main () {
    #if CC_USE_MODEL
      mvp = cc_matViewProj * cc_matWorld;
    #else
      mvp = cc_matViewProj;
    #endif
      
    gl_Position = mvp * vec4(a_position, 1);
  }
}%

因此,当我们判定复制节点可以和最后的小人合批时,我们将渲染数据附在了最后的小人上,一起提交给了渲染引擎。
基于错误的换算矩阵计算出来的位置自然也是错误的。

这里有个新的疑问,为什么Sprite中没有出现类似的问题
答案就在effect的代码中:CC_USE_MODEL的值。
Sprite的CC_USE_MODEL值为false,而Spine的CC_USE_MODEL值为true
也就是说,Sprite渲染时,并不会进行转换。自然也不会因为节点的位置导致错误~

CC_USE_MODEL值取决于是否启用合批(enableBatch属性),在复用Spine的情况下,不一定能够合批,因此不进一步讨论。

5. 修正合批检测

5.1 实现合批检测

在原本的方案中,我们调用共享节点的renderComponent来实现合批检测,现在我们需要拓展合批检测逻辑,来实现一个类似的~

// CopyNode.js
class CopyNode {
    /**
     * 检测是否可以合批
     */
    checkBacth(renderer) {
        if (!this.checkCanBacth(renderer)) {
            this.flush(renderer);
            return;
        }

        // cocos内置检测逻辑
        let node = this.shareNode;
        let cullingMask = node._cullingMask;
        let material = node._renderComponent._materials[0];
        if (renderer.cullingMask !== cullingMask ||
            (material && material.getHash() !== renderer.material.getHash())
        ) {
            this.flush(renderer);
        }
    }

    /**
     * 检测是否可以合批(子类可以继承拓展)
     */
    checkCanBacth(renderer) {
        return true;
    }

    /**
     * 刷写渲染数据
     */
    flush(renderer) {
        renderer._flush();

        let node = this.shareNode;
        let material = node._renderComponent._materials[0];
        renderer.node = material.getDefine('CC_USE_MODEL') ? node : renderer._dummyNode;
        renderer.material = material;
        renderer.cullingMask = node._cullingMask;
    }
}

与内置的合批检测相比,我们在检测之前增加了一个可拓展的函数checkCanBatch,来针对某些特殊子类。

修改原有的checkBatch函数调用(CopySpine和CopySprite都需要修改)

fillBuffers(renderer) {
    this.checkBacth(renderer);
    let renderComp = this.shareNode._renderComponent;
    // 省略后续代码
}

5.2 实现Spine合批检测

// CopySpine.js
class CopySpine extends CopyNode {
    checkCanBacth(renderer) {
        return this.shareNode === renderer.node;
    }
}

就这么简单,当节点不一样的时候,我们就认定为无法合批。


很好,位置正确了!哥布林和小人们快乐地… (不是

6. 性能对比

测试数据由于环境的影响,常常波动不定,因此我们将小人复制五百份,来增大对比性。
两组对照为:
优化前:使用cc.instantiate
优化后:使用复制节点

image1405×775 59.3 KB


emmm… 可以明显地看出它们的差距… 大到甚至不想多次实验取平均值…
使用复制节点对比优化前(ShareCache),大约只消耗了32%的性能。

7. 总结

共享节点可以减少节点、组件的创建,避免了大量组件的各类刷新函数调用,同时还可以减少对于渲染数据的计算。
对于复用Spine有奇效,Spine往往需要使用CPU进行位移、变换等运算,使用共享节点后,可以省掉这些计算量,当然SharedCache也可以,但需要额外的内存空间。

本文的方案在某些场景下,已经实现了最初目的——复用Spine,但还是存在一些限制。比如对节点移动、变换等效果的支持不足。也有一些地方还可以优化,比如必须先设置父节点才能修改位置。
受限于本人的技术(以及懒),若干问题不赘述,但技术是个好技术,希望给大家一点启发~
乐府的方案是对原生平台的优化,本人学艺不精,只能做个H5版本玩玩。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/426201.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[数据结构-C语言] 算法的时间复杂度

目录 1.算法的复杂度 2.时间复杂度 2.1 时间复杂度的概念 2.2 大O的渐进表示法 3、常见时间复杂度计算举例 3.1 冒泡排序 3.2 二分查找 3.3 阶乘递归 3.4 斐波那契数列 1.算法的复杂度 算法在编写成可执行程序后&#xff0c;运行时需要耗费时间资源和空间(内存)资源 …

PS学习记录——自动选择的【图层】/【组】模式说明

PS中&#xff0c;【移动工具】状态下&#xff0c;上方的工具栏有个【自动选择功能】&#xff0c;用于移动图像 自动选择下有【图层】、【组】两种选项 1、自动选择-【图层】 图层模式下&#xff0c;自动选择可以任意移动选中的图像图层&#xff0c;想移动哪个就移动哪个 即便不…

43.CSS grid布局

本节我们学习的初始代码如下&#xff1a; <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta http-equiv"X-UA-Compatible" content"IEedge" /><meta name"viewport" …

110页智慧农业解决方案(农业信息化解决方案)(ppt可编辑)

本资料来源公开网络&#xff0c;仅供个人学习&#xff0c;请勿商用&#xff0c;如有侵权请联系删除。 第一部分 智慧农业概述 智慧农业以农业资源为基础、市场为导向、效益为中心、产业化为抓手&#xff0c;面向农业管理部门、农技推广部门、农业企业、农业园区和基地、农业专家…

【C】Pointer

系列连载 【python / C / C】 参考 《C语言程序设计&#xff08;第四版&#xff09;谭浩强》【C语言】C语言视频教程《郝斌 C 语言自学教程》 文章目录一、指针的重要性二、指针的定义三、指针的分类基本类型指针指针和一维数组指针和函数指针和结构体多级指针int * p;// p …

CASS自带数据-绘制地形图

1、打开cass软件&#xff0c;如下图&#xff1a; 2、点击“绘图处理”菜单栏&#xff0c;选择“展野外测点点号”&#xff0c;如下&#xff1a; 3、在命令栏输入绘图比例尺&#xff0c;这里选择默认1:500&#xff0c;点击回车键进入下一步&#xff0c; 4、在弹出的窗口中&#…

单片机通过串口向电脑端发送数据电脑端发送数据控制led

上节课我们学习了串口的理论部分&#xff0c;这节课我们要来学习实操部分。 要想实现单片机通过串口向电脑端发送数据&#xff0c;我们首先要来配置寄存器。 1.配置SCON SCON寄存器中的SM0配置为0&#xff0c;SM1配置为1决定了串口工作在模式一&#xff0c;也就是8位UART&am…

前端透明分栏设计

目前在开发一个基于众包的打分网站&#xff0c;前端遇到的一个需求是&#xff1a; 背景是电影的海报&#xff0c;且不能随着scroll-bar滚动&#xff0c;需要一个蒙版分为两栏&#xff0c;左侧是影视的媒体信息&#xff0c;不随页面滚动右侧是影视的基本信息和评分信息&#xf…

如何使用基于GPT-4的Cursor编辑器提升开发效率

程序员最恨两件事情&#xff1a;一是别人代码不写文档&#xff0c;二是要让自己写文档。随着 GPT-4 的到来这些都不是问题了&#xff0c;顺带可能连程序员都解决了。。。 之前一直觉得 AI 生成的代码也就写个面试题的水平&#xff0c;小打小闹&#xff0c;现在时代可变了。Curs…

@Conditional四个较常用的派生注解总结

该内容是在学习SpringBoot底层的时候&#xff0c;看到SpringBoot的使用&#xff0c;所以这的做一个简单的总结方便以后复习。可能会有一些问题&#xff0c;还望指出共同学习 Conditional注解&#xff1a; 作用&#xff1a;按照一定的条件进行判断&#xff0c;在满足给定条件后…

如果采用密钥对的形式登录系统后,如何由普通用户切换到root用户

使用xshell工具 采用秘钥的方式进行登录 创建一个新用户并设置密码 切换到新用户之后 su root 想要切换成root用户的时候 输入密码提示 su:Authentication failure su&#xff1a;身份验证失败 使用秘钥之后是不能使用密码再进行登录 我去阿里云查了一下 有一个相同…

stable diffusion 安装xFormers 报错:Couldn‘t install open_clip.

一、No module ‘xformers’. Proceeding without it. 这是因为没有安装xformers导致的。 解决办法&#xff1a; 在webui-user.bat文件这添加一行&#xff1a; set COMMANDLINE_ARGS--xformers如下图所示&#xff1a; 试着点击webui-user.bat&#xff0c;看能否下载&#xff…

17:00面试,17:04就出来了 ,问的实在是太...

从外包出来&#xff0c;没想到算法死在另一家厂子 自从加入这家公司&#xff0c;每天都在加班&#xff0c;钱倒是给的不少&#xff0c;所以也就忍了。没想到8月一纸通知&#xff0c;所有人不许加班&#xff0c;薪资直降30%&#xff0c;顿时有吃不起饭的赶脚。 好在有个兄弟内推…

你具备抽离与封装的思想吗?

笔者最近思考了自己参与的分布式系统业务的架构小细节&#xff0c;虽然笔者每天做的是实现部分需求与业务&#xff0c;但是笔者还是拥有很多时间去读底层源码的&#xff0c;加之笔者自身的思考与实践demo的总结&#xff0c;笔者将在本篇文章中提出笔者自己对“抽离”与“封装”…

Nginx编译安装及配置文件详解

写在前面 Centos版本&#xff1a;Centos 7.6 - 64bit Nginx版本&#xff1a;1.20.2 一、什么是Nginx Nginx (engine x) 是一款轻量级的Web 服务器 、反向代理服务器及电子邮件&#xff08;IMAP/POP3&#xff09;代理服务器。 二、Nginx用在哪些地方 2.1 静态资源服务 动静…

mkv视频文件怎么转成mp4,这3个方法很好用

对于那些电脑不是很精通的小伙伴来说&#xff0c; mkv视频文件怎么转成mp4&#xff1f;小编先来介绍一下mkv视频格式&#xff0c;mkv是一种开放标准的自由的容器和文件格式&#xff0c;是一种多媒体封装格式&#xff0c;能够在一个文件中容纳无限数量的视频、音频、图片或字幕轨…

MVCC底层原理

目录说明MVCC的底层原理隐藏字段undo logRead View说明 在被面试官问面试题的时候&#xff0c;首先它问了Mysql的事务的隔离级别有几种&#xff1f;默认是哪种&#xff1f;他们分别解决了什么问题&#xff1f; 我在一顿回答“巴巴巴巴。。。。”之后&#xff0c;它又继续问题…

追踪项目进展常用的衡量方法

作为项目管理活动&#xff0c;项目中的进度跟踪可以帮助你了解项目实时的完成状态。有了正确的方法、项目管理工具和指标&#xff0c;项目跟踪可以指导你的团队的行动&#xff0c;甚至在问题出现之前向你发出预警。 项目经理使用各种方法、工具和指标来跟踪进度。其中最常见的…

自然语言处理(Natural Language Processing,NLP)简要

自然语言处理&#xff08;Natural Language Processing&#xff0c;NLP&#xff09;简要一、发展状况二、发展优势三、发展瓶颈四、具体研究方向五、自然语言处理工具六、未来发展方向自然语言处理&#xff08;Natural Language Processing&#xff0c;NLP&#xff09;是计算机…

(原创)Flutter基础入门:各种常用容器类组件

前言 上篇博客主要讲了Flutter的Shape实现 Flutter基础入门&#xff1a;实现各种Shape效果 今天主要讲下Flutter中常用的一些容器组件 Flutter中组件数量很多&#xff0c;分类方式也各有不同 比如可以分为有态组价和无态组件 又可以分为容器组件、功能组件等 拿功能组件来说 有…