Unity Dots理论学习-5.与ECS相关的概念

DOTS的面向数据编程方式比你在MonoBehaviour项目中常见的面向对象编程方式更适合硬件开发。可以尝试理解一些与数据导向设计(DOD)相关的关键概念,以及这些概念如何影响你的代码,对你在MonoBehaviour项目中的C#编程通常是较少涉及的。

内存分配和垃圾回收

在现代操作系统中,程序作为独立进程运行,每个进程的内存由操作系统管理。当一个进程需要更多内存时,它必须向操作系统请求,操作系统会给该进程分配一个连续的内存块,这称为内存分配

当进程终止时,操作系统会回收内存,将其释放以供其他地方使用。然而,对于长时间运行的程序来说,通常合理的做法是将不再使用的内存块交还给操作系统,这称为内存释放内存去分配。在简单的短生命周期程序中,通常只需要分配内存而不需要释放它。但如果一个长期运行的程序不断进行新的内存分配,却忽视释放它们,那么程序可能会使用不合理的内存量,这种情况称为内存泄漏,它可能导致性能下降和不稳定。

程序通常使用自己的内部分配器,其工作方式如下:

  1. 程序从操作系统分配一个大块内存。
  2. 程序的内部分配器跟踪该内存块中当前正在使用的范围。
  3. 当程序需要更多内存时,它向内部分配器请求,而不是向操作系统请求。
  4. 当这些内部分配的内存块不再需要时,程序应通知其分配器释放内存。

内部分配器有一些优点:

  • 与从操作系统进行分配和去分配不同,从内部分配器进行分配和去分配通常不需要昂贵的系统调用。
  • 程序可以使用多个自定义分配器,以更好地适应不同的使用场景:一些分配器更适合小型、短期的分配,而另一些分配器更适合大型、长期的分配。例如,所谓的“竞技场分配器”(arena allocator)会同时释放它的所有分配,因此它的内部逻辑和账簿管理非常简单且便宜。

在许多现代编程语言中,包括C#,运行时使用垃圾回收器来扫描内存,找到未使用的分配并释放它们。与手动分配相比,这种自动化方式更方便,可以更容易地避免内存泄漏和其他内存相关问题。但不利的一面是,垃圾回收会产生开销,并且需要中断程序的执行,这可能会导致明显的暂停,从而负面影响玩家体验。

在DOTS中,实体和本地集合是非托管的,这意味着它们不由运行时或其垃圾回收器分配或管理:

  • 对于实体,内存由EntityManager为你分配和释放,因此只有当你忽视销毁不再需要的实体时,它们才会发生内存泄漏。在实际使用中,这种情况通常容易被察觉,因此这种错误也容易被检测并修正。
  • 对于本地集合,DOTS提供了几种具有不同权衡的分配器。例如,Allocator.Temp分配器提供非常廉价的分配,这些分配会在帧结束或job完成时自动释放。与此相对,Allocator.Persistent分配器提供更昂贵的分配,这些分配会一直存在,直到手动释放。与Allocator.Temp分配器不同,Allocator.Persistent分配器可以用于较大的分配,并且可以传递给job。其他分配器包括Allocator.TempJobWorldUpdateAllocator

多线程编程

大多数现代CPU有多个核心,充分利用这些额外的核心可以极大提升CPU密集型游戏的性能。然而,多线程编程可能既困难又不安全,因为它需要大量低级手动编程,对于许多C#开发者来说,这可能是陌生的领域。在DOTS中,C#Job系统帮助你以简单的方式编写安全的多线程代码,避免常见的陷阱,但本节将帮助你理解潜在的问题。

当操作系统启动一个进程时,它一开始只有一个执行线程。通过系统调用,进程可以启动额外的线程,这些线程属于同一个进程,因此共享相同的内存。

每个CPU的逻辑核心一次只能运行一个线程,操作系统控制哪些线程在什么时间在哪个核心上运行。任何时候,操作系统都可以中断一个正在运行的线程并将其挂起,以便其他线程可以使用该核心。当两个线程同时访问同一资源(即数据)时,问题在于,一个线程可能会在另一个线程没有预料到的情况下改变该资源。一般来说,线程应该仅在“临界区”内读取和修改共享资源——在临界区中,线程对共享资源拥有独占访问权限。

