当前位置: 首页 > news >正文

AI推理计算框架中的内存优化

背景

内存管理是AI计算中非常重要的一部分。我们希望模型计算时占用内存尽可能小,这样我们训练或推理时就可以用更大的batch size使其尽快收敛,或者提高吞吐率。又或者让我们可以使用参数更多、或更复杂的模型从而达到更好的准确率。由于现代深度学习模型大多在GPU上运行,而GPU的显存相比CPU小得多,因此我们这里主要关注的是GPU memory。

首先看下我们需要重点关注哪些GPU memory。对于模型在计算中需要用到的GPU memory,论文《Estimating GPU Memory Consumption of Deep Learning Models》做了比较具体的总结。对推理计算而言,主要有这么几类:

  • 权重参数(Weight Parameter):这个不用多说,模型中的参数。
  • 中间张量(Intermediate Tensor):网络中每层的输出与输入张量。
  • 其它:计算中需要的临时内存(如kernel中使用的一些memory,cuDNN的workspace),还有一些常驻的内存(如CUDA context)。

其中第三类本身占的空间不大,也比较难优化。第一类减少权重内存占用的话可以通过一些模型压缩方法,如量化,剪枝。之前写过一些相关文章如《闲话模型压缩之量化(Quantization)篇》和《闲话模型压缩之网络剪枝(Network Pruning)篇》,有兴趣可以参考。相比之下,第二类,即中间张量的优化空间更大,因此很多业界的工作也是针对它来优化。优化的思路有很多,比如:

  • 内存重用(Memory reuse):由于有些中间张量的生命周期间互不重叠,因此可以reuse。MegEngine, IREE, TensorRT等框架都做了memory usage相关的优化。
  • 重计算(Recomputation):该技术主要用于训练中。它将模型中的一些节点作为checkpoint,其它节点的输出可丢弃,当在计算梯度时需要时通过最近的checkpoint重新计算生成。因此被称为checkpointing技术。该问题也被称为tensor rematerialization优化问题。相关论文如适用于静态网络的offline方法《Training Deep Nets with Sublinear Memory Cost》,适用于动态网络的online方法《Dynamic Tensor Rematerialization》,建模为MILP进行求解的《Checkmate: Breaking the Memory Wall with Optimal Tensor Rematerialization》等。
  • 交换(Swap):基本思想是将显存中的数据交换到CPU上,相当于把GPU memory当成CPU memory的cache。一些塞不进GPU显存的层,如DLRM模型的embedding层可能会用到这种技术。相关的论文如《Supporting Massive DLRM Inference Through Software Defined Memory》,《vDNN: Virtualized Deep Neural Networks for Scalable, Memory-Efficient Neural Network Design》等。
  • 压缩(Compression):将数据进行压缩,如《Gist: Efficient Data Encoding for Deep Neural Network Training》,根据特定层输出特点对层的输出,即feature map数据进行编码压缩。

