UE4GC简介

UE4为我们搭建了一套UObject对象系统,并且加入了垃圾回收机制,使我们用C++进行游戏开发时更加方便,而且游戏本身也可以极大程度的避免了内存泄漏问题。

UE4采用了标记-清扫垃圾回收方式,是一种经典的垃圾回收方式。一次垃圾回收分为两个阶段。第一阶段从一个根集合出发,遍历所有可达对象,遍历完成后就能标记出可达对象和不可达对象了,这个阶段会在一帧内完成。第二阶段会渐进式的清理这些不可达对象,因为不可达的对象将永远不能被访问到,所以可以分帧清理它们,避免一下子清理很多UObject,比如map卸载时,发生明显的卡顿。

GC发生在游戏线程上,对UObject进行清理,支持多线程GC。

对GC可以设置若干参数,比如MaxObjectsInGame,规定了游戏中最大存在的UObject对象(对编辑器不生效),移动平台上默认设置了131072,当UObject数量超过这个阈值时,游戏会崩溃,其他详细参数可见UGarbageCollectionSettings,GarbageCollection.cpp,UnrealEngine.cpp中相关的属性。

下图为标记-清扫的工作原理:

GC何时进行:

UE4中GC可以分为主动引发和自动引发两种方式

主动引发

可以在执行一些操作时手动调用GC,比如卸载一个资源后,立即调用一次GC进行清理。

而且方式有多种,游戏中可以调用ForceGarbageCollection来让World下次tick时进行垃圾回收。也可以直接调用CollectGarbage进行垃圾回收,引擎中大部分情况都用这种方式主动引发。

自动引发

游戏中,大部分的垃圾回收操作都是由UE4自动引发的,普通情况下不需要手动调用GC,这也是理想的GC使用方式。

当World进行tick时,会调用UEngine::ConditionalCollectGarbage()函数,函数中进行了一些判断,当满足GC条件时,才会执行GC。下面分析一下ConditionalCollectGarbage的执行逻辑。

UE4GC流程

入口为UObjectGlobals.h中定义的CollectGarbage()函数,如下:

void CollectGarbage(EObjectFlags KeepFlags, bool bPerformFullPurge)
{// No other thread may be performing UObject operations while we're runningAcquireGCLock();// Perform actual garbage collectionCollectGarbageInternal(KeepFlags, bPerformFullPurge);// Other threads are free to use UObjectsReleaseGCLock();
}

过程包括3个部分,获取GC锁,执行CollectGarbageInternal,释放GC锁。

获取GC锁

因为GC是多线程的,因此要设置GC锁,防止其他线程做UObject相关操作,会与GC冲突,这主要用于保护异步加载过程。

一个作用为防止一个对象被加载后,存储的变量还没来得及添加引用,就被当作不可达垃圾回收掉了。如下代码就是一个例子,FGCScopeGuard起到了阻止任何GC操作的作用。

FGCSyncObject

GC锁是一个广义的概念,其实是FGCSyncObject这个单例类,其内部封装了多个用于锁和同步的变量,可以用于在GC运行时阻塞其他non-game线程,也可以在non-game线程执行关键操作时阻塞GC线程。当然,也并非所有情况都会阻塞,当不能立即获取到GC锁时各个线程也可以根据具体逻辑执行其他内容。

主要成员变量如下:

FThreadSafeCounter AsyncCounter:是一个线程安全计数器,当由线程执行关键Async操作时,会对这个值进行增减

FThreadSafeCounter GCCounter:用作GC锁,不为0表示线程已经获取了GC锁,正在执行GC

FThreadSafeCounter GCWantsToRunCounter:这个计数器表示线程意向进行GC,但尚未获取到GC锁,Async线程没有自动强制实现这个逻辑,需要我们手动实现对这个变量的支持

FCriticalSection Critical:线程执行GC相关操作的关键区的保护,防止其他人进入

FEvent* GCUnlockedEvent:通知non-game线程GC正在执行的event,可执行Wait(),Trigger(),类似给线程的signal