为了控制线程间的访问,某些共享资源由某种同步原语管理,如互斥锁(mutex)。然而,这些同步原语通常要求所有使用它们的线程遵循严格的协议,若未遵守协议,可能会导致原语无效或使程序死锁。

另一个问题是,某些系统调用可能会阻塞调用线程,意思是暂停线程的执行。例如,当一个线程调用系统调用读取文件时,数据可能尚未在内存中,因此必须先从设备复制到内存中,才能返回系统调用。由于等待数据可能会非常长(在CPU层面上),操作系统可能会在数据加载时阻塞调用线程,同时另一个线程可以在CPU核心上运行。只有当数据准备好后,操作系统才会解除阻塞,允许该线程继续执行。

因此,多线程的一种用例是将较长时间的阻塞操作放到“后台线程”中处理,如读取文件和写入文件,同时“主线程”继续执行其他任务。例如,一个交互式程序可能希望在后台加载文件的同时,主线程仍然响应用户输入并重绘屏幕。

在其他情况下,你可能只是希望将程序的CPU工作负载分配到多个核心上,以更快地完成工作。例如,数据压缩是非常消耗CPU的,因此通常可以通过多线程来提高效率。

需要考虑的一点是,CPU核心必须相互争夺存储设备、系统内存和其他系统资源的使用。例如,如果两个线程试图同时访问内存,它们不能在同一时刻访问,而必须轮流访问。幸运的是,这些重叠的问题在硬件层面上会被解决,但问题仍然存在:每个线程的内存访问会减慢其他线程重叠内存访问的速度。因此,对于一个需要大量内存访问而相对于CPU计算较轻的任务,将工作分配到多个线程上往往会产生递减的回报。

例如,你可能假设一个分配到10个线程上的任务应该比仅在一个线程上运行的相同任务快10倍,但由于内存争用,这个理论上的极限在实际中很难实现。相反,10个线程可能更现实地带来5到7倍的性能提升(但这仍然取决于具体的场景)。事实上,如果任务需要相对较高的内存访问量而计算工作较少,使用更少的线程可能会提高性能,因为线程数量较少,内存争用也较少。

内存和CPU缓存

今天大多数CPU有一到三级缓存,通常标记为L1、L2和L3。当CPU执行一个读取系统内存地址的指令时,硬件首先检查该地址的数据是否在L1缓存中。如果没有,硬件会接着检查L2缓存(如果存在),然后是L3缓存(如果存在)。如果没有任何缓存命中,硬件将直接读取系统内存。读取数据后,硬件会将数据复制到更低级的缓存中,例如,从L3读取的数据会被复制到L1和L2缓存,从系统内存读取的数据会被复制到所有缓存级别。

这种缓存策略是有意义的,因为当一个地址被读取时,通常很可能在不久的将来会再次读取同一个地址。通过将数据复制到缓存中,下次需要时可以直接从缓存中读取数据。

当然,并不是所有的系统内存都可以装入缓存:每级缓存的大小都比上一级小,而且最大的缓存仍然远小于整个系统内存。因此,当某部分内存被复制到缓存时,它必须覆盖先前缓存的某些其他部分。

当从缓存读取数据时,叫做“缓存命中”(cache hit)。当没有缓存到需要的数据而必须从系统内存本身读取,则叫做“缓存未命中”(cache miss)。

不同芯片的缓存性能特性差异很大,但大致来说,L1的速度至少是L2的几倍,L2的速度至少是L3的一个数量级,L3的速度至少是系统内存的两倍。总体而言,CPU访问L1缓存中的数据的速度可能比访问系统内存中的数据快两个数量级。由于低级缓存与系统内存之间的速度差距如此之大,减少程序中触发缓存未命中的次数是一个关键的性能优化考量。

由于硬件特性叫做预取(prefetching),最简单且最有效的减少缓存未命中的方式是顺序地访问内存地址,而不是随机跳跃。因此,应当使用缓存高效的数据结构将它们的元素紧密打包并连续存储在内存中,也就是把数据存储在数组中。