后面几种都会一定程度上牺牲性能,这里我们主要关注第一种。它在对延迟关注的推理场景用得尤其多一些。比如TensorFlow Lite利用该技术可以显著减少内存占用(详见https://blog.tensorflow.org/2020/10/optimizing-tensorflow-lite-runtime.html)。

对于网络前面层的输出,到计算后面的层时可能已经不再使用了。换句话说,对于中间层的输出张量,它们的生命周期可能是没有重叠的,对于它们便可以进行重用。下面是最简单情况下的示意图:
在这里插入图片描述
其中Task 1写数据到Tensor 1交给Task 2,Task 2处理后将结果写入Tensor 2,交给Task 3。因为Tensor 1与Tensor 2生命周期并不重叠,所以它们可以重用同一个Tensor。典型的如神经网络中的一些element wise操作。

但实际中的情况远没有这么简单。网络中不总是线性结构,另外张量的大小可能各不相同,给重用带来困难。要得到其最优的分配策略,是一个NP-complete问题。因此实际当中,我们可以倾向于一些heuristic方法,这样可以在合理时间内得到一个近似最优解。

那如何优化呢?在不少地方,如TensorRT会提到基于register allocation的思想。任何一本编译器的教材中都会介绍register allocation,在此不展开。大体会先通过liveness analysis得到变量的live range,然后根据它生成inference graph,转为着色问题来解。业界也有采用这种方法的做法,如《Memory Allocation for Neural Networks using Graph Coloring》。但是,memory planning与register allocation所面临的问题还是有所区别的,比如:

  • 大小不确定:Register的大小基本是相同,或者说是差不多的,而memory的大小可能差异很大,重用一块过大或过小的memory会产生问题。
  • 拷贝成本高:Register拷贝一下还好,但memory拷贝开销比较大,尤其是大块的memory。因此理想情况下我们希望不要拷贝。
  • Fallback机制:Register实在分配不了会产生spill,即放到memory中。虽然memory要不不够理论上也能往更下一层存储器上搬,业界也有这方面的研究,但很多情况下因为时延等原因不会这么做。

另外,从静态/动态角度,内存的管理方式大体有动态分配与静态规划两种:

  • 动态分配(Dynamic Memory Allocation):内存分配在运行时进行。由于从系统中分配的开销较大,通常维护一个memory pool。需要时从中分配,不再需要时放回到pool。如TensorFlow中的BFC allocator。
  • 静态规划(Static Memory Planning):内存分配在运行前进行,常见于基于编译器的计算框架。通过规划进一步减少内存使用,减少OOM带来的不确定性,同时最少化运行时内存管理的开销。如MXNet与MegEngine/MegCC中的static memory planning。

光看概念有些抽象,下面就以一个实例 - TensorFlow Lite(TF Lite)中的内存优化来看看具体的实现。其实在论文《Efficient Memory Management for Deep Neural Net Inference》与《On-Device Neural Net Inference with Mobile GPUs》中对其原理已经介绍得比较清楚了。下面主要是结合代码理解下它的实现。

代码走读

基础数据结构

先来看几个关键数据结构。它们定义在types.h文件中。结构体TensorUsageRecord即论文中提到的Tensor usage record,用于记录张量的使用记录。它主要包含三个信息:tensor size, 以及第一个与最后一个使用它的task。代表这两个task的成员first_tasklast_task即它的生命周期。

using UsageGraph = std::vector<std::vector<size_t>>;template <typename TensorSizeT>
struct TensorUsageRecord {TensorSizeT tensor_size;TaskId first_task;TaskId last_task;...
};

注意它是个模板类,有针对size_tuint2uint3BHWC的特化(实现在memory_management.c文件)。

结构体ObjectsAssignmentOffsetsAssignment都用于存放分配结果。

// Information about assignment of tensors to shared objects
template <typename TensorSizeT>
struct ObjectsAssignment {// shared_object_ids_[i] is ID of shared object, that tensor i will be using.std::vector<size_t> object_ids;// shared_object_sizes_[i] is a size of shared object with ID equal to i.std::vector<TensorSizeT> object_sizes;
};// Information about assignment of tensors to offsets for the case, when all of
// them are going to be allocated in one continuous memory block.
struct OffsetsAssignment {std::vector<size_t> offsets;size_t total_size;
};

它们对应后面会提到的两种分配方式。前者用于shared object(指可以用于多个tensor的内存块)分配,后者用于从大块连续内存中分配子内存区域。

为了解它的使用,可以参考它的测试用例memory_management_test.cc。比较典型的有几个case:OneRecordChainRecordsComplexRecords,分别对于只有一个节点,链式(即线性)计算图,和复杂计算图。
在这里插入图片描述

ChainRecords这个case为例:

TEST(Model, ChainRecords) {                                                          std::vector<TensorUsageRecord<size_t>> usage_records{                              {/*size=*/16, /*first=*/0, /*last=*/1},                                        {/*size=*/8, /*first=*/1, /*last=*/2},                                         {/*size=*/64, /*first=*/2, /*last=*/3},                                        {/*size=*/32, /*first=*/3, /*last=*/4},                                        {/*size=*/8, /*first=*/4, /*last=*/5},                                         };                                                                                 ObjectsAssignment<size_t> assignment;                                              ASSERT_TRUE(                                                                       AssignObjectsToTensors(usage_records, MemoryStrategy::NAIVE, &assignment)      .ok());                                                                    EXPECT_THAT(assignment.object_ids, ElementsAre(0, 1, 2, 3, 4));                    EXPECT_THAT(assignment.object_sizes, ElementsAre(16, 8, 64, 32, 8));    ...

可以看到,其中最核心的是AssignObjectsToTensors()函数。该函数基于由TensorUsageRecord数组表示的张量使用记录(按拓扑序排列),根据指定的分配策略(由MemoryStrategy表示),计算得到分配结果(由ObjectsAssignment对象表示)。

Object分配方式

注意AssignObjectsToTensors()是个模板函数,根据TensorUsageRecord的类型不同有多种实现。以最简单的TensorUsageRecord<size>的情况(即tensor的大小以一个size_t类型表示)为例,相关代码如下:

template <>
absl::Status AssignObjectsToTensors(const std::vector<TensorUsageRecord<size_t>>& usage_records,MemoryStrategy strategy, ObjectsAssignment<size_t>* assignment,const UsageGraph* reallocation_graph) {switch (strategy) {case MemoryStrategy::NAIVE:return NaiveAssignment(usage_records, assignment);case MemoryStrategy::EQUALITY:return EqualityAssignmentWithHash(usage_records, assignment);case MemoryStrategy::GREEDY_IN_ORDER:return GreedyInOrderAssignment(usage_records, assignment,reallocation_graph);case MemoryStrategy::GREEDY_BY_BREADTH:return GreedyByBreadthAssignment(usage_records, assignment);case MemoryStrategy::GREEDY_BY_SIZE:return GreedyBySizeDistPriorityAssignment(usage_records, assignment);case MemoryStrategy::GREEDY_BEST:return BestGreedy(usage_records, assignment);case MemoryStrategy::MINCOSTFLOW:return MinCostFlowAssignment(usage_records, assignment);default:return absl::InternalError("MemoryStrategy is not supported with current tensor size type.");}return absl::OkStatus();
}

可以看到,它的主体部分就是根据指定策略调用相应的分配算法。这几种策略分别是:

NATIVE

NaiveAssignment函数将每个张量分配独立的memory object。实现在native_assignment.h文件中。该算法为每个张量分配一个新的shared object。这是最简单,也是最浪费内存的做法。代码逻辑比较好理解,不多说了。

EQUALITY

EqualityAssignmentWithHash()函数实现于equality_assignment.h文件中。它适用于TensorSizeT为hashable type的情况(unhashable type的情况使用EqualityAssignment()函数)。

该算法维护两个关键数据结构:一个是pool,它是一个map。其键值为size,值为目前free(即空闲)的且size与键值相同的shared objects。另一个是优先队列objects_in_use,它保存目前在使用的share objects,并按size排序。整个过程遍历所有的tensor usage record,对于每个tensor usage record,先将队列objects_in_use中相对于当前tensor已不再使用的shared object弹出,放入pool。然后在pool中找有没有与当前tensor的size匹配的shared object,如有就重用,没有的话就新创建一个shared object。过程示意图如下:
在这里插入图片描述
注意这里只是做memory planning,所以不用真正做内存分配。

GREEDY_IN_ORDER

函数GreedyInOrderAssignment()实现在greedy_in_order_assignment.h文件中。它维护与前一算法中相似的两个数据结构:一个是存放free shared objects的pool,以object size排序。另一个是存放正在使用的shared object的优先队列objects_in_use,以last_task排序。该算法主体部分遍历tensor usage records。在第一步中,首先看哪些object不再使用,将它们放入pool。这一步与前面算法一样。

然后尝试将pool中的shared object分配给当前tensor。前面是需要严格匹配才能重用,这里放宽了一些,允许在size不严格一致时也能重用。这里在从pool中找可用的shared object时,不是用的find()函数,而是用的是二分查找lower_bound(),即查找不小于当前tensor的size的第一个元素。这样做保证找到的shared object(如有)能容纳当前tensor,同时又使浪费的空间尽可能小。然后尝试检查前一个元素,其shared object size与tensor size的差值是否比前面找到的元素更小。如果是就选它了。这样使得对shared object的size改变尽可能得小就能满足当前tensor的需求。比如pool中有size分别为1, 3, 6的shared object,而当前tensor为5。这种情况下就会找到size为3这个shared object,因为它与5的差值是最小的。可以看到,它每一步尽可能找与当前tensor的size尽可能接近的shared object进行重用,体现了贪心的思想。

当前面的步骤中找到合适的shared object,就把它从pool中拿走,将之分配给当前tensor,分配信息写入assignment。同时将该信息记录在objects_in_use中。如果shared object的size小于tensor,会增大shared object的size。如果没有找到合适的shared object,则创建新的shared object。

GREEDY_BY_BREADTH

函数GreedyByBreadthAssignment()实现在greedy_by_breadth_assignment.cc文件中。与前面的贪心算法类似,主要区别在于它优先考虑breadth大的task。Breadth表示该task执行时所有tensor的size之和,记录于TaskProfile对象中。TaskProfile是一个vector,元素表示task执行时还在使用的张量,以size降序排序。obj_schedulesSharedObjectSchedule的vector。SharedObjectSchedule记录了shared object对应的所有tensor usage record。

// Set of usage records for all tensors assigned to the shared object, ordered
// by first_task.
using SharedObjectSchedule = std::set<TensorUsageRecord<size_t>>;               struct TaskBreadthWithId {size_t breadth;TaskId task_id;TaskBreadthWithId(size_t breadth, size_t task_id)                             : breadth(breadth), task_id(task_id) {}                                   // Default order of TaskBreadthWithId is increasing order of their breadth.   bool operator<(const TaskBreadthWithId& other) const {return breadth < other.breadth;}         
};        

算法首先调用CalculateTaskProfiles()函数计算出所有task的TaskProfile,它包含了task的breadth信息。然后以breadth非递增顺序遍历所有task。对于每个task,遍历其所有的tensor,为它们分配shared object。分配过程中考虑obj_schedules中的shared object是否可重用。遍历其中元素,如果不合适就跳过,否则用lower_bound()函数找到其中不小于当前所需size的第一个shared object。如果找到的话检查一下,如果合法,就将该shared object作为最优候选。如果没找到就创建新的shared object。

GREEDY_BY_SIZE

函数GreedyBySizeDistPriorityAssignment()实现在greedy_by_size_assignment.cc文件中。它的流程大体与前一种相似,区别在于优先考虑哪些tensor时所用的策略。该方法使用了一种更加复杂的heuristic。核心数据结构是SizeDistPriorityInfo

struct SizeDistPriorityInfo {// - Tensor with leftmost position in positional maximums vector has higher// priority;// - If two tensors have equal position, the one, that has usage interval with// smallest positive distance (best_dist) to some of already assigned tensors,// has higher priority;// - If two tensors have equal position and best_dist, the one with greater// tensor_size has higher priority.bool operator>(const SizeDistPriorityInfo& other) const {return position < other.position ||(position == other.position &&(best_dist < other.best_dist || (best_dist == other.best_dist &&tensor_size > other.tensor_size)));}// Recalculate best distance and best object, based on precalculated distances// in vector dist.void RecalcBestDist() {best_dist = kNotAssigned;for (size_t obj_id = 0; obj_id < dist.size(); ++obj_id) {if (dist[obj_id] < best_dist) {best_dist = dist[obj_id];best_object = obj_id;}}}size_t position;size_t tensor_size;std::vector<size_t> dist;size_t best_dist;size_t best_object;size_t tensor_usage_id;
};

根据上面的注释,在positional maximum vector中位置越靠左的tensor优先级越高。如果在该vector中位置一样,则与已分配的tensor的positive distance小的优先级更高。如果前两个标准还决不出高下,那size较大的tensor优先级更高。这个优先级考虑了tensor的size,也考虑了与时序上的局部性。

主流程中首先调用CalculatePositionalMaximums()函数计算positional maximums vector。即每个shared object size的lower bound的数组。然后根据这个信息填好SizeDistPriorityInfo数组。接下来分两层循环。它会以SizeDistPriority递增顺序遍历所有tensor。即对于每个tensor,按前面计算得到的优先级信息找到优先级最高的tensor,以及相应的object。如果找到就按之进行分配,没找到就创建新的shared object。分配完成后,SizeDistPriority信息会产生变化,因此需要进行相应的更新。

GREEDY_BEST

函数BestGreedy()实现在memory_management.cc文件中。它结合了前两种贪心算法。结合方式比较直观,即把GREEDY_BY_SIZEGREEDY_BY_BREADTH都跑下,看哪个的总用量少(即更优)就选哪个。

  RETURN_IF_ERROR(GreedyBySizeDistPriorityAssignment(usage_records, assignment));ObjectsAssignment<size_t> assignment_by_breadth;if (GreedyByBreadthAssignment(usage_records, &assignment_by_breadth).ok() &&TotalSize(assignment_by_breadth) < TotalSize(*assignment)) {std::swap(*assignment, assignment_by_breadth);}

MINCOSTFLOW

函数MinCostFlowAssignment()实现在min_cost_flow_assignment.cc文件中。
它使用Minimum-cost flow matching algorithm。它将问题建模为一个minimum-cost flow problem(MCFP)。对于原问题,它创建一个auxiliary flow graph,对于每个intermediate tensor在图中插入两个对应节点,另外创建两个特殊节点-source与sink。然后按一定规则向图中添加有向边(具体可参见论文《On-Devie Neural Net Inference with Mobile GPUs》)。创建完后,使用Shortest Path Faster Algorithm(SPFA)求解。

TF Lite中虽然提供了多种机制,但至于哪种好不一定,所以实际使用当中可能要都试一下。

Offset分配方式

除了这种以shared object分配的模式外,TF Lite还支持一种在一整块内存块分配的模式。两种模式区别示意图:
在这里插入图片描述
代码中检测如果支持sub buffer方式的话,会试图以memory中的切块(以offset表示)进行分配。一般使用在先分配一大块连续memory,然后在里边“切”小块的情况。这种情况下,会调用AssignOffsetsToTensors()函数,继而调用GreedyBySizeAssignment()函数。这是论文中提到的Offet Calculation方法。它可以看作是二维的strip packing problem。在主逻辑函数GreedyBySizeAssignment()中,需要注意的是两个比较关键的数据结构:ordered_records是按size排序后的TensorUsageRecordordered_allocs是已经分配好memory的tensor,按offset排序。主要逻辑包含两层循环。外循环按序遍历TensorUsageRecord,然后内循环中遍历已分配的内存块。如果生命周期不重叠,则会考察它前面空出的区域是否能容纳下当前的tensor。如果能容纳且浪费的区域最小,则将当前tensor塞进去。找一圈都找不到的话就在最右边新分配一段。

框架对接

看了内存分配的实现,再看下它是如何结合进框架,用于模型的计算的。我们知道TF Lite会将计算放到相应的后端,如Hexagon,OpenGL, OpenCL, Metal等。这些后端实现放在delegates目录下。以GPU的OpenCL后端为例,相应的实现主要位于tensorflow/tensorflow/lite/delegates/gpu/cl/inference_context.cc文件中。函数InitFromGpuModel()调用AllocateMemory()函数,函数AllocateMemory继而调用AllocateBufferBasedTensors()函数或者AllocateStrongShapesTensors函数。前者用于buffer based的tensor(参见IsBufferBased()函数),比如clCreateBuffer()分配的内存。其它的则用AllocateStrongShapesTensors()函数进行处理。函数AllocateStrongShapesTensors()用于那些具有特定layout的tensor(参考delegates/gpu/common/shape.h),调用AssignObjectsToTensors()函数时传入的分配策略是MemoryStrategy::EQUALIT

下面看下AllocateBufferBasedTensors()函数。其中核心函数GetBufferAsignment()负责buffer的分配。它先调用GetUsages()函数得到网络中张量的使用信息,然后基于它构成TensorUsageRecord数组。有了这些信息,就可以调用前面提到的AssignObjectsToTensors()函数进行内存对象的分配了。默认用的分配策略是MemoryStrategy::GREEDY_BEST

另外如果支持sub buffer的话,还会调用AssignOffsetsToTensors()进行分配。最后判断与前面以buffer为单位(AssignObjectsToTensors()函数)的分配方式得到的结果哪种好,即使用的memory少,从而选择更优的一种。

最后总结一下整个调用流程作为收尾吧:
在这里插入图片描述

相关文章:

AI推理计算框架中的内存优化

背景 内存管理是AI计算中非常重要的一部分。我们希望模型计算时占用内存尽可能小&#xff0c;这样我们训练或推理时就可以用更大的batch size使其尽快收敛&#xff0c;或者提高吞吐率。又或者让我们可以使用参数更多、或更复杂的模型从而达到更好的准确率。由于现代深度学习模…...

C语言学习小结(1)——初认识C语言

一、C语言概念 C语言是一门通用计算机编程语言&#xff0c;广泛应用于底层开发。C语言的设计目标是提供一种能以简易 的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。尽管C语言提供了许多低级处理的功能&#xff0c;但仍然保持着…...

30分钟吃掉wandb可视化自动调参

wandb.sweep: 低代码&#xff0c;可视化&#xff0c;分布式 自动调参工具。使用wandb 的 sweep 进行超参调优&#xff0c;具有以下优点。(1)低代码&#xff1a;只需配置一个sweep.yaml配置文件&#xff0c;或者定义一个配置dict&#xff0c;几乎不用编写调参相关代码。(2)可视化…...

【8】AMBA_SOC项目自学IC验证项目-仿真平台脚本使用讲解

仿真平台文件介绍和脚本使用说明 1、项目路径:2、文件夹说明:3、仿真运行命令:第一步:进入项目路径第二步:设置环境第三步:运行仿真第四步:查看波形1、项目路径: 位置:/tool/project/axi 2、文件夹说明: a、env就是放的我们uvm环境相关的env文件; b、out就是我们…...

智慧水务未来技术发展方向预测探讨

随着科技的不断发展和城市化的加速&#xff0c;智慧水务作为一种新的水务模式&#xff0c;逐渐受到广泛关注。未来&#xff0c;智慧水务将会面临更多的技术挑战和商机。本博客将对智慧水务的未来技术发展方向进行预测&#xff0c;以探讨智慧水务未来可能的技术重点。 1. 人工…...

数据结构 | 栈与队列

&#x1f525;Go for it!&#x1f525; &#x1f4dd;个人主页&#xff1a;按键难防 &#x1f4eb; 如果文章知识点有错误的地方&#xff0c;请指正&#xff01;和大家一起学习&#xff0c;一起进步&#x1f440; &#x1f4d6;系列专栏&#xff1a;数据结构与算法 &#x1f52…...

Redux 源码分析

Redux 目录结构 redux ├─ .babelrc.js ├─ .editorconfig ├─ .gitignore …...

第五十二章 BFS进阶(二)——双向广搜

第五十二章 BFS进阶&#xff08;二&#xff09;——双向广搜一、双向广搜1、优越之处2、实现逻辑3、复杂度分析二、例题1、问题2、分析3、代码一、双向广搜 1、优越之处 双向广搜是指我们从终点和起点同时开始搜索&#xff0c;当二者到达同一个中间状态的时候&#xff0c;即相…...

业务建模题

一. 单选题&#xff1a;1.在活动图中负责在一个活动节点执行完毕后切换到另一个节点的元素是( A)。A.控制流 B.对象流 C.判断节点 D.扩展区城2.以下说法错误的是(C)。A.活动图中的开始标记一般只有一一个,而终止标记可能有多个B.判断节点的出口条件必须保证不互相重复,并且不缺…...

电子秤专用模拟数字(AD)转换器芯片HX711介绍

HX711简介HX711是一款专为高精度电子秤而设计的24 位A/D 转换器芯片。与同类型其它芯片相比&#xff0c;该芯片集成了包括稳压电源、片内时钟振荡器等其它同类型芯片所需要的外围电路&#xff0c;具有集成度高、响应速度快、抗干扰性强等优点。降低了电子秤的整机成本&#xff…...

微服务 RocketMQ-延时消息 消息过滤 管控台搜索问题

~~微服务 RocketMQ-延时消息 消息过滤 管控台搜索问题~~ RocketMQ-延时消息实现延时消息RocketMQ-消息过滤Tag标签过滤SQL标签过滤管控台搜索问题RocketMQ-延时消息 给消息设置延时时间&#xff0c;到一定时间&#xff0c;消费者才能消费的到&#xff0c;中间件内部通过每秒钟扫…...

js发送邮件(node.js)

以前看别人博客留言或者评论文章时必须填写邮箱信息&#xff0c;感觉甚是麻烦。 后来才知道是为了在博主回复后让访客收到邮件&#xff0c;用心良苦。 于是我也在新增留言和文章评论的接口里&#xff0c;新增了给自己发送邮件提醒的功能。 我用的QQ邮箱&#xff0c;具体如下…...

English Learning - Day58 一周高频问题汇总 2023.2.12 周日

English Learning - Day58 一周高频问题汇总 2023.2.12 周日这周主要内容继续说说状语从句结果状语从句这周主要内容 DAY58【周日总结】 一周高频问题汇总 &#xff08;打卡作业详见 Day59&#xff09; 一近期主要讲了 一 01.主动脉修饰 以下是最常问到的知识点拓展&#xff…...

【微电网】基于风光储能和需求响应的微电网日前经济调度(Python代码实现)

目录 1 概述 2 知识点及数学模型 3 算例实现 3.1算例介绍 3.2风光参与的模型求解 3.3 风光和储能参与的模型求解 3.5 风光储能和需求响应都参与模型求解 3.6 结果分析对比 4 Python代码及算例数据 1 概述 近年来&#xff0c;微电网、清洁能源等已成为全球关注的热点…...

四种方式的MySQL安装

mysql安装常见的方法有四种序号 安装方式 说明1 yum\rpm简单、快速&#xff0c;不能定制参数2二进制 解压&#xff0c;简单配置就可使用 免安装 mysql-a.b.c-linux2.x-x86_64.tar.gz3源码编译 可以定制参数&#xff0c;安装时间长 mysql-a.b.c.tar.gz4源码制成rpm包 把源码制…...

软考高级信息系统项目管理师系列之九:项目范围管理

软考高级信息系统项目管理师系列之九:项目范围管理 一、范围管理输入、输出、工具和技术表二、范围管理概述三、规划范围管理四、收集需求1.收集需求:2.需求分类3.收集需求的工具与技术4.收集需求过程主要输出5.需求文件内容6.需求管理7.可跟踪性8.双向可跟踪性9.需求跟踪矩阵…...

【项目精选】javaEE健康管理系统(论文+开题报告+答辩PPT+源代码+数据库+讲解视频)

点击下载源码 javaEE健康管理系统主要功能包括&#xff1a;教师登录退出、教师饮食管理、教师健康日志、体检管理等等。本系统结构如下&#xff1a; &#xff08;1&#xff09;用户模块&#xff1a; 实现登录功能 实现用户登录的退出 实现用户注册 &#xff08;2&#xff09;教…...

ctfshow nodejs

web 334 大小写转换特殊字符绕过。 “ı”.toUpperCase() ‘I’&#xff0c;“ſ”.toUpperCase() ‘S’。 “K”.toLowerCase() ‘k’. payload: CTFſHOW 123456web 335 通过源码可知 eval(xxx)&#xff0c;eval 中可以执行 js 代码&#xff0c;那么我们可以依此执行系…...

无线传感器原理及方法|重点理论知识|2021年19级|期末考试

Min-Max定位 【P63】 最小最大法的基本思想是依据未知节点到各锚节点的距离测量值及锚节点的坐标构造若干个边界框,即以参考节点为圆心,未知节点到该锚节点的距离测量值为半径所构成圆的外接矩形,计算外接矩形的质心为未知节点的估计坐标。 多边定位法的浮点运算量大,计算代…...

带你写出符合 Promise/A+ 规范 Promise 的源码

Promise是前端面试中的高频问题&#xff0c;如果你能根据PromiseA的规范&#xff0c;写出符合规范的源码&#xff0c;那么我想&#xff0c;对于面试中的Promise相关的问题&#xff0c;都能够给出比较完美的答案。 我的建议是&#xff0c;对照规范多写几次实现&#xff0c;也许…...

回流与重绘

触发回流与重绘条件&#x1f449;回流当渲染树中部分或者全部元素的尺寸、结构或者属性发生变化时&#xff0c;浏览器会重新渲染部分或者全部文档的过程就称为 回流。引起回流原因1.页面的首次渲染2.浏览器的窗口大小发生变化3.元素的内容发生变化4.元素的尺寸或者位置发生变化…...

openpyxl表格的简单实用

示例:创建简单的电子表格和条形图 在这个例子中,我们将从头开始创建一个工作表并添加一些数据,然后绘制它。我们还将探索一些有限的单元格样式和格式。 我们将在工作表上输入的数据如下: 首先,让我们加载 openpyxl 并创建一个新工作簿。并获取活动表。我们还将输入我们…...

【寒假day4】leetcode刷题

&#x1f308;一、选择题❤1.下列哪一个是析构函数的特征&#xff08; &#xff09;。A: 析构函数定义只能在类体内 B: 一个类中只能定义一个析构函数 C: 析构函数名与类名相同 D: 析构函数可以有一个或多个参数答案&#xff1a;B答案解析&#xff1a;析构函数是构造函…...

【竞赛题】6355. 统计公平数对的数目

题目&#xff1a; 给你一个下标从 0 开始、长度为 n 的整数数组 nums &#xff0c;和两个整数 lower 和 upper &#xff0c;返回 公平数对的数目 。 如果 (i, j) 数对满足以下情况&#xff0c;则认为它是一个 公平数对 &#xff1a; 0 < i < j < n&#xff0c;且 l…...

Redis集群搭建(主从、哨兵、分片)

1.单机安装Redis 首先需要安装Redis所需要的依赖&#xff1a; yum install -y gcc tcl然后将课前资料提供的Redis安装包上传到虚拟机的任意目录&#xff1a; 例如&#xff0c;我放到了/tmp目录&#xff1a; 解压缩&#xff1a; tar -xzf redis-6.2.4.tar.gz解压后&#xff1…...

Dart语法基础补充

Asynchrony support Dart 库中充满了返回 Future 或 Stream 对象的函数。 这些函数是异步的&#xff1a;它们在设置一个可能耗时的操作&#xff08;例如 I/O&#xff09;后返回&#xff0c;而不等待该操作完成。 async 和 await 关键字支持异步编程&#xff0c;让编写看起来类…...

Nginx - 深入理解nginx的处理请求、进程关系和配置文件重载

概述 Nginx的系统学习整理的第三篇博客&#xff0c;主要介绍nginx的应用场景和架构基础&#xff0c;以便更好的理解&#xff0c;再生产环境中进行性能调优。 Nginx的三个主要应用场景 1.静态资源服务&#xff0c;通过本地文件系统提供服务 2.反向代理服务&#xff0c;强大的性…...

华为OD机试 - 服务依赖(Python)| 真题含思路

服务依赖 题目 在某系统中有众多服务,每个服务用字符串(只包含字母和数字,长度<=10)唯一标识,服务间可能有依赖关系,如A依赖B,则当 B 故障时导致 A 也故障。 传递具有依赖性,如 A依赖 B,B 依赖 C,当 C 故障时导致 B 故障,也导致 A 故障。给出所有依赖关系以及当…...

html的表单标签(form)

目录标题1、表单标签主要有三大类&#xff1a;2、表单标签中常见的属性3、例子代码及结果4、注意&#xff1a;5、表单中特殊的属性表单标签可以用来数据交互&#xff0c;而前面学的六个标签只能发送不能接收。 表单标签的作用就是数据交互1、表单标签主要有三大类&#xff1a; …...

手把手教你部署ruoyi前后端分离版本

下载源码&#xff08;当前版本3.8.5&#xff09;RuoYi-Vue: &#x1f389; 基于SpringBoot&#xff0c;Spring Security&#xff0c;JWT&#xff0c;Vue & Element 的前后端分离权限管理系统&#xff0c;同时提供了 Vue3 的版本 (gitee.com)创建数据库(一定要是这三个&…...

网站原型的交互怎么做/产品运营方案

*题意&#xff1a; 在1到n之间添加‘’&#xff0c;‘-’&#xff0c;‘.’三种运算符令最后结果为0&#xff0c;输出前20种添加方法&#xff0c;若不足20种则全 输出&#xff0c;最后输出共有多少种添加方式。&#xff08;‘.’表示将其两侧的数连成一个数例&#xff1a…...

西安网站托管排名/aso具体优化

一、准备&#xff1a; 1.1、GOPATH目录下的bin文件夹添加系统path变量中。 添加后可直接在任意位置控制台中直接调用bin目录下的可执行程序。 1.2、准备好自己的程序ico图标文件&#xff0c;放在main.go同级目录。 下文中提到的&#xff1a;控制台运行命令&#xff0c;都是在…...

app设计平台/结构优化

如果有同学看完上一篇关于MySQL文章&#xff0c;文末留有两个很开放的问题&#xff0c;如有兴趣可以在脑袋里想想。本文也会试着回答这两个问题&#xff0c;希望能给你一些参考。现在可以思考一个问题&#xff0c;如果数据量非常大的情况下&#xff0c;您根据业务选择了合适的字…...

wordpress计算器插件/宁波seo外包服务

在测试程序里面&#xff0c;我们使用的是一个测试函数&#xff0c;函数体内部可以通过改变YY的值来改变函数的耗时。测试对比是 循环调用XX次函数&#xff0c;和循环XX次函数内部的YY循环。结果发现&#xff0c;在YY足够小&#xff0c;X足够大的情况下&#xff0c;函数调用耗时…...

专业的模板建站企业/seo推广公司排名

Bootstrap资源: 兼容性不错的简洁BuiBootstrap后台管理系统模板 [url]http://www.js-css.cn/divcss/admin/bui-bootstrap[/url], 下载 [url]http://www.js-css.cn/a/css/template/admin/2013/1108/1023.html[/url] 值得收藏&#xff01;4个Bootstrap3开发的后台管理模板 [url]h…...

可以做网站的app/流量精灵app

CentOS 7官方下载地址&#xff1a;官方网站上&#xff0c;CentOS 7提供了三种ISO镜像文件的下载&#xff1a;DVD ISO、Everything ISO、Minimal ISO。以下针对各个版本的ISO镜像文件&#xff0c;进行一一说明&#xff1a;CentOS-7.0-x86_64-DVD-1503-01.iso 标准安…...