opengl计算帧率_或许是迄今为止第一篇讲解 fps 计算原理的文章吧

前言

fps,是 frames per second 的简称,也就是我们常说的“帧率”。在游戏领域中,fps 作为衡量游戏性能的基础指标,对于游戏开发和手机 vendor 厂商都是非常重要的数据,而计算游戏的 fps 也成为日常测试的基本需求。目前市面上有很多工具都能够计算 fps,那么这些工具计算 fps 的方法是什么?原理是什么呢?本文将针对这些问题,深入源码进行分析,力求找到一个详尽的答案(源码分析基于 Android Q)

计算方法

目前绝大部分帧率统计软件,在网上能找到的各种统计 fps 的脚本,使用的信息来源有两种:一种是基于 dumpsys SurfaceFlinger --latency Layer-name(注意是 Layer 名字,不是包名,不是 Activity 名字,至于为什么,下面会解答);另一种是基于 dumpsys gfxinfo。其实这两种深究到原理基本上是一致的,本篇文章专注于分析第一种,市面上大部分帧率统计软件用的也是第一种,只不过部分软件为了避免被人反编译看到,将这个计算逻辑封装成 so 库,增加反编译的难度。然而经过验证,这些软件最后都是通过调用上面的命令来计算的 fps 的。

但是这个命令为什么能够计算 fps 呢?先来看这个命令的输出,以王者荣耀为例(王者荣耀这种游戏类的都是以 SurfaceView 作为控件,因此其 Layer 名字都以 SurfaceView - 打头):

> adb shell dumpsys SurfaceFlinger --latency "SurfaceView - com.tencent.tmgp.sgame/com.tencent.tmgp.sgame.SGameActivity#0"1666666659069638658663  59069678041684  5906965415829859069653090955  59069695022100  5906967089423659069671034444  59069711403455  5906968794986159069688421840  59069728057361  5906970441512159069705420850  59069744773350  5906972076783059069719818975  59069761378975  5906973741600759069736702673  59069778060955  5906975456866359069753361528  59069794716007  5906977076163259069768766371  59069811380486  59069787649600......

输出的这一堆数字究竟是什么意思?首先,第一行的数字是当前的 VSYNC 间隔,单位是纳秒。例如现在的屏幕是 60Hz 的,因此就是 16.6ms,然后下面的一堆数字,总共有 127 行(为什么是 127 行,下面也会说明),每一行有 3 个数字,每个数字都是时间戳,单位是纳秒,具体的意义后文会说明。而在计算 fps 的时候,使用的是第二个时间戳。原因同样会在后文进行解答。

fence 简析

后面的原理分析涉及到 fence,但是 fence 囊括的内容众多,因此这里只是对 fence 做一个简单地描述。如果大家感兴趣,后面我会专门给 fence 写一篇详细的说明文章。

fence 是什么

首先得先说明一下 App 绘制的内容是怎么显示到屏幕的:

App 需要显示的内容要要绘制在 Buffer 里,而这个 Buffer 是从 BufferQueue 通过 dequeueBuffer() 申请的。申请到 Buffer 以后,App 将内容填充到 Buffer 以后,需要通过 queueBuffer() 将 Buffer 还回去交给 SurfaceFlinger 去进行合成和显示。然后,SurfaceFlinger 要开始合成的时候,需要调用 acquireBuffer() 从 BufferQueue 里面拿一个 Buffer 去合成,合成完以后通过 releaseBuffer() 将 Buffer 还给 BufferQueue,如下图:

5382ea0911149f0e92ea8923e6baa10d.png
BufferQueue

在上面的流程中,其实有一个问题,就是在 App 绘制完通过 queueBuffer() 将 Buffer 还回去的时候,此时仅仅只是 CPU 侧的完成,GPU 侧实际上并没有真正完成。因此如果此时拿这个 Buffer 去进行合成/显示的话,就会有问题(Buffer 可能还没有完全地绘制完)。