当你开始按顺序读取内存地址时,硬件会注意到这种模式,并开始预读取并将数据复制到缓存中,预期你会继续读取。这在某些情况下可能会导致一些浪费的工作,因为额外的数据可能不需要,但在读取数组时,这种预取行为会在CPU需要数据之前将数据加载到缓存中。因此,除了在读取数组的第一个字节时可能发生的初始缓存未命中外,数组可以在不触发缓存未命中的情况下读取。随着CPU的运算“列车”加速,轨道恰好已经在列车前面铺叙完成了。

GameObjectsMonoBehaviours这样的托管C#对象是单独实例化的,它们可能在内存中处于离散状态。因此,遍历托管对象通常需要在内存中跳跃,从而触发很多次缓存未命中的情况。

在DOTS中,实体及其组件通过设计被紧密打包在连续的数组中,这使得它们能够按顺序遍历,最大限度地减少缓存未命中。

面向对象编程(OOP)的成本

对于面向对象编程(OOP)一个常见的质疑就是其定义。有些人坚持认为OOP完全是关于继承、多态、封装,或这三者的结合,而其他人则提供了不太传统的理论。以下是Wikipedia的定义:

“面向对象编程(OOP)是一种编程范式,基于对象的概念,对象可以包含数据和代码:数据以字段的形式(通常称为属性或特性),代码以过程的形式(通常称为方法)。在OOP中,计算机程序是由相互交互的对象构成的。” — Wikipedia

换句话说,面向对象的程序由交互的“对象”组成,每个对象是一个封装的数据和代码单元,具有一定程度的自主性和独立性。就像网络上的程序通过发送消息彼此协作一样,面向对象程序中的对象通过调用彼此的方法来协作,实际上,定义面向对象编程的并不是单独的对象本身,而是对象之间的交互。

OOP的理论优点包括:

组合性:由对象构成的程序可以逐步组装和修改。
可重配置性:通过插入、删除和替换对象,可以轻松添加、删除和修改功能。
代码重用:对象可以在不同程序之间轻松重用。
直观性:现实世界中的事物和过程自然地对应于对象。
抽象性:对象使程序员能够在高层次上解决问题,而不会被低层次的细节分散注意力。

Steve Jobs在1994年6月16日《Rolling Stone》杂志的采访中详细阐述了最后一点:

“对象就像人一样。它们是活生生的、有生命的事物,里面有关于如何做事情的知识,还有可以记住事情的记忆。而不是在非常低层次与它们交互,你可以在非常高层次的抽象中与它们交互,就像我们现在这样。”

这里有一个例子:如果我就是你的洗衣对象,你可以把脏衣服交给我,并告诉我:“你能把我的衣服洗干净吗?”我恰好知道旧金山最好的洗衣店在哪里。我会讲英语,口袋里有美元。我会出去拦一辆出租车,告诉司机带我去旧金山的那个地方。我去把你的衣服洗干净,跳回出租车,回到这里。我给你干净的衣服,并说:“这是你的干净衣服。”

你根本不知道我是怎么做到的。你对洗衣店一无所知。也许你讲法语,甚至无法叫出租车。你不能为出租车付费,口袋里没有美元。然而,我知道如何做到这一切。而你不需要知道其中任何一个细节。所有的复杂性都隐藏在我内部,我们能够在一个非常高层次的抽象上进行交互。这就是对象的意义。它们封装了复杂性,且对外的接口是高层次的。”

面向对象编程(OOP)的性能成本

然而,面向对象编程也会带来一些性能成本:

分散的数据布局:OOP代码通常被拆分成许多小对象,数据通常分散在内存中(这会导致缓存效率低下,正如前面讨论的那样)。
过度抽象:面向对象设计通常鼓励多层委托,其中较高层的代码将实际工作推给较低层,结果是产生许多对象和方法,但它们做的工作很少。
复杂的调用链:由于许多抽象层和偏好短小函数,调用链变得非常复杂。
虚拟调用:虚拟调度表相较于常规函数调用带来了额外的开销,且虚拟调用通常无法内联(尽管某些JIT编译器可能会在运行时进行内联)。
糟糕的分配模式:OOP鼓励的复杂代码路径往往让人难以推理对象的生命周期,因此OOP代码倾向于依赖频繁的小规模内存分配和垃圾回收,而不是更高效的替代方案。
逐个处理:由于直接操作对象的代码是对象本身的一部分,OOP有一种天然的倾向,使得它处理对象时往往是一个接一个地处理,而不是批量处理。