对GC锁有了基本认识后,接下介绍一下获取GC锁的过程:

执行CollectGarbageInternal

执行CollectGarbageInternal,进行垃圾回收,进行标记与清扫

该函数接受2个参数:KeepFlags,bPerformFullPurge。KeepFlags表示有这些标记的object无论是否被引用到,都会被保留。bPerformFullPurge表示是否在标记后进行全purge,而不是分帧递增清除。

执行流程如下:

其中可以看到几个注意点:

  1. 这个流程一定会执行扫描对象可达性操作,黄色方框为具体的流程,稍后会做分析。清理操作视情况而执行,如有需要才会进行对象清理,而且一定是完全清理,否则就在World tick里面做增量清理。
  2. UE4会运行在多线程环境,GC又会对所有UObject进行操作,所以一定要注意锁的使用。
  3. GC本身可以多线程进行,加快速度

标记流程

使用FRealtimeGC的PerformReachabilityAnalysis方法进行uobject可达性分析。FRealTimeGC继承自FGarbageCollectionTracer,可以多线程、实时的分析对象引用关系。

关键代码如下:

// Make sure GC referencer object is checked for references to other objects even if it resides in permanent object pool
if (FPlatformProperties::RequiresCookedData() && FGCObject::GGCObjectReferencer && GUObjectArray.IsDisregardForGC(FGCObject::GGCObjectReferencer))
{ObjectsToSerialize.Add(FGCObject::GGCObjectReferencer);
}{const double StartTime = FPlatformTime::Seconds();MarkObjectsAsUnreachable(ObjectsToSerialize, KeepFlags, bForceSingleThreaded);UE_LOG(LogGarbage, Verbose, TEXT("%f ms for Mark Phase (%d Objects To Serialize"), (FPlatformTime::Seconds() - StartTime) * 1000, ObjectsToSerialize.Num());
}{const double StartTime = FPlatformTime::Seconds();PerformReachabilityAnalysisOnObjects(ArrayStruct, bForceSingleThreaded);UE_LOG(LogGarbage, Verbose, TEXT("%f ms for Reachability Analysis"), (FPlatformTime::Seconds() - StartTime) * 1000);
}

这里用到了一个FGCArrayStruct类型数据结构ArrayStruct,用于存储用于序列化uobject的array和weak reference列表。

第一步,我们可以向ObjectsToSerialize添加FGCObject::GGCObjectReferencer

后者是一个静态的UGCObjectReferencer,添加后可用于在非UObject对象上调用AddReferencedObjects方法。

第二步,调用MarkObjectsAsUnreachable方法,把所有不带KeepFlags和EInternalObjectFlags::GarbageCollectionKeepFlags标记的对象标记为不可达

首先,这里涉及到GUObjectArray这个变量,这是一个全局的Uobject allocator,其中的ObjObjects数组保存了所有的UObject(通过FUObjectItem进行封装),UObjectBase::InternalIndex属性就是对象对应的FUObjectItem在数组中的下标,因此可以方便的根据下标找到UObject或者通过UObject找到对应下标。

GUObjectArray中前部存储了一些不纳入GC的object,因此扫描的object列表中会去掉前面这些object,只考虑后面的,得到MaxNumberOfObjects。具体哪些对象不被GC考虑,可以查看FUObjectArray的实现。

接下来就需要对这些uobject进行不可达标记,这里使用了多线程版本的For循环。多线程执行的原理并不复杂,首先可以获取当前可用的工作线程,然后把待标记的object平均分配给这些线程进行遍历,多线程底层使用了UE的GraphTask框架。在对一个uobject进行标记时,正常情况下都读对应的FUObjectItem中属性,特殊情况才读uobject,因为FUObjectItem是一个结构体,而且在GUObjectArray中紧密排列,所以在顺序遍历下是缓存友好的。