事实上,由于 CPU 和 GPU 之前是异步的,因此我们在代码里面执行一系列的 OpenGL 函数调用的时候,看上去函数已经返回了,实际上,只是把这个命令放在本地的 command buffer 里。具体什么时候这条 GL command 被真正执行完毕 CPU 是不知道的,除非使用 glFinish() 等待这些命令完全执行完,但是这样会带来很严重的性能问题,因为这样会使得 CPU 和 GPU 的并行性完全丧失,CPU 会在 GPU 完成之前一直处于空等的状态。因此,如果能够有一种机制,在不需要对 Buffer 进行读写 的时候,大家各干各的;当需要对 Buffer 进行读写的时候,可以知道此时 Buffer 在 GPU 的状态,必要的时候等一下,就不会有上面的问题了。

fence 就是这样的同步机制,如它直译过来的意思一样——“栅栏”,用来把东西拦住。那么 fence 是要拦住什么东西呢?就是前面提到的 Buffer 了。Buffer 在整个绘制、合成、显示的过程中,一直在 CPU,GPU 和 HWC 之前传递,某一方要使用 Buffer 之前,需要检查之前的使用者是否已经移交了 Buffer 的“使用权”。而这里的“使用权”,就是 fence。当 fence 释放(即 signal)的时候,说明 Buffer 的上一个使用者已经交出了使用权,对于 Buffer 进行操作是安全的。

fence in Code

在 Android 源码里面,fence 的实现总共分为四部分:

•fence driver    同步的核心实现•libsync    位于 system/core/libsynclibsync 的主要作用是对 driver 接口的封装•Fence 类    这个 Fence 类位于 frameworks/native/libs/ui/Fence.cpp,主要是对 libsync 进行 C++ 封装,方便 framework 调用•FenceTime 类    这个 FenceTime 是一个工具类,是对 Fence 的进一步封装,提供两个主要的接口——isValid() 和 getSignalTime(),主要作用是针对需要多次查询 fence 的释放时间的场景(通过调用 Fence::getSignalTime() 来查询 fence 的释放时间)。通过对 Fence 进行包裹,当第一次调用 FenceTime::getSignalTime() 的时候,如果 fence 已经释放,那么会将这个 fence 的释放时间缓存起来,然后下次再调用 FenceTime::getSignal() 的时间,就能将缓存起来的释放时间直接返回,从而减少对 Fence::getSignalTime() 不必要的调用(因为 fence 释放的时间不会改变)。

fence in Android

在 Android 里面,总共有三类 fence —— acquire fence,release fence 和 present fence。其中,acquire fence 和 release fence 隶属于 Layer,present fence 隶属于帧(即 Layers):

•acquire fence    前面提到, App 将 Buffer 通过 queueBuffer() 还给 BufferQueue 的时候,此时该 Buffer 的 GPU 侧其实是还没有完成的,此时会带上一个 fence,这个 fence 就是 acquire fence。当 SurfaceFlinger/ HWC 要读取 Buffer 以进行合成操作的时候,需要等 acquire fence 释放之后才行。•release fence    当 App 通过 dequeueBuffer() 从 BufferQueue 申请 Buffer,要对 Buffer 进行绘制的时候,需要保证 HWC 已经不再需要这个 Buffer 了,即需要等 release fence signal 才能对 Buffer 进行写操作。•present fence    present fence 在 HWC1 的时候称为 retire fence,在 HWC2 中改名为 present fence。当前帧成功显示到屏幕的时候,present fence 就会 signal。

原理分析

简单版

现在来看一下通过 dumpsys SurfaceFlinger --latency Layer-name 计算 Layer fps 的原理。dumpsys 的调用流程就不赘述了,最终会走到 SurfaceFlinger::doDump():