OOP的结构性成本

即使我们愿意为了让程序更易于编写和维护而牺牲最佳性能,OOP在这些领域也可能存在缺点。以下是几个问题:

将数据和代码交织在一起使得两者都变得更加混乱和复杂

通常有一种说法是,OOP优先考虑数据而非代码:

“面向对象编程(OOP)是一种将软件设计围绕数据或对象而非函数和逻辑组织的计算机编程模型。[...] OOP专注于开发者想要操作的对象,而不是操作这些对象所需的逻辑。”
Alexander S. Gillis, 《什么是面向对象编程》,发布于TechTarget Network

然而,实际上,OOP将数据和代码交织在一起:如果一个对象的能力必须直接来自其数据,反之亦然,那么对象能做什么就是其定义的核心,不能与对象的数据分开。

这种交织通常会导致一些值得怀疑的设计选择,例如:

— 对象中包含应该只有数据的代码。
— 对象中包含应该只有代码的数据。
— 对象为了代码而将数据聚集在一起。
— 对象为了数据而将代码聚集在一起。
— 代码为了数据而被拆分到不同的对象中。
— 数据为了代码而被拆分到不同的对象中。

用分散的复杂性替代集中复杂性反而增加了整体复杂性

根据面向对象设计的规则,具有过多“职责”的对象应该被拆分成更小的对象。然而,当你将大对象拆分成小块时,可能最终只是将复杂性分散开,而没有减少整体复杂性。实际上,拥有许多小块的代码库常常让人很难理解某个数据或代码片段的目的,也难以找到与某个特定功能相关的代码部分。

因此,虽然面向对象设计的目标是只要正确地委托责任给设计良好的对象集,就能使代码变得清晰,但面向对象设计过程本身往往是繁重的,充满了推测,通常导致的程序结构过于碎片化。

对象使得追踪哪些代码/访问哪些数据变得困难

理解一个程序归根结底是理解它的数据以及这些数据如何被转换。数据越容易理解,程序也越容易理解。无论是添加功能还是修复bug,程序员需要能够确定哪些代码影响了某个数据,以及从另一个角度,哪些数据被某段代码影响。

在面向对象的程序中,对象之间的连接越多,做出这些判断就越困难。虽然对象封装可以使对数据的直接访问保持私有,但任何间接连接的对象都可以通过公共方法调用的某条路径间接访问数据。例如,在调试为什么一个值被错误地设置时,确定所有相关的代码路径可能需要很多侦探工作。相比之下,在严格的过程式程序中,识别所有可能影响某个数据的代码路径通常需要考虑的可能性要少得多(只要程序不滥用全局变量)。

数据导向设计(DOD)

“数据导向设计(DOD)”这一术语在2000年代被创造出来以描述当时一些游戏程序员和其他高性能软件开发者提出的一套思想。没有单一来源拥有数据导向设计的权威定义,但以下资源可能最接近:

“数据导向设计与C++”“构建数据导向的未来”:Mike Acton的两次演讲
数据导向设计:Richard Fabian的书籍
数据导向设计资源:关于DOD的链接集合

在这里,我们并不提供理论上的解释,而是将DOD总结为几个实践性的建议。

在设计代码之前设计数据

DOD的核心前提是数据至少和代码一样重要。在宏观和微观层面,程序的最终目的是转换和生成数据。

因此,你的数据结构应该决定代码的结构,而不是反过来。这适用于所有阶段而不仅仅只是开始阶段,因此在添加或更改功能时,你应该在重构代码之前,首先重新评估数据的结构。

需要注意的是,这与面向对象设计(OOP)相冲突,后者将数据和代码不可分割地联系在一起。将数据设计与代码关注点混合会使设计过程更加复杂,并且常常导致次优的设计选择。相反,允许数据在不立即考虑代码的情况下自由变化,可以简化设计过程,并通常会产生更简单、更优化的数据结构。

优先选择简单的数据

一般来说,简单的数据会导致简单且高效的代码。特别是,你应该偏向使用数组而非层级结构和图结构:数组是存储许多数据元素的最简单方式,按顺序遍历平面数组是访问内存的最高效方式。

你还应该小心不要在数据元素之间创建不必要的连接(通过指针和数组索引)。错误地维护这些连接会使代码复杂化,而遍历这些连接则只需要进行次优的随机查找。

将代码视为数据管道

一旦你有了大致的数据显示设计,接下来的问题是数据必须经历哪些转换:

  • 在服务器中,客户端请求和数据库数据会被转换为服务器响应。
  • 在编译器中,源代码被转换为机器代码或某种中间代码。
  • 在音频编码器中,一种形式的音频数据被转换为另一种形式。
  • 在视频游戏中,一次游戏状态更新的用户输入和游戏状态被转换为新的游戏状态,然后再转换为新的渲染帧。

当然,这些宏观级的转换会分解成多个子步骤,但目标始终不变:对于某个初始状态的数据,你只需要连接这些点,以达到预期的最终状态。然后,代码可以自然地构建为“数据管道”,其中每一步都将转换或生成数据,并传递给管道的后续步骤。

这种编程描述可能听起来太简单或显而易见,但与其他软件开发理论相比,它提供了极大的清晰度。一旦你有了明确的起点和终点,搞清楚如何从A点到达B点就是一个非常具体且可以解决的问题,而且每个独立的转换步骤可以与其他部分独立编写和重构。

这种模型使得工作解决方案不仅更容易创建,而且更容易优化:

首先,识别顺序步骤中的瓶颈就像对所有步骤进行性能分析一样简单。少数几个步骤通常会占据大部分开销,这给你一个清晰的思路,知道优化的重点在哪里。因此,在优先考虑优化工作时,重要的是要权衡成本与收益,并在性能优化过程中保持务实。

其次,管道模型使得发现优化机会更容易。你很可能会发现以下情况:

  • 某些数据应该被转换为一种中间形式,使得后续步骤能够更高效地处理。
  • 由多个步骤冗余产生的数据应该在早期步骤中一次性缓存。
  • 访问相同数据的独立步骤应该完全或部分合并成更少的步骤,以减少重复访问的开销。
  • 一些逐个处理的数据元素应该改为批量处理,这通常会带来更高效的内存访问、更少的分支、更低的函数调用开销等优化。
  • 最后,数据管道有利于并行化:只要你清楚区分哪些步骤处理哪些数据,就很容易识别出哪些步骤可以安全地并行处理。

在开发的各个阶段衡量、估算并预算性能

在游戏开发中,一个常见的错误是等到项目的最后才去解决性能问题。后期优化工作既昂贵又具有风险,因为:

  • 许多优化在项目后期变得更难做。
  • 后期优化需要的时间和精力不可预测。
  • 后期优化可能无法达到令人满意的结果。

与其等待,不如从项目开始就关注性能。即使你在原型设计和Beta阶段愿意容忍次优的性能,也应该至少应该持续重新评估项目的需求,并设定性能预算。每个功能和整个游戏的内存、CPU、GPU、存储空间和网络带宽的预算是多少?这些目标在你的目标平台之间是否有差异?这些问题你应该在开发的各个阶段重新评估。

优先选择具体解决方案而非抽象

在编程中,“抽象”是指隐藏内部细节并通过简化的外观提供通用解决方案。抽象有多种形式,包括函数、对象、库、框架、编程语言,甚至游戏引擎。

虽然适度的抽象是合理的,但过度热衷于抽象可能会导致一些问题:

  • 使用现成的抽象(如库、框架或游戏引擎)的一大原因是,它们可以让你免于一些困难且耗时的实现工作。然而,这种便利的代价通常是提供的解决方案与你的具体需求之间出现尴尬的错配。最终,将一个现成的抽象强行适应你的需求,可能比直接编写你自己的具体解决方案花费更多的时间和精力。
  • 抽象往往会带来许多隐藏的性能成本,比如巨大的内存占用或CPU开销,这些成本最终是为了你可能根本不使用的功能。
  • 在你自己的实现中,可能会有将某个抽象的解决方案泛化到当前需求之外的诱惑,认为它以后可能会有用。然而,这种预想的工作通常会比解决当前问题带来更多的工作,且最终的解决方案往往是次优的(或者至少难以优化)。
  • 事实上,抽象可能会使得未来的变化变得更加困难,因为你可能会发现当前的需求再也不能与原先的抽象完美契合。