值得一提的是,UE使用了簇(Cluster)来提高效率,具体如何提高会在下面介绍。如果一个object属于RootSet,则直接加入到ObjectsToSerializeList中,如果是ClusterRoot或在Cluster中,也加入到KeepClusterRefsList列表中。如果object的ClusterRootIndex<=0(不在cluster中或者为ClusterRoot),则先根据是否有KeepFlags,判断是否要标记为不可达,如果不要标记,则把object加到ObjectsToSerializeList中,且如果为ClusterRoot就加入到KeepClusterRefsList中,如果要标记,则加入到ClustersToDissolveList中,且对ObjectItem设置Unreachable标记。会对Cluster做一些额外的处理,细节可看代码。

第三步,调用PerformReachabilityAnalysisOnObjects来判断uobject可达性

这里会用到FGCReferenceProcessor,TFastReferenceCollector,FGCCollector这几个类,都同时支持单线程和多线程。

先介绍一下ReferenceToken概念

在UObject体系中,每个类有一个UClass实例用于描述该类的反射信息,使用UProperty可描述每个类的成员变量,但在GC中如果直接遍历UProperty来扫描对象引用关系,效率会比较低(因为存在许多非Object引用型Property),所以UE创建了ReferenceToken,它是一组toke流,描述类中对象的引用情况。下图中列举了引用的类型:

/*** Enum of different supported reference type tokens.*/
enum EGCReferenceType
{GCRT_None          = 0,GCRT_Object,GCRT_PersistentObject,GCRT_ArrayObject,GCRT_ArrayStruct,GCRT_FixedArray,GCRT_AddStructReferencedObjects,GCRT_AddReferencedObjects,GCRT_AddTMapReferencedObjects,GCRT_AddTSetReferencedObjects,GCRT_EndOfPointer,GCRT_EndOfStream,
};

FGCReferenceTokenStream

这个类用于创建tokenstream和从tokenstream中解析出object引用,可以算是GC的一个核心理念了。ReferenceToken在其中保存为TArray<uint32>的形式,为什么是这种形式呢,下面就分析一下ReferenceToken的工作原理:

FGCReferenceInfo这个类描述了一个引用所需的信息,有一个union成员变量:

/** Mapping to exactly one uint32 */
union
{/** Mapping to exactly one uint32 */struct{/** Return depth, e.g. 1 for last entry in an array, 2 for last entry in an array of structs of arrays, ... */uint32 ReturnCount    : 8;/** Type of reference */uint32 Type         : 4;/** Offset into struct/ object */uint32 Offset      : 20;};/** uint32 value of reference info, used for easy conversion to/ from uint32 for token array */uint32 Value;
};

Type:引用的类型,就是EGCRefenceType

Offset:这个引用对应的属性在类中的地址偏移

ReturnCount:返回的嵌套深度

UE巧妙的把这3个信息编码成了一个uint32,因此FGCReferenceTokenStream可以通过TArray<uint32>形式存储tokens。

当我们处理TokenStream时,可以先从中解析出一个个referencetoken,然后通过Offset直接获取属性,不仅处理起来更简单,更能有效利用缓存,加快速度。

TokenStream还有一种特殊的用法,就是用两个连续的token来存储一个指针(64位),比如运行时可以通过执行AddReferencedObjects来动态添加引用的对象,而这个函数的指针就储存在TokenStream中。

UClass::AssembleReferenceTokenStream(bool bForce)方法

可以实时创建tokenstream,只需执行一次,就能把结果保存下来,并在ClassFlags中通过CLASS_TokenStreamAssembled进行体现,避免重复计算。如果之前已经创建过TokenStream,就替换调旧的。