status_t SurfaceFlinger::doDump(int fd, const DumpArgs& args,                                bool asProto) NO_THREAD_SAFETY_ANALYSIS {    ...        static const std::unordered_map<std::string, Dumper> dumpers = {                ......                {"--latency"s, argsDumper(&SurfaceFlinger::dumpStatsLocked)},                ......        };

从这里可以看到,我们在执行 dumpsys SurfaceFlinger 后面加的那些 --xxx 参数最终都会在这里被解析,这里咱们是 --latency,因此看 SurfaceFlinger::dumpStatsLocked

void SurfaceFlinger::dumpStatsLocked(const DumpArgs& args, std::string& result) const {    StringAppendF(&result, "%" PRId64 "\n", getVsyncPeriod());    if (args.size() > 1) {        const auto name = String8(args[1]);        mCurrentState.traverseInZOrder([&](Layer* layer) {            if (name == layer->getName()) {                layer->dumpFrameStats(result);            }        });    } else {        mAnimFrameTracker.dumpStats(result);    }}

从这里就能够看到,这里先会打印当前的 VSYNC 间隔,然后遍历当前的 Layer,然后逐个比较 Layer 的名字,如果跟传进来的参数一致的话,那么就会开始 dump layer 的信息;否则命令就结束了。因此,很多人会遇到这个问题:

❔ 为什么执行了这个命令却只打印出一个数字?

✔ 其实这个时候你应该去检查你的 Layer 参数是否正确。

接下来 layer->dumpFrameStats() 会去调 FrameTrack::dumpStats()

void FrameTracker::dumpStats(std::string& result) const {    Mutex::Autolock lock(mMutex);    processFencesLocked();    const size_t o = mOffset;    for (size_t i = 1; i < NUM_FRAME_RECORDS; i++) {        const size_t index = (o+i) % NUM_FRAME_RECORDS;        base::StringAppendF(&result, "%" PRId64 "\t%" PRId64 "\t%" PRId64 "\n",                            mFrameRecords[index].desiredPresentTime,                            mFrameRecords[index].actualPresentTime,                            mFrameRecords[index].frameReadyTime);    }    result.append("\n");}

NUM_FRAME_RECORDS 被定义为 128,因此输出的数组有 127 个。每组分别有三个数字—— desiredPresentTimeactualPresentTimeframeReadyTime,每个数字的意义分别是:

desiredPresentTime     下一个 HW-VSYNC 的时间戳•actualPresentTime     retire fence signal 的时间戳•frameReadyTime     acquire fence signal 的时间戳

结合前面对 present fence 的描述就可以看出 dumpsys SurfaceFlinger --latency 计算 fps 的原理:

从 dumpsys SurfaceFlinger --latency 获取到最新 127 帧的 present fence 的 signal time,结合前面对于 present fence 的说明,当某帧 present fence 被 signal 的时候,说明这一帧已经被显示到屏幕上了。因此,我们可以通过判断一秒内有多少个 present fence signal 了,来反推出一秒内有多少帧被刷到屏幕上,从而计算出 fps。

复杂版

我们已经知道了 fps 计算的原理了,但是呢,小朋友,你是否有很多问号?

•这个 actualPresentTime 是从哪来的?•假设要统计 fps 的 Layer 没有更新,但是别的 Layer 更新了,这种情况下 present fence 也会正常 signal,那这样计算出来的 fps 是不是不准啊?

为了解答这些问题,我们还得接着看。

前面已经提到计算 fps 的时候使用的是第二个数值,因此后面的文章着重分析这个 actualPresentTime。那么 actualPresentTime 是在哪里赋值的呢?实际赋值的位置是在 FrameTracker::dumpStats() 调用的一个子函数——processFencesLocked()

void FrameTracker::processFencesLocked() const {    FrameRecord* records = const_cast<FrameRecord*>(mFrameRecords);    int& numFences = const_cast<int&>(mNumFences);    for (int i = 1; i < NUM_FRAME_RECORDS && numFences > 0; i++) {        size_t idx = (mOffset+NUM_FRAME_RECORDS-i) % NUM_FRAME_RECORDS;        ...        const std::shared_ptr<FenceTime>& pfence =                records[idx].actualPresentFence;        if (pfence != nullptr) {            // actualPresentTime 是在这里赋值的            records[idx].actualPresentTime = pfence->getSignalTime();            if (records[idx].actualPresentTime < INT64_MAX) {                records[idx].actualPresentFence = nullptr;                numFences--;                updated = true;            }        }        ......

其中,FrameRecord 的完整定义如下:

struct FrameRecord {     FrameRecord() :         desiredPresentTime(0),         frameReadyTime(0),         actualPresentTime(0) {}     nsecs_t desiredPresentTime;     nsecs_t frameReadyTime;     nsecs_t actualPresentTime;     std::shared_ptr<FenceTime> frameReadyFence;     std::shared_ptr<FenceTime> actualPresentFence;};

从上面的代码可以看出,actualPresentTime 是调用 actualPresentFence 的 getSignalTime() 赋值的。而 actualPresentFence 是通过 setActualPresentFence() 赋值的:

void FrameTracker::setActualPresentFence(        std::shared_ptr<FenceTime>&& readyFence) {    Mutex::Autolock lock(mMutex);    mFrameRecords[mOffset].actualPresentFence = std::move(readyFence);    mNumFences++;}

setActualPresentFence() 又是经过下面的调用流程最终被调用的:

SurfaceFlinger::postComposition()  \_ BufferLayer::onPostCompostion()

这里重点看一下 SurfaceFlinger::postComposition()

void SurfaceFlinger::postComposition(){    ......    mDrawingState.traverseInZOrder([&](Layer* layer) {        bool frameLatched =                layer->onPostComposition(displayDevice->getId(), glCompositionDoneFenceTime,                                         presentFenceTime, compositorTiming);    ......

回忆一下我们前面的问题:

❔ 假设要统计 fps 的 Layer 没有更新,但是别的 Layer 更新了,这种情况下 present fence 也会正常 signal,那这样计算出来的 fps 是不是不准啊?

答案就在 mDrawingState,在 Surfacelinger 中有两个用来记录当前系统中 Layers 状态的全局变量:

•mDrawingState    mDrawingState 代表的是上次 “drawing” 时候的状态•mCurrentState    mCurrentState 代表的是当前的状态因此,如果当前 Layer 没有更新,那么是不会被记录到 mDrawingState 里的,因此这一次的 present fence 也就不会被记录到该 Layer 的 FrameTracker 里的 actualPresentTime 了。

再说回来, SurfaceFlinger::postComposition() 是 SurfaceFlinger 合成的最后阶段。presentFenceTime 就是前面的 readyFence 参数了,它是在这里被赋值的:

mPreviousPresentFences[0] = mActiveVsyncSource        ? getHwComposer().getPresentFence(*mActiveVsyncSource->getId())        : Fence::NO_FENCE;auto presentFenceTime = std::make_shared<FenceTime>(mPreviousPresentFences[0]);

而 getPresentFence() 这个函数,就把这个流程转移到了 HWC 了:

sp<Fence> HWComposer::getPresentFence(DisplayId displayId) const {    RETURN_IF_INVALID_DISPLAY(displayId, Fence::NO_FENCE);    return mDisplayData.at(displayId).lastPresentFence;}

至此,我们一路辗转,终于找到了这个 present fence 的真身,只不过这里它还蒙着一层面纱,我们需要在看一下这个 lastPresentFence 是在哪里赋值的,这里按照不同的合成方式位置有所不同:

DEVICE 合成

DEVICE 合成的 lastPresentFence 是在 HWComposer::prepare() 里赋值:

status_t HWComposer::prepare(DisplayId displayId, const compositionengine::Output& output) {    ......    if (!displayData.hasClientComposition) {        sp<Fence> outPresentFence;        uint32_t state = UINT32_MAX;        error = hwcDisplay->presentOrValidate(&numTypes, &numRequests, &outPresentFence , &state);        if (error != HWC2::Error::HasChanges) {            RETURN_IF_HWC_ERROR_FOR("presentOrValidate", error, displayId, UNKNOWN_ERROR);        }        if (state == 1) { //Present Succeeded.            ......            displayData.lastPresentFence = outPresentFence;

经常看 systrace 的同学对这个函数绝对不会陌生,就是 systrace 里面 SurfaceFlinger 的那个 prepare()

2c35e071a595ab6f5a12e082f2c2ae68.png
Systrace 中的 prepare

这个函数非常重要,它通过一系列的调用:

HWComposer::prepare()  \_ Display::presentOrValidate()       \_ Composer::presentOrValidateDisplay()            \_ CommandWriter::presentOrvalidateDisplay()

最终通过 HwBinder 通知 HWC 的 Server 端开始进行 DEVICE 合成,Server 端在收到 Client 端的请求以后,会返回给 Client 端一个 present fence(时刻记住,fence 用于跨环境的同步,例如这里就是 Surfacelinger 和 HWC 之间的同步)。然后当下一个 HW-VSYNC 来的时候,会将合成好的内容显示到屏幕上并且将该 present fence signal,标志着这一帧已经显示在屏幕上了。

GPU 合成

GPU 合成的 lastPresentFence 是在 presentAndGetPresentFences() 里赋值:

status_t HWComposer::presentAndGetReleaseFences(DisplayId displayId) {    ......    displayData.lastPresentFence = Fence::NO_FENCE;    auto error = hwcDisplay->present(&displayData.lastPresentFence);

后面的流程就跟 DEVICE 合成类似了,Display::present() 最终也会经过一系列的调用,通过 HwBinder 通知 HWC 的 Server 端,调用 presentDisplay() 将合成好的内容显示到屏幕上。

总结

说了这么多,一句话总结计算一个 App 的 fps 的原理就是:

统计在一秒内该 App 往屏幕刷了多少帧,而在 Android 的世界里,每一帧显示到屏幕的标志是:present fence signal 了,因此计算 App 的 fps 就可以转换为:一秒内 App 的 Layer 有多少个有效 present fence signal 了(这里有效 present fence 是指,在本次 VSYNC 中该 Layer 有更新的 present fence)

尾巴

这篇文章在二月份其实就已经完成了一多半了,但是一直拖到了五月才最终写完,因为其中涉及到很多我不知道的知识,例如 HWC。这块领域涉及到硬件,文档其实不多。因此在写的过程会变得异常痛苦,很多东西不懂,我也不知道自己写的东西究竟对不对,就需要花很多时间进行多方求证,找很多大佬提问。很多时候会卡在某一个地方很久,甚至会萌生随便写点糊弄过去算了的想法。而且,写到什么程度也很难拿捏,写浅了我自己过意不去,感觉对不起各位关注的读者;写深了我自己也是写不下去,毕竟这个领域确实之前没有接触过。不过好在这个过程中有很多大佬给我提供了很大的帮助,在此对在这几月中给我答疑解惑的各位大佬表示衷心的感谢。

写作是一个孤独的旅程,感谢各位大佬的指路,感谢各位读者的关注,你们是星星太阳和月亮,陪着我一直写下去,谢谢。

6893b4e269f16cd47cbb376d8374bc2f.png

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

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

相关文章

041、基于CNN的样式迁移

之——基于CNN的滤镜 目录 之——基于CNN的滤镜 杂谈 正文 1.基于CNN的样式迁移 2.实现 杂谈 通过CNN的特征提取&#xff0c;可以实现将一个图片的样式模式特征迁移到另一张图像上。 正文 1.基于CNN的样式迁移 就是在某些层的输出上用其他的图片进行监督。 2.实现 一般来…

LeetCode 483. 最小好进制(二分查找)

文章目录1. 题目2. 解题1. 题目 对于给定的整数 n, 如果 n 的 k&#xff08;k>2&#xff09;进制数的所有数位全为1&#xff0c;则称 k&#xff08;k>2&#xff09;是 n 的一个好进制。 以字符串的形式给出 n, 以字符串的形式返回 n 的 最小 好进制。 示例 1&#xff…

LeetCode LCS 01. 下载插件

文章目录1. 题目2. 解题1. 题目 小扣打算给自己的 VS code 安装使用插件&#xff0c;初始状态下带宽每分钟可以完成 1 个插件的下载。 假定每分钟选择以下两种策略之一: 使用当前带宽下载插件将带宽加倍&#xff08;下载插件数量随之加倍&#xff09; 请返回小扣完成下载 n …

LeetCode LCS 02. 完成一半题目(计数+排序)

文章目录1. 题目2. 解题1. 题目 有 N 位扣友参加了微软与力扣举办了「以扣会友」线下活动。 主办方提供了 2*N 道题目&#xff0c;整型数组 questions 中每个数字对应了每道题目所涉及的知识点类型。 若每位扣友选择不同的一题&#xff0c;请返回被选的 N 道题目至少包含多少种…

摄像头图像分析目标物体大小位置_单个运动摄像头估计运动物体深度,谷歌挑战新难题...

雷锋网 AI 科技评论按&#xff1a;人类视觉系统有一个我们习以为然但其实极其强大的功能&#xff0c;那就是可以从平面图像反推出对应的三维世界的样子。即便在有多个物体同时移动的复杂环境中&#xff0c;人类也能够对这些物体的几何形状、深度关系做出合理的推测。然而类似的…

LeetCode LCS 03. 主题空间(广度优先搜索BFS)

文章目录1. 题目2. 解题1. 题目 「以扣会友」线下活动所在场地由若干主题空间与走廊组成&#xff0c;场地的地图记作由一维字符串型数组 grid&#xff0c;字符串中仅包含 “0"&#xff5e;"5” 这 6 个字符。 地图上每一个字符代表面积为 1 的区域&#xff0c;其中 …

SQL SERVER PIVOT 行转列、列传行

在数据库操作中&#xff0c;有些时候我们遇到需要实现“行转列”的需求&#xff0c;例如一下的表为某店铺的一周收入情况表&#xff1a; WEEK_INCOME(WEEK VARCHAR(10),INCOME DECIMAL) 我们先插入一些模拟数据&#xff1a; INSERT INTO WEEK_INCOME SELECT 星期一,1000 UNION…

mysql cluster 查看数据库表名称_MySQL Cluster如何创建磁盘表方法解读

MySQL Cluster采用一系列的Disk Data objects来实现磁盘表;接下来为您详细介绍一、概念MySQL Cluster采用一系列的Disk Data objects来实现磁盘表。Tablespaces&#xff1a;作用是作为其他Disk Data objects的容器。Undo log files&#xff1a;存储事务进行回滚需要的信息&…

(运算符) 运算符

& 运算符既可作为一元运算符也可作为二元运算符。 备注 一元 & 运算符返回操作数的地址&#xff08;要求 unsafe 上下文&#xff09;。 为整型和 bool 类型预定义了二进制 & 运算符。 对于整型&#xff0c;& 计算操作数的逻辑按位“与”。 对于 bool 操作数&am…

LeetCode 1903. 字符串中的最大奇数

文章目录1. 题目2. 解题1. 题目 给你一个字符串 num &#xff0c;表示一个大整数。 请你在字符串 num 的所有 非空子字符串 中找出 值最大的奇数 &#xff0c;并以字符串形式返回。如果不存在奇数&#xff0c;则返回一个空字符串 “” 。 子字符串 是字符串中的一个连续的字符…

mysql分页插件springboot_SpringBoot--使用Mybatis分页插件

1、导入分页插件包和jpa包org.springframework.bootspring-boot-starter-data-jpacom.github.pagehelperpagehelper-spring-boot-starter1.2.52、增加分页配置# 主键自增回写方法,默认值MYSQL,详细说明请看文档mapper:identity: MYSQL# 设置 insert 和 update 中&#xff0c;是…

LeetCode 1904. 你完成的完整对局数

文章目录1. 题目2. 解题1. 题目 一款新的在线电子游戏在近期发布&#xff0c;在该电子游戏中&#xff0c;以 刻钟 为周期规划若干时长为 15 分钟 的游戏对局。 这意味着&#xff0c;在 HH:00、HH:15、HH:30 和 HH:45 &#xff0c;将会开始一个新的对局&#xff0c;其中 HH 用一…

LeetCode 1905. 统计子岛屿(BFS)

文章目录1. 题目2. 解题1. 题目 给你两个 m x n 的二进制矩阵 grid1 和 grid2 &#xff0c;它们只包含 0 &#xff08;表示水域&#xff09;和 1 &#xff08;表示陆地&#xff09;。 一个 岛屿 是由 四个方向 &#xff08;水平或者竖直&#xff09;上相邻的 1 组成的区域。 任…

vue是什么软件_Angular vs React vs Vue:2020年的最佳选择是什么?

在2020年&#xff0c;想象没有HTML&#xff0c;CSS和Javascript的Web开发是不切实际的。 Javascript是Web应用程序前端开发的灵魂。 如果您登陆此页面&#xff0c;那么我认为您在Java语言和Java编程语言的不同框架和库之间感到困惑。企业和软件开发人员最常见的一些查询是&…

LeetCode 1910. 删除一个字符串中所有出现的给定子字符串

文章目录1. 题目2. 解题1. 题目 给你两个字符串 s 和 part &#xff0c;请你对 s 反复执行以下操作直到 所有 子字符串 part 都被删除&#xff1a; 找到 s 中 最左边 的子字符串 part &#xff0c;并将它从 s 中删除。 请你返回从 s 中删除所有 part 子字符串以后得到的剩余…

tcp长连接和短连接的区别_TCP --- 连接

一个TCP连接由4个元组组成&#xff1a;2个ip地址和2个端口号tcp三次握手为什么是三次握手解决历史连接问题通过三次握手才能阻止重复历史连接的初始化通过三次握手&#xff0c;才能对通讯双方的初始序号初始化如果只有2次握手&#xff0c;发送方一旦发送创建连接的请求就无法撤…

LeetCode 1911. 最大子序列交替和(动态规划)

文章目录1. 题目2. 解题1. 题目 一个下标从 0 开始的数组的 交替和 定义为 偶数 下标处元素之 和 减去 奇数 下标处元素之 和 。 比方说&#xff0c;数组 [4,2,5,3] 的交替和为 (4 5) - (2 3) 4 。 给你一个数组 nums &#xff0c;请你返回 nums 中任意子序列的 最大交替和…

LeetCode 1912. 设计电影租借系统(map+set)

文章目录1. 题目2. 解题1. 题目 你有一个电影租借公司和 n 个电影商店。 你想要实现一个电影租借系统&#xff0c;它支持查询、预订和返还电影的操作。 同时系统还能生成一份当前被借出电影的报告。 所有电影用二维整数数组 entries 表示&#xff0c;其中 entries[i] [shopi…

python第一周心得_python第一周心得-Go语言中文社区

Python 简介Python 作为一个近年备受好评的语言&#xff0c;它的一些优点让人无法忽视。Python 是一个高层次的结合了解释性、编译性、互动性和面向对象的脚本语言。它的设计具有很强的可读性&#xff0c;相比其他语言经常使用英文关键字&#xff0c;其他语言的一些标点符号&am…

使用预训练模型进行句对分类(Paddle、PyTorch)

文章目录1. Paddle2. PyTorch3. 提交结果分别使用两种框架&#xff0c;加载预训练模型&#xff0c;对句对进行分类 数据下载&#xff1a;千言数据集&#xff1a;文本相似度 1. Paddle 可以使用 paddlenlp 直接加载预训练模型&#xff0c;比较方便 # %% # 比赛地址 # https…