与其担心你的需求未来可能如何变化,通常更好的做法是根据你当前理解的需求来解决问题。接受迭代的思维:你不会在解决问题之前完全理解它,你可以等到需求真正发生变化后再修改代码。正如他们对写作所说的那样,也同样适用于代码:好的写作是重写。而使代码更易于重写的最佳方法是什么?简单性

如果你感到有抽象的诱惑,最好的建议是等待:首先解决至少几个具体的案例,然后再考虑将它们的解决方案组合成一个抽象。正如Richard Fabian所写:

“数据导向设计是当前的,它并不是问题历史的表现,也不是为了应对未来的一些通用解决方案。坚持过去会干扰灵活性,而展望未来通常是徒劳的,因为程序员并不是预言家。作者认为,未来可预见的系统通常并非那么可靠。”

总结为一个警告:过早抽象是万恶之源

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【hive】记一次hiveserver内存溢出排查,线程池未正确关闭导致

一、使用 MemoryAnalyzer软件打开hprof文件 很大有30G,win内存24GB,不用担心可以打开,ma软件能够生成索引文件,逐块分析内存,如下图。 大约需要4小时。 overview中开不到具体信息。 二、使用Leak Suspects功能继续…

(篇三)基于PyDracula搭建一个深度学习的软件之解析yolo算法融合

文章目录 1YoloPredictor类——检测器1.1继承BasePredictor解析1.2继承QObject解析 2MainWindow类——主窗口 在前面两篇中,篇一介绍了启动界面的制作,篇二介绍了如何修改PyDracula的界面,那么这一篇我们学习一下yolo要融合进入软件中&#x…

26~31.ppt

目录 26.北京主要的景点 题目 解析 27.创新产品展示及说明会 题目​ 解析 28.《小企业会计准则》 题目​ 解析 29.学习型社会的学习理念 题目​ 解析 30.小王-产品展示信息 题目​ 解析 31.小王-办公理念-信息工作者的每一天 题目​ 解析 26.北京主要的景点…

Vue.js 状态管理库Pinia

Pinia Pinia :Vue.js 状态管理库Pinia持久化插件-persist Pinia :Vue.js 状态管理库 Pinia 是 Vue 的专属状态管理库,它允许你跨组件或页面共享状态。 要使用Pinia ,先要安装npm install pinia在main.js中导入Pinia 并使用 示例…

day10-字符串

目录 字符串1、API 和 API 帮助文档2、String概述3、String构造方法代码实现 和 内存分析3.1 创建String对象的两种方式3.2 Java的内存模型 4、字符串的比较4.1 号的作用4.2 equals方法的作用 练习5、用户登录6、遍历字符串和统计字符个数7、字符串拼接和翻转8、较难练习-金额转…

从二叉树遍历深入理解BFS和DFS

1. 介绍 1.1 基础 BFS(Breadth-First Search,广度优先搜索)和 DFS(Depth-First Search,深度优先搜索)是两种常见的图和树的遍历算法。 BFS:从根节点(或起始节点)开始&am…

【大数据安全分析】大数据安全分析技术框架与关键技术

在数字化时代,网络安全面临着前所未有的挑战。传统的网络安全防护模式呈现出烟囱式的特点,各个安全防护措施和数据相互孤立,形成了防护孤岛和数据孤岛,难以有效应对日益复杂多变的安全威胁。而大数据分析技术的出现,为…

亚博microros小车-原生ubuntu支持系列 27、手掌控制小车运动

背景知识 本节跟上一个测试类似:亚博microros小车-原生ubuntu支持系列:26手势控制小车基础运动-CSDN博客 都是基于MediaPipe hands做手掌、手指识别的。 为了方便理解,在贴一下手指关键点分布。手掌位置就是靠第9点来识别的。 2、程序说明…