具体流程为:

  1. 遍历自身的UProperty(不包括父类的),依次调用UProperty的EmitReferenceInfo方法。这是一个虚函数,不同的UProperty会实现它,主要会把自己在Class中的内存偏移,ReferenceType信息发送给UClass,UClass再通过EmitObjectReference把这个引用信息编码成token,加入到ReferenceTokenStream中。不同的UProperty处理方式有很大区别,普通的UObjectProperty比较好处理,UArrayProperty和UMapProperty就比较复杂,因为它们内部的数据类型也需要生成TokenStream,如果碰到struct,还会涉及到递归。
  2. 如果这个类有父类,则递归调用父类的AssembleReferenceTokenStream方法,生成父类的ReferenceTokenStream,并把父类的stream添加到自己的stream之前。这个步骤会一直到UObjectBase这个类为止,UObjectBase的处理方式比较特殊,只会把ClassPrivate和OuterPrivate添加到stream中。
  3. 如果自身的AddReferencedObjects()函数不是指向Uobject::AddReferencedObjects,则向TokenStream中加入或更新这个函数指针对应的token,在执行可达性分析时即可调用到这个函数了。
  4. TokenStream添加完毕,把"EndOfStream"token添加到TokenStream,并对tokens array进行shrink,去掉空闲的array slack,因为toneks数组长度在接下来应该是固定的。
  5. ClassFlags中把CLASS_TokenStreamAssembled设为true。

TFastReferenceCollector

CollectReferences方法用于可达性分析,如果时单线程,就直接调用ProcessObjectArray方法,遍历uobject的token stream来寻找引用关系。如果是多线程,就会把uobject列表分割给多个线程处理,每个线程同样会调用到ProcessObjectArray。

ProcessObjectArray方法会遍历ObjectsToSerialize中的UObject,找到引用关系,判断可达性。注意,过程中ObjectsToSerialize会不断增长,直到全部遍历完。内部使用了递归的方法,但用栈来模拟。

  1. 如果是单线程且开启了自动生成tokenstream,则当object对应的UClass还没有tokenstream时,就实时调用UClass的AssembleReferneceTokenStreams创建tokenstream
  2. 获取当前uobject的TokenStream,解析出FGCReferenceInfo,来找到正被引用的UObject。

token的ReferenceInfo会是不同的类型,需要分多种情况处理。像GCRT_Object和GCRT_ArrayObject都比较好处理,只要把其中的uobject对象添加到ObjectsToSerialize中就行了。

GCRT_ArrayStruct就比较麻烦,需要递归处理。这里说的"struct"并不单指C++中的struct结构体,一些不属于UObject体系的class也算,比如UEdGraphPin。处理GCRT_ArrayStruct时,需要先把递归的栈递增,然后逐个处理Array中的"Struct"。

GCRT_AddStructReferencedObjects表示struct或不继承自FGCObject的class也可以对UOBject添加引用关系,UStructProperty::EmitReferenceInfo中代码也确实显示structproperty可以添加引用。但看代码和注释,觉得UE4应该以后会把这些特殊的struct和class都继承FGCObject,使用AddReferencedObjects函数来添加引用。

GCRT_AddReferencedObjects就表示需要调用这个对象的AddReferencedObjects函数来添加引用。让我们回想一下FGCObject,这个类不继承UObject,但也能通过AddReferencedObjects函数来对UObject添加引用,同时这个函数又只能由UClass来添加到TokenStream中,那FGCObject是怎么工作的?其实UE中有一个专门的UObject实例来管理FGCObject,就是UGCObjectReferencer,看一下这个类的AddReferencedObjects函数:

void UGCObjectReferencer::AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector)
{   UGCObjectReferencer* This = CastChecked<UGCObjectReferencer>(InThis);// Note we're not locking ReferencedObjectsCritical here because we guard// against adding new references during GC in AddObject and RemoveObject.// Let each registered object handle its AddReferencedObjects callfor (FGCObject* Object : This->ReferencedObjects){check(Object);Object->AddReferencedObjects(Collector);}Super::AddReferencedObjects( This, Collector );
}

引用标记阶段搜集到这个类的实例时,它会逐个调用FGCObject上的AddReferencedObjects方法,搜集UObject引用,从而把FGCObject纳入到GC体系中。

3. 得到被引用的UObject后,一般会对其添加引用,并加入到ObjectsToSerialize数组中。

如果一个UObject已经被标记为isPendingKill了,那么即使它被引用到,也会忽略。

由于标记可以多线程进行,因此有可能两个线程同时对一个对象标记为可达,并加入到ObjectsToSerialize数组,继续进行引用检查,这显然是浪费的。因此对一个对象进行标记时,不仅要检查这个对象当前是否为Unreachable,清理它的Unreachable标记也要有一个原子的“比较再替换”操作,防止两个线程碰巧同时设置。

如果这个UObject在Cluster中,则把它标记为ReachableInCluster,同时如果需要也把它的ClusterOwner标记为可达,并加入到ObjectsToSerialize中做后续处理。

4. 对ObjectsToSerialize数组的扫描会一轮一轮进行,一轮扫描过程中扫描到的新的UObject会暂时存放在NewObjectsToSerialize数组中,当对ObjectsToSerialize一轮扫描完时,如果NewObjectsToSerialize中元素数量超过MinDesiredObjectsPerSubTask这一阈值,则新开多个线程处理,如果不到阈值,则在当前线程中继续新的一轮处理。

得到待清理Uobject列表

首先介绍一下Cluster概念:

对于复合性的逻辑物体,其内部的Object随父物体的状态进行管理,可以用Cluster来进行GC管理。Cluster是一组UObject,在GC流程中被视为一个单一的单位,能加速GC。粒子系统中,一组粒子对象就被标记为一个Cluster。Actor也可以通过设置“can be in Cluster”熟悉把自己加到一个Cluster中。

在程序中的表示为FUObjectCluster这个类。其中Objects属性为Cluster中的所有Objects,ReferencedClusters为这个cluster引用的其他Cluster。

当一个Cluster引用的其他Cluster根对象被标记为PendingKill时,就要把这个Cluster中的所有Object都加入到ObjectsToSerialize数组中,来处理其他的pendingkill引用。同时,这个Cluster将会被标记为需要分解,因为cluster间的引用关系已经得不到保证了。

在分析完可达对象后,需要对标记为分解的Cluster进行分解操作。

之后,需要再扫描一遍所有的UObject,搜集所有不可达对象。这个操作也可以多线程并行处理。对于不可达对象,如果不在Cluster中,就把它直接加入到GUnreachableObjects数组中。如果是ClusterRoot,则需要对它所包含的所有Objects进行分析,如果Object没有来自Cluster外部的引用,则也为不可达,需要加入到GUnreachableObjects数组中。至此,GUnreachableObjects数组中的UObject就是这次扫描得到的待清理对象了。

释放GC锁

去掉GC锁,允许其他线程使用UObject,释放的流程比获取的流程简单很多

  1. 调用GCUnlockedEvent->Trigger(),唤醒正在等待这个event的线程
  2. GCCounter递减,表示退出GC状态

清理流程

清理和引用标记可以分开进行,通常清理是分帧递增执行的。

调用IncrementalPurgeGarbage( bool bUseTimeLimit, float TimeLimit )可进行垃圾清理,接受两个参数。

bUseTimeLimit:清理是否有时间限制

TimeLimit:方法一次执行的时间限制

通过这两个参数可以选择性的进行递增清理或者全量清理。

首先,会调用UnhashUnreachableObjects方法,对所有的不可达且未设置RF_BeginDestroyed的UObject调用BeginDestroy方法,通知这个UObject它将要被销毁,可以让UObject进行一些异步的清理操作,我们可以覆写BeginDestroy方法,执行自定义的操作。

之后,我们就可以获取GC锁了。

遍历要清理的对象,调用这些对象的FinishDestroy方法。但是这是对象有可能还没执行完BeginDestroy,因为有一些异步操作,比如一些图形资源在等待渲染线程使用完毕,所以对这些对象不能立即调用FinishDestroy,而要把它们先储存到GGCObjectsPendingDestruction数组中,之后再处理。遍历过程中会检查已过的时间,如果时间限制到了,就会中途停止,并记下当前的进度,之后会退出清理流程。

如果我们把所有要清理的对象都遍历完了,即对它们都尝试调用了FinishDestroy方法,那么会再遍历一遍GGCObjectsPendingDestruction数组,再尝试执行一下FinishDestroy。当对GGCObjectsPendingDestruction的这一次遍历完成后,就要分是否使用TimeLimit分情况看了。如果使用了TimeLimit,就直接执行下面逻辑,下次再处理GGCObjectsPendingDestruction中的对象。如果不使用,就会阻塞在这个地方,强行等待哪些UObject执行完BeginDestroy,主要还是等待渲染线程。当GGCObjectsPendingDestruction中的所有对象都执行了FinishDestroy后,才会进行下一步的清理操作。

接下来会执行真正的清理过程。主要会遍历待清理对象,执行UObject的析构函数,并释放内存空间。这个步骤也会检查释放超过了TimeLimit,超过的化会在下一帧继续清理。

指向UObject的指针如何更新为NULL

这是一个引申出来的问题。我们可以显示调用Uobject的Destroy()函数,这样下次垃圾回收时这个UObject就会被回收掉,但是当前还指向这个UObject的UProperty指针,或者容器中的指针,怎么办呢,会不会造成“悬空指针”问题呢?答案是不会的,使用过UE4就知道,判定一个UProperty指针是否有效,只要判定一下是否为空即可,为我们进行C++开发提供了很多便利。UE4在一个对象销毁时,会自动把指向这个对象的UProperty指针更新为Null,不是UProperty的指针不会被更新,依然会有悬空指针问题。UE4的官方文档中有比较详细的描述,https://docs.unrealengine.com/en-US/Programming/UnrealArchitecture/Objects/Optimizations/index.html (Automatic Updating of References)。

指针自动更新实现原理

在可达性分析阶段,我们会解析每个对象的tokenstream,找到当前对象引用的其他对象,如果其他对象已经被标记为PendingKill了,就会把指向它的指针置为NULL。所以指针更新流程是先把所有指向将要销毁对象的指针置为NULL,再销毁这个对象,这样就肯定不会出现悬空指针问题了。

【UE4基础】UE4 垃圾回收相关推荐

  1. 【JVM基础】垃圾回收算法详解(引用计数、标记、清除、压缩、复制)

    前言 笔记参考 Java 全栈知识体系.星羽恒.星空茶 文章目录 前言 垃圾回收概述 引用计数法 案例 优点 缺点 标记.清除.压缩 标记 清除 压缩 标记清除算法 优点 缺点 标记压缩算法 优点 缺 ...

  2. java基础之垃圾回收_繁星漫天_新浪博客

    在java中,当一个对象成为垃圾后仍会占用内存空间,时间一长,就会导致内存空间的不足.针对这种情况,java中引入了垃圾回收机制.程序员不需要过多的关心垃圾对象回收的问题,java虚拟机会自动回收垃圾 ...

  3. Lua基础之垃圾回收

    Lua内存管理机制 Lua 使用的是垃圾自动回收机制. Lua 主要是通过运行一个垃圾收集器来收集所有垃圾(Lua 中不会被访问到但还没销毁的对象)以完成自动内存管理的工作. Lua的垃圾清理过程由4 ...

  4. Java基础:JVM垃圾回收算法

    众所周知,Java的垃圾回收是不需要程序员去手动操控的,而是由JVM去完成.本文介绍JVM进行垃圾回收的各种算法. 1. 如何确定某个对象是垃圾 1.1. 引用计数法 1.2. 可达性分析 2. 典型 ...

  5. JavaScript基础09-day11【原型对象、toString()、垃圾回收、数组、数组字面量、数组方法】

    学习地址: 谷粒学院--尚硅谷 哔哩哔哩网站--尚硅谷最新版JavaScript基础全套教程完整版(140集实战教学,JS从入门到精通) JavaScript基础.高级学习笔记汇总表[尚硅谷最新版Ja ...

  6. Java垃圾回收机制(Garbage Collection)

    引用博客地址:http://www.cnblogs.com/ywl925/p/3925637.html 以下两篇博客综合描述Java垃圾回收机制 第一篇:说的比较多,但是不详细 http://www. ...

  7. JVM—垃圾回收与算法

    目录 一.如何确定垃圾 1.引用计数法 2.可达性分析 二.标记清除算法(Mark-Sweep) 三.复制算法(copying) 四.标记整理算法(Mark-Compact) 五.分代收集算法 1.新 ...

  8. 6种java垃圾回收算法_被说烂了的Java垃圾回收算法,我带来了最“清新脱俗”的详细图解...

    一.概况 理解Java虚拟机垃圾回收机制的底层原理,是系统调优与线上问题排查的基础,也是一个高级Java程序员的基本功,本文就针对Java垃圾回收这一主题做一些整理与记录.Java垃圾回收器的种类繁多 ...

  9. 【Java】Java垃圾回收机制

    Java垃圾回收机制 说到垃圾回收(Garbage Collection,GC),很多人就会自然而然地把它和Java联系起来.在Java中,程序员不需要去关心内存动态分配和垃圾回收的问题,这一切都交给 ...

  10. 垃圾回收算法_Java 垃圾回收算法与几种垃圾回收器

    一.如何确定某个对象是"垃圾"? 目前主流垃圾回收器都采用的是可达性分析算法来判断对象是否已经存活,不使用引用计数算法判断对象时候存活的原因在于该算法很难解决相互引用的问题.如何确 ...

最新文章

  1. U-Mail邮件网关测试勒索病毒样例图
  2. 超大 Cookie 拒绝服务攻击
  3. Android数据库LitePal的存储操作
  4. 10通信端口感叹号_S71200 技术篇——MODBUS TCP通信
  5. php获取当前时间戳方法
  6. 电脑怎么结束进程_深刻了解windows系统的任务管理器,电脑高手的成长之路
  7. c语言实现数据结构中的链式表
  8. 9个元素换6次达到排序序列_面试题精选(排序算法类)c/c++版 上篇
  9. jbutton如何实现点击_点击量突破22.1亿人次!这场云上祈福拜祖是如何实现的
  10. mysql加入时间戳sql语句,SQL插入时间戳问题
  11. 初次接触面元法对螺旋桨的性能预报,发现之前很多学者都是用fortran进行编程进行性能预报,为什么不用matlab呢,两者的差异在哪里,建议初学者用这哪个软件呢
  12. 16/4/4二代支付硬盘故障处理
  13. 超参数调优方法整理大全
  14. dw自动滚动图片_Dreamweaver实现滚动图片文字
  15. [Lorg/openxmlformats/schemas/spreadsheetml/x2006/main/CTPhoneticRun报错
  16. c语言用户态锁使用,用户态自旋锁、读写自旋锁及互斥锁
  17. [Elasticsearch]4.可伸缩性解密:集群、节点和分片
  18. 智原领先推出网通ASIC专用28纳米28G可编程SerDes
  19. Kafka之Fetch offset xxx is out of range for partition xxx,resetting offset情况总结
  20. 星星下落_与星星共舞

热门文章

  1. 【渝粤题库】国家开放大学2021春2403外科护理学题目
  2. java二级考什么_计算机二级主要考什么内容?
  3. 机器学习深度学习 常用算法推导
  4. 金山WPS:云端协同 AI赋能 WPS树起了Office新四大件|企服三会系列报道
  5. matlab(1):使用matlab处理excel数据进行画图
  6. 2455. 可被三整除的偶数的平均值
  7. 炎炎夏日,深夜详谈nginx的配置中location和rewrite的语法规则(从入门到高手的第六步)
  8. 电阻(电阻器)学习干货
  9. python可视化窗口库_Python可视化工具介绍——找到合适的库
  10. linux wenj 立即生效_Linux系统调用(转载)