MySQL第五次作业

根据图片内容完成作业 1.建表 (1)建立两个表:goods(商品表)、orders(订单表) mysql> create table goods( -> gid char(8) primary key, -> name varchar(10), -> price decimal(8,2), -> num int); mysql> create t…

Linux:软硬链接和动静态库

hello,各位小伙伴,本篇文章跟大家一起学习《Linux:软硬链接和动静态库》,感谢大家对我上一篇的支持,如有什么问题,还请多多指教 ! 如果本篇文章对你有帮助,还请各位点点赞&#xff0…

CSS 组合选择符详解与实战示例

在 Web 开发过程中,CSS 用于定义页面元素的样式,而选择器则帮助我们精确定位需要添加样式的元素。今天我们主要来讲解 CSS 中的组合选择符,它们能够根据 DOM 结构中元素之间的关系来选中目标元素,从而写出结构清晰、易于维护的 CS…

【Linux系统】—— 简易进度条的实现

【Linux系统】—— 简易进度条的实现 1 回车和换行2 缓冲区3 进度条的准备代码4 第一版进度条5 第二版进度条 1 回车和换行 先问大家一个问题:回车换行是什么,或者说回车和换行是同一个概念吗?   可能大家对回车换行有一定的误解&#xff0…

Winform开发框架(蝇量级) MiniFramework V2.1

C/S框架网与2022年发布的一款蝇量级开发框架,适用于开发Windows桌面软件、数据管理应用系统、软件工具等轻量级软件,如:PLC上位机软件、数据采集与分析软件、或企业管理软件,进销存等。适合个人开发者快速搭建软件项目。 适用开发…

win10 llamafactory模型微调相关②

微调 使用微调神器LLaMA-Factory轻松改变大语言模型的自我认知_llamafactory 自我认知-CSDN博客 【大模型微调】使用Llama Factory实现中文llama3微调_哔哩哔哩_bilibili 样本数据集 (数据集管理脚本处需更改,见报错解决参考1) 自我认知微…

AI大模型随机初始化权重并打印网络结构方法(以Deepseekv3为例,单机可跑)

背景 当前大模型的权重加载和调用,主要是通过在HuggingFace官网下载并使用transformer的库来加以实现;其中大模型的权重文件较大(部分>100GB),若只是快速研究网络结构和数据流变化,则无需下载权重。本文…

前端项目打包完成后dist本地起node服务测试运行项目

1、新建文件夹 node-test 将打包dist 文件同步自定义本地服务文件夹node-test 中,安装依赖包。 npm install express serve-static cors 2、新创建服务文件js server.js 构建链接及端口 const express require(express); const path require(path); const co…

《语义捕捉全解析:从“我爱自然语言处理”到嵌入向量的全过程》

首先讲在前面,介绍一些背景 RAG(Retrieval-Augmented Generation,检索增强生成) 是一种结合了信息检索与语言生成模型的技术,通过从外部知识库中检索相关信息,并将其作为提示输入给大型语言模型&#xff…

Word中Ctrl+V粘贴报错问题

Word中CtrlV粘贴时显示“文件未找到:MathPage.WLL”的问题 Word的功能栏中有MathType,但无法使用,显示灰色。 解决方法如下: 首先找到MathType安装目录下MathPage.wll文件以及MathType Commands 2016.dotm文件,分别复…

Git 与 Git常用命令

Git 是一个开源的分布式版本控制系统,广泛用于源代码管理。与传统的集中式版本控制系统不同,Git 允许每个开发者在本地拥有完整的代码库副本,支持离线工作和高效的分支管理。每次提交时,Git 会对当前项目的所有文件创建一个快照&a…

构建jdk17包含maven的基础镜像

1、先拉取jdk17基础镜像 docker pull openjdk:17-jdk-alpine 2、使用jdk17基础镜像创建容器 docker run -it openjdk:17-jdk-alpine sh 或 docker run -it --name jdk17 openjdk:17-jdk-alpine sh 3、修改镜像源地址 cat /etc/apk/repositories https://mirrors.aliyun.com…