Spark 3.3.x版本中Runtime Filter在非分区字段上的设计实现分析

文章目录

  • 什么是Runtime Filter
    • JOIN示例
  • Runtime Filter的作用
  • Runtime Filter的分类
    • 分区字段的Filter表达式
      • DynamicPruningSubquery
    • 非分区字段的Filter表达式
      • BloomFilterSubquery
      • InSubquery
  • 在非分区字段上的Runtime Filter的生成过程
    • 插入Runtime Filter
      • 优化开始
      • 自底向上更新Plan,尝试插入Runtime Filter
      • 检查应用Runtime Filter侧的计划满足条件
      • 从Creation Side Plan抽取Filter + Scan信息
      • 构建带有BloomFilter/InSubquery的逻辑计划树
        • 构建新的、带有BloomFilter的逻辑计划树
        • 构建新的、带有IN表达式的逻辑计划树
        • 优化ScalarSubquery/InSubquery
  • 拓展知识
    • Subquery的执行

什么是Runtime Filter

我们常见的Partition FilterRow Filter,是在由SQL优化器生成的静态的表达式,例如a = 1,它们的左值右值都是确定的,因此可以很容易地被下推到数据源的读取算子上,对分区信息/数据进行预处理或是实时过滤。
但在有关联子查询或是JOIN的场景下,关联表达式的右值则通常是不确定的,需要在运行时才能确定其值,例如a IN (SELECT aa FROM b)中的子查询或a.id = b.id中的b.id。由于数据集无法在planning阶段确定,因此就不能像普通的条件表达式那样进行下推。
但如果能够提前计算这些不确定的表达式,使得它们变成常量,那不就可以下推了吗?
答案是可行的,Spark中的Subquery概念和实现机制就可以支持这种想法,因此通过将不确定的右值视情况不同转成不同的Subquery,从而在触发整个表达式的计算时,右值已经变成了常量,也就成了所谓的runtime filter

JOIN示例

例如有初始SQL,其中a.id、b.id均是32位整型类型:

SELECT a.*,b.* FROM a JOIN b ON a.id = b.id

转换成带有Runtime Filter的SQL(其中表a上的读取逻辑增加了带有子查询的IN过滤表达式,而子查询SELECT id FROM b则对应了runtime filter构建时新生成的子查询):

SELECT a.*, b.* FROM (SELECT * FROM a WHERE a.id IN (SELECT id FROM b)
JOIN b ON a.id = b.id

Runtime Filter的作用

Runtime Filter的作用下如其名,是在runtime阶段才能对数据过滤的一类过滤条件,是对planning阶段就能被应用的过滤条件场景外的补充,期望在某一侧的数据被读取后能够被再次过滤,减少下游算子的输入数据量。

Runtime Filter的分类

分区字段的Filter表达式

主要由PartitionPruning优化规则覆盖,具体的处理逻辑在之前的文章有介绍。

DynamicPruningSubquery

过滤表达式的右值在运行时才能确定,例如WHERE a IN (SELECT a FROM b WHERE b.a > 0),会将SELECT a FROM b WHERE b.a > 0转换为,DynamicPruningSubquery()被下推到Scan算子,

非分区字段的Filter表达式

InjectRuntimeFilter优化规则覆盖,是本文介绍的主要内容,该规则主要是覆盖JOIN的场景。

BloomFilterSubquery

优化后的表达式,使用的是Spark自己实现的Bloom Filter,对应内置类org.apache.spark.util.sketch.BloomFilter
这里简单归纳,例如有a LEFT JOIN b WHERE a.id = b.id AND a.name = b.name,对根据a.id = b.id条件为表b的id字段构建BloomFilter数据结构,最终得到类似这样的新表达式bloom_filter_might_contain(a.id);根据a.name = b.name条件为b表的name字段构建BloomFilter数据结构,最终得到类似这样的新表达式bloom_filter_might_contain(a.name)

InSubquery

如果不能使用BloomFilter时,则可以尝试基于JOIN的某一侧生成JOIN KEY上的InSubquery,如此便可以在后续的优化过程将InSubquery表达式转换为SEMI JOIN(类似Dynamic Pruning Partitions的逻辑,参考之前处理逻辑文章有介绍。

在非分区字段上的Runtime Filter的生成过程

插入Runtime Filter

InjectRuntimeFilter优化规则覆盖,仅在限定条件下才尝试插入,具体的实现逻辑见后节分析。

优化开始

object InjectRuntimeFilter extends Rule[LogicalPlan] with PredicateHelper with JoinSelectionHelper {override def apply(plan: LogicalPlan): LogicalPlan = plan match {// 如果输入的plan是一个Subquery,且是关联子查询(可能还没有被解耦),就不处理,直接返回原plancase s: Subquery if s.correlated => plancase _ if !conf.runtimeFilterSemiJoinReductionEnabled &&!conf.runtimeFilterBloomFilterEnabled => plancase _ =>// 只有开启了Semi Join或bloom filter优化功能时,才尝试插入runtime filterval newPlan = tryInjectRuntimeFilter(plan)if (conf.runtimeFilterSemiJoinReductionEnabled && !plan.fastEquals(newPlan)) {RewritePredicateSubquery(newPlan)} else {newPlan}}
}

自底向上更新Plan,尝试插入Runtime Filter

  private def tryInjectRuntimeFilter(plan: LogicalPlan): LogicalPlan = {var filterCounter = 0val numFilterThreshold = conf.getConf(SQLConf.RUNTIME_FILTER_NUMBER_THRESHOLD)plan transformUp {// 只有输入的plan存在JOIN子结构时,才会尝试进行优化case join @ ExtractEquiJoinKeys(joinType, leftKeys, rightKeys, _, _, left, right, hint) =>var newLeft = leftvar newRight = right(leftKeys, rightKeys).zipped.foreach((l, r) => {// Check if:// 1. There is already a DPP filter on the key// 2. There is already a runtime filter (Bloom filter or IN subquery) on the key// 3. The keys are simple cheap expressionsif (filterCounter < numFilterThreshold && // 生成的runtime filter数量不能超过阈值!hasDynamicPruningSubquery(left, right, l, r) &&!hasRuntimeFilter(newLeft, newRight, l, r) &&isSimpleExpression(l) && isSimpleExpression(r)) {// 过滤条件的数量小于可配置的阈值numFilterThreashold,而且左、右子树中不存在当前的// JOIN KEY所对应的DPP过滤条件而且也没有runtime filter,而且JOIN KEYs都是简单的表达式val oldLeft = newLeftval oldRight = newRightif (canPruneLeft(joinType) && filteringHasBenefit(left, right, l, hint)) {// JOIN类型是left join/semi left join/inner join时,说明可以对左侧的计划树进行优化。// 同时还需要有,JOIN KEY中的`l` 能够被下推到left plan的叶子结点,right plan存在过滤算子且能够过滤数据,才能向左侧的计划树中插入runtime filter。newLeft = injectFilter(l, newLeft, r, right)}// Did we actually inject on the left? If not, try on the rightif (newLeft.fastEquals(oldLeft) && canPruneRight(joinType) &&filteringHasBenefit(right, left, r, hint)) {// 同理左侧计划树的rewrite逻辑,这里 尝试在右边的计划树插入runtime filternewRight = injectFilter(r, newRight, l, left)}if (!newLeft.fastEquals(oldLeft) || !newRight.fastEquals(oldRight)) {// 左、右计算树中只要有一侧更新了,就累加生成的runtime filters的数量filterCounter = filterCounter + 1}}})// 返回一个新的JOIN计划树join.withNewChildren(Seq(newLeft, newRight))}}

检查应用Runtime Filter侧的计划满足条件

Application Side和Filter Side必须满足如下的条件中,才会从Filter Side抽取必要的Filter + Scan信息:

  1. filterApplicationSideExp引用的attribute确实来自于filterApplicationSide的叶子结点;
  2. 当前的JOIN计划必须是Shuffle Join,或者为Broadcast Join时,application side的子计划中存在shuffle算子,目的是保证filter side被广播到application side后,filter有足够大的效果(filter可以借后续的Filter下推优化规则,下推到靠近Scan算子的位置);
  3. application side的预估读取数据量大于可配置的阈值,目的是application side的数据集足够大时才有必要生成runtime filter。
object InjectRuntimeFilter extends Rule[LogicalPlan] with PredicateHelper with JoinSelectionHelper {/*** Extracts the beneficial filter creation plan with check show below:* - The filterApplicationSideJoinExp can be pushed down through joins, aggregates and windows*   (ie the expression references originate from a single leaf node)* - The filter creation side has a selective predicate* - The current join is a shuffle join or a broadcast join that has a shuffle below it* - The max filterApplicationSide scan size is greater than a configurable threshold*/private def extractBeneficialFilterCreatePlan(filterApplicationSide: LogicalPlan,filterCreationSide: LogicalPlan,filterApplicationSideExp: Expression,filterCreationSideExp: Expression,hint: JoinHint): Option[LogicalPlan] = {if (findExpressionAndTrackLineageDown(filterApplicationSideExp, filterApplicationSide).isDefined &&(isProbablyShuffleJoin(filterApplicationSide, filterCreationSide, hint) ||probablyHasShuffle(filterApplicationSide)) &&satisfyByteSizeRequirement(filterApplicationSide)) {// findExpressionAndTrackLineageDown方法验证filterApplicationSideExp引用的attribute确实来自于filterApplicationSide的叶子结点,否则不生成filter scan。//// (isProbablyShuffleJoin(filterApplicationSide, filterCreationSide, hint)//  probablyHasShuffle(filterApplicationSide))则验证当前层级的JOIN需要是Shuffle Join,// 或者为Broadcast Join时application side的子计划中存在shuffle算子,目的是保证filter side被广播到application side后,// filter有足够大的效果(filter可以借后续的Filter下推优化规则,下推到靠近Scan算子的位置)。//// satisfyByteSizeRequirement(filterApplicationSide)则确保application side的预估读取数据量足够大,才有优化的必要性。extractSelectiveFilterOverScan(filterCreationSide, filterCreationSideExp)} else {None}}/*** 对exp表达式进行解引用,并验证解引用后的attribute是否来自于输入plan的叶子结点的输出集。* 如果是,则返回输入的表达式和叶子结点;否则返回None。*/def findExpressionAndTrackLineageDown(exp: Expression,plan: LogicalPlan): Option[(Expression, LogicalPlan)] = {// 常量了呗,不管了if (exp.references.isEmpty) return Noneplan match {case p: Project =>// 从当前结点的表达式中,抽取所有的别名val aliases = getAliasMap(p)// 将exp中的同名别名,替换成被引用的Attribute;然后继续下推到底层的结点进行处理。findExpressionAndTrackLineageDown(replaceAlias(exp, aliases), p.child)// we can unwrap only if there are row projections, and no aggregation operationcase a: Aggregate =>val aliasMap = getAliasMap(a)findExpressionAndTrackLineageDown(replaceAlias(exp, aliasMap), a.child)case l: LeafNode if exp.references.subsetOf(l.outputSet) =>Some((exp, l))case u: Union =>// 对于Union结点,由于exp只可能有一个attribute,因此找到它在Union的输出列表的位置即可。val index = u.output.indexWhere(_.semanticEquals(exp))if (index > -1) {u.children.flatMap(child => findExpressionAndTrackLineageDown(child.output(index), child)).headOption} else {None}case other =>// 对于其它任意结点,将exp交给其孩子结点继续处理other.children.flatMap {child => if (exp.references.subsetOf(child.outputSet)) {findExpressionAndTrackLineageDown(exp, child)} else {None}}.headOption}}
}

从Creation Side Plan抽取Filter + Scan信息

如果不存在可以过滤数据的、简单过滤表达式时,说明无法找到一个有效的Spark Plan,其结果集大小可估算的,用来构建Runtime Filter。

object InjectRuntimeFilter extends Rule[LogicalPlan] with PredicateHelper with JoinSelectionHelper {/*** 从filterCreationSideExp抽取必要的Filter和Scan信息,以帮助构建完成的Runtime Filter。*/private def extractSelectiveFilterOverScan(plan: LogicalPlan,filterCreationSideExp: Expression): Option[LogicalPlan] = {@tailrecdef extract(p: LogicalPlan,predicateReference: AttributeSet, // 记录了过滤条件(谓词)引用的AttributehasHitFilter: Boolean, // 标记是否在之前的步骤中遇到过Filter结点hasHitSelectiveFilter: Boolean, // 标记上一次遇到的Filter结点的过滤表达式能够用于过滤数据currentPlan: LogicalPlan): Option[LogicalPlan] = p match {case Project(projectList, child) if hasHitFilter =>// We need to make sure all expressions referenced by filter predicates are simple// expressions.// 只有当p是类似Filter...Project..的结构时,才会执行这里的逻辑,由于Filter引用的Attribute来自Project的定义,// 因此需要使用projectList中引用了相同的Attribute的表达式,例如Alias(a, c + d),继续下探。val referencedExprs = projectList.filter(predicateReference.contains)if (referencedExprs.forall(isSimpleExpression)) {extract(child,referencedExprs.map(_.references).foldLeft(AttributeSet.empty)(_ ++ _),hasHitFilter,hasHitSelectiveFilter,currentPlan)} else {None}case Project(_, child) =>// 遇到Project结点时,而且前面的过程没有遇到Filter时,则必须满足如下的条件assert(predicateReference.isEmpty && !hasHitSelectiveFilter)// 继续遍历孩子结点extract(child, predicateReference, hasHitFilter, hasHitSelectiveFilter, currentPlan)case Filter(condition, child) if isSimpleExpression(condition) =>// 如果遇到了Filter结点,而且过滤条件是简单表达式时,才会执行这里的逻辑。// 思考:对于a AND b这样的条件,应该也可以被拆成a、b分别处理吧?extract(child,predicateReference ++ condition.references,hasHitFilter = true, // 遇到了Filter,预期在后续遇到Project结点时,能够反射得到底层结点的输出Attribute的表达式hasHitSelectiveFilter = hasHitSelectiveFilter || isLikelySelective(condition), // 过滤条件中是否存确实能够过滤的数据的表达式。currentPlan)case ExtractEquiJoinKeys(_, _, _, _, _, left, right, _) =>// Runtime filters use one side of the [[Join]] to build a set of join key values and prune// the other side of the [[Join]]. It's also OK to use a superset of the join key values// (ignore null values) to do the pruning.// 由于Runtime Filter是利用Join某一侧的plan的结果集去进行剪裁,因此如果遇到了子树中的Join结构时(它的输出结果集肯定是上层JOIN结点的超集),// 显然可以复用某一侧的plan,因此这里会判断filterCreationSideExp在哪个子plan中,从而开始新一轮的抽取。if (left.output.exists(_.semanticEquals(filterCreationSideExp))) {extract(left, AttributeSet.empty,hasHitFilter = false, hasHitSelectiveFilter = false, currentPlan = left)} else if (right.output.exists(_.semanticEquals(filterCreationSideExp))) {extract(right, AttributeSet.empty,hasHitFilter = false, hasHitSelectiveFilter = false, currentPlan = right)} else {None}case _: LeafNode if hasHitSelectiveFilter =>// 遍历完所有的结点,同时在之前的过滤中确实找到了可以过滤数据的表达式时,才会返回正确的plan。Some(currentPlan)case _ => None}if (!plan.isStreaming) {extract(plan, AttributeSet.empty,hasHitFilter = false, hasHitSelectiveFilter = false, currentPlan = plan)} else {None}}
}

构建带有BloomFilter/InSubquery的逻辑计划树

Spark目前支持两种实现,一个是基于BloomFilter;一个是基于SEMI JOIN,类似dynamic pruning filter的实现,其中BloomFilter 通常是最高效的。

object InjectRuntimeFilter extends Rule[LogicalPlan] with PredicateHelper with JoinSelectionHelper {private def injectFilter(filterApplicationSideExp: Expression,filterApplicationSidePlan: LogicalPlan,filterCreationSideExp: Expression,filterCreationSidePlan: LogicalPlan): LogicalPlan = {require(conf.runtimeFilterBloomFilterEnabled || conf.runtimeFilterSemiJoinReductionEnabled)if (conf.runtimeFilterBloomFilterEnabled) {injectBloomFilter(filterApplicationSideExp,filterApplicationSidePlan,filterCreationSideExp,filterCreationSidePlan)} else {injectInSubqueryFilter(filterApplicationSideExp,filterApplicationSidePlan,filterCreationSideExp,filterCreationSidePlan)}}
}
构建新的、带有BloomFilter的逻辑计划树

新的计划树结构看起来的样子:

    Filter(|    might_contain(|        ScalarSubquery(bloomFilterAgg(filterCreationSideExp) AS bloomFilter),  xxhash64(filterApplicationSidePlan))|filterApplicationSidePlan
  /*** 构建BloomFilter,并构建新的子计划。*/private def injectBloomFilter(filterApplicationSideExp: Expression,filterApplicationSidePlan: LogicalPlan,filterCreationSideExp: Expression,filterCreationSidePlan: LogicalPlan): LogicalPlan = {// Skip if the filter creation side is too bigif (!conf.runtimeFilterAllowBigFilterCreationSide) {// 虽然开启了bloom filter的优化功能,但同时不希望当filtering side的结果集太大时,// 生成bloom filter,则直接返回原计划。if (filterCreationSidePlan.stats.sizeInBytes > conf.runtimeFilterCreationSideThreshold) {return filterApplicationSidePlan}}// 获取预估过滤计划树的结果集的行数val rowCount = filterCreationSidePlan.stats.rowCount// 创建BloomFilterAggregate实例,它是一个TypedImperativeAggregate类型的实现类,是一个Aggregation算子,// 可以根据输入的HASH值,生成BloomFilter实例。val bloomFilterAgg =if (rowCount.isDefined && rowCount.get.longValue > 0L) {// 如果行数可预估,且不为0时,则会创建BloomFilterAggregate实例,其中//   new XxHash64(Seq(filterCreationSideExp)),用于计算JOIN表达式的hash值//   Literal(rowCount.get.longValue),用于预估结果集中不同HASH值的个数// 综上即构建了一个基于filterCreationSideExp的hash为key的bloom filter,并且hash functions的数量基于行数确定。new BloomFilterAggregate(new XxHash64(Seq(filterCreationSideExp)),Literal(rowCount.get.longValue))} else {// 如果无法预估行数,则基于默认的bloom filter相关的参数构建实例//   spark.sql.optimizer.runtime.bloomFilter.expectedNumItems=400,0000//   spark.sql.optimizer.runtime.bloomFilter.numBits=838,8608new BloomFilterAggregate(new XxHash64(Seq(filterCreationSideExp)))}// 这里构建一个新的AggregateExpression表达式,它的类型为Complete,意味着聚合计算的结果将直接从input生成,不需要经过partial aggregation。val aggExp = AggregateExpression(bloomFilterAgg, Complete, isDistinct = false, None)val alias = Alias(aggExp, "bloomFilter")()// 这里生成的Aggregate实例,其入参中的Nil表示没有group by表达式,因此最终会生成一行;Seq(alias)指示要聚合计算的表达式;filterCreationSidePlan表示结果集。// ColumnPruning用于裁剪掉filterCreationSidePlan计划结中不需要输出的columns。// ConstantFolding,优化可以通过静态方法估算是常量的表达式,为啥要在这里单独应用这个表达式?//     因为由于boolm filter agg plan的结果集只会有一行,因此这里会将此plan封装成一个ScalarSubquery实例,//.    而在后续的优化迭代过程中,ConstantFolding规则是无法对ScalarSubquery的子树生效的,因此这里显示地应用此优化规则,以提前优化常量表达式。val aggregate =ConstantFolding(ColumnPruning(Aggregate(Nil, Seq(alias), filterCreationSidePlan)))// Boolm filter估算器实例上就是一个复杂的数据类型,只需要一个值就可以了,因此这里会构建ScalarSubquery实例,且其outer references为Nil,即不引用外部属性。val bloomFilterSubquery = ScalarSubquery(aggregate, Nil)// 将ScalarSubquery封装到BloomFilterMightContain表达式中,BloomFilterMightContain的第一个参数是subquery实例,因此在执行当前的计划树时bloomFilterSubquery就已经确定了;第二参数是一个Expression,用于计算application side每一行的filterApplicationSideExp属性。// 构建一个Filter实例,至此就得到了一个可以基于Bloom Filter过滤数据的表达式,类似DynamicPruningExpression。val filter = BloomFilterMightContain(bloomFilterSubquery,new XxHash64(Seq(filterApplicationSideExp)))Filter(filter, filterApplicationSidePlan)}
构建新的、带有IN表达式的逻辑计划树

与Dynamic Pruning Partitions的优化逻辑不同的是,这里会为JOIN 表达式的左、右操作数,进行归一化处理,即计产生原数值或是hash值,尽量减少要广播的数据集大小。
这种构建的筛选条件方式不难想象,与Bloom Filter相似,存在假正例的情况,但效果是没有BloomFilter那样好罢了。
新的计划树看起来的样子:

Filter(
|    mayWrapWithHash(filterApplicationSideExp) IN (mayWrapWithHash(filterCreationSideExp))
|
filterApplicationSidePlan
  private def injectInSubqueryFilter(filterApplicationSideExp: Expression,filterApplicationSidePlan: LogicalPlan,filterCreationSideExp: Expression,filterCreationSidePlan: LogicalPlan): LogicalPlan = {require(filterApplicationSideExp.dataType == filterCreationSideExp.dataType)// mayWrapWithHash方法会根据表达式的返回类型进行归一个,要么取原值,要么对原值进行hashval actualFilterKeyExpr = mayWrapWithHash(filterCreationSideExp)val alias = Alias(actualFilterKeyExpr, actualFilterKeyExpr.toString)()// 构建一个Aggregation算子,计算creation side plan的结果集中的join字段进行聚合并对结果去重,// 后续就可以通过类SET数据结构快速过滤数据了val aggregate = Aggregate(Seq(alias), Seq(alias), filterCreationSidePlan)if (!canBroadcastBySize(aggregate, conf)) {// Skip the InSubquery filter if the size of `aggregate` is beyond broadcast join threshold,// i.e., the semi-join will be a shuffled join, which is not worthwhile.// 如果新生成的聚合计算计划树的结果集估算出来有些大,不能够被广播,那么也就没必要去做一次额外的SHUFFLE JOIN了,代价有点大return filterApplicationSidePlan}// 此时Agg可以被广播,因此就可以像Dynamic Prunning Partition的优化逻辑那样,构建一个带有子查询的过滤条件,替换原来的applicationi side plan。val filter = InSubquery(Seq(mayWrapWithHash(filterApplicationSideExp)),ListQuery(aggregate, childOutputs = aggregate.output))Filter(filter, filterApplicationSidePlan)}// Wraps `expr` with a hash function if its byte size is larger than an integer.private def mayWrapWithHash(expr: Expression): Expression = {if (expr.dataType.defaultSize > IntegerType.defaultSize) {// 数据类型的值的最大长度,超过32位数值时,就使用HASH值替代new Murmur3Hash(Seq(expr))} else {expr}}
优化ScalarSubquery/InSubquery

例如MergeScalarSubqueries优化ScalarSubquery;RewritePredicateSubquery重写InSubquery为SEMI/ANTI JOIN(同DPP讲解中的流程)等。

拓展知识

Subquery的执行

由于Runtime Filter需要在估算表达式时,确保依赖的右值已经常量化,因此为了简化数据的依赖实现,Spark利用了内置的Subquery机制。
Subquery即子查询,是相对于Root Plan来说的,为root plan的执行做准备,因此Spark会在Driver侧触发当前的plan生成RDD时,以Blocking的方式完成子查询计算。

abstract class SparkPlan extends QueryPlan[SparkPlan] with Logging with Serializable {/*** Returns the result of this query as an RDD[InternalRow] by delegating to `doExecute` after* preparations.** Concrete implementations of SparkPlan should override `doExecute`.*/// executeQuery是封装了执行当前plan的前置过程。final def execute(): RDD[InternalRow] = executeQuery {if (isCanonicalizedPlan) {throw SparkException.internalError("A canonicalized plan is not supposed to be executed.")}doExecute()}/*** Executes a query after preparing the query and adding query plan information to created RDDs* for visualization.*/protected final def executeQuery[T](query: => T): T = {RDDOperationScope.withScope(sparkContext, nodeName, false, true) {// 做一些准备工作,主要是收集所有的可执行的子查询,如execution.InSubqueryExec/execution.ScalarSubquery,同时触发它们的执行。prepare()// 阻塞并等待所有依赖的子查询执行完成waitForSubqueries()// 实际上是调用this.doExecutor()query}}/*** Blocks the thread until all subqueries finish evaluation and update the results.*/protected def waitForSubqueries(): Unit = synchronized {// fill in the result of subqueriesrunningSubqueries.foreach { sub =>// 以blocking的方式collect当前子查询的结果,并更新状态sub.updateResult()}runningSubqueries.clear()}
}

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

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

相关文章

C++_day2

目录 1. 引用 reference&#xff08;重点&#xff09; 1.1 基础使用 1.2 特性 1.3 引用参数 2. C窄化&#xff08;了解&#xff09; 3. 输入&#xff08;熟悉&#xff09; 4. string 字符串类&#xff08;掌握&#xff09; 4.1 基础使用 4.2 取出元素 4.3 字符串与数字转换 5. …

Vuex的基本使用

文章目录 一、Vuex概述1.是什么2.使用场景3.优势4.注意二、如何构建vuex多组件共享数据环境1.创建项目2.创建三个组件3.源代码三、vuex 的使用 - 创建仓库1.安装 vuex2.新建 `store/index.js` 专门存放 vuex3.创建仓库 `store/index.js`4 在 main.js 中导入挂载到 Vue 实例上5.…

WPF+MVVM案例实战(二十一)- 制作一个侧边弹窗栏(CD类)

文章目录 1、案例效果1、侧边栏分类2、CD类侧边弹窗实现1、样式代码实现2、功能代码实现3 运行效果4、源代码获取1、案例效果 1、侧边栏分类 A类 :左侧弹出侧边栏B类 :右侧弹出侧边栏C类 :顶部弹出侧边栏D类 :底部弹出侧边栏2、CD类侧边弹窗实现 1、样式代码实现 在原有的…

揭开广告引擎的神秘面纱:如何在0.1秒内精准匹配用户需求?

目录 一、广告系统与广告引擎介绍 &#xff08;一&#xff09;广告系统与广告粗分 &#xff08;二&#xff09;广告引擎在广告系统中的重要性分析 二、广告引擎整体架构和工作过程 &#xff08;一&#xff09;一般概述 &#xff08;二&#xff09;核心功能架构图 三、标…

[论文阅读]A Survey of Embodied Learning for Object-Centric Robotic Manipulation

Abstract --以对象为中心的机器人操纵的Embodied learning是体现人工智能中一个快速发展且具有挑战性的领域。它对于推进下一代智能机器人至关重要&#xff0c;最近引起了人们的极大兴趣。与数据驱动的机器学习方法不同&#xff0c;具身学习侧重于通过与环境的物理交互和感知反…

NFTScan Site:以蓝标认证与高级项目管理功能赋能 NFT 项目

自 NFTScan Site 上线以来&#xff0c;它迅速成为 NFT 市场中的一支重要力量&#xff0c;凭借对各类 NFT 集合、市场以及 NFTfi 项目的认证获得了广泛认可。这个平台帮助许多项目提升了曝光度和可见性&#xff0c;为它们在竞争激烈的 NFT 市场中创造了更大的成功机会。 在最新更…

指数分布的原理和应用

本文介绍指数分布&#xff0c;及其推导原理。 Ref: 指数分布 开始之前&#xff0c;先看个概率密度函数的小问题&#xff1a; 问题描述&#xff1a;你于上午10点到达车站&#xff0c;车在10点到10:30 之间到达的时刻 X 的概率密度函数如图&#xff1a; 则使用分段积分&#xff0…

Javase——正则表达式

正则表达式的相关使用 public static void main(String[] args) {//校验QQ号 System.out.println("3602222222".matches("[1-9][0-9]{4,}"));// 校验18位身份证号 System.out.println("11050220240830901X".matches("^([0-9]){7,18}…

安装中文版 Matlab R2022a

下载安装包 压缩包有点大&#xff0c;大概20G 百度网盘&#xff1a;下载链接 提取码&#xff1a;rmja 安装 解压后打开目录&#xff0c;右键以管理员身份运行 setup.exe 选择输入安装秘钥 输入秘钥&#xff1a; 50874-33247-14209-37962-45495-25133-28159-33348-18070-6088…

SICTF Round #4|MISC

1.派森 腐乳昂木 奥普瑞特儿 阴坡尔特 艾克斯奥尔 腐乳昂木 提克有第爱慕 阴坡尔特 ⭐ 弗拉格 等于 布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉 印刻 等于 左中括号右中括号 佛儿 唉 因 梯软者左括号 零&#xff0c;楞左括号弗拉格右…

保研考研机试攻略:python笔记(2)

&#x1f428;&#x1f428;&#x1f428;宝子们好呀&#xff0c;今天我们继续来学习N诺提供的python笔记&#xff0c;fighting&#xff01;( •̀ ω •́ )✧ 对这个系列感兴趣的宝子欢迎关注保研考研机试攻略专栏哦 ~ 目录 &#x1f428;&#x1f428;&#x1f428;4进制转…

Hyper-V 安装 KylinOS V10【图文教程】

文章目录 下载 KylinOSHyper-V 安装 KylinOS新建虚拟机配置虚拟机启动虚拟机并配置下载 KylinOS KylinOS 没有直接提供下载地址,需要在页面上点试用,填写个人信息后,才能看到下载地址。 https://www.kylinos.cn/support/trial.html?trial=425887 试用地址:产品试用申请国…

LeetCode 0685.冗余连接 II:并查集(和I有何不同分析)——详细题解(附图)

【LetMeFly】685.冗余连接 II&#xff1a;并查集&#xff08;和I有何不同分析&#xff09;——详细题解(附图) 力扣题目链接&#xff1a;https://leetcode.cn/problems/redundant-connection-ii/ 在本问题中&#xff0c;有根树指满足以下条件的 有向 图。该树只有一个根节点&…

mysql查表相关练习

作业要求&#xff1a; 单表练习&#xff1a; 1 . 查询出部门编号为 D2019060011 的所有员工 2 . 所有财务总监的姓名、编号和部门编号。 3 . 找出奖金高于工资的员工。 4 . 找出奖金高于工资 40% 的员工。 5 找出部门编号为 D2019090011 中所有财务总监&#xff0c;和…

GHuNeRF: Generalizable Human NeRF from a Monocular Video

研究背景 研究问题&#xff1a;这篇文章要解决的问题是学习一个从单目视频中泛化的人类NeRF模型。尽管现有的泛化人类NeRF已经取得了令人印象深刻的成果&#xff0c;但它们需要多视图图像或视频&#xff0c;这在某些情况下可能不可用。此外&#xff0c;一些基于单目视频的人类…

Linux - grep的正则用法

新建u.txt&#xff0c;文本内容如图&#xff1a; 搜寻特定字符串 利用中括号[]搜寻集合字符 行首与行位字符^$ 任意一个字符.与重复字符*限定连续RE字符范围{} 总结:

项目模块十五:HttpResponse模块

一、模块设计思路 存储HTTP应答要素&#xff0c;提供简单接口 二、成员变量 int _status; // 应答状态码 unordered_map<string, string> _headers; // 报头字段 string _body; // 应答正文 bool _redirect_flag; // 是否重定向信息 stri…

从零开始的c++之旅——继承

1. 继承 1.继承概念及定义 继承是面向对象编程的三大特点之一&#xff0c;它使得我们可以在原有类特性的基础之上&#xff0c;增加方法 和属性&#xff0c;这样产生的新的类&#xff0c;称为派生类。 继承 呈现了⾯向对象程序设计的层次结构&#xff0c;以前我们接触的…

6.1、实验一:静态路由

源文件获取&#xff1a;6.1_实验一&#xff1a;静态路由.pkt: https://url02.ctfile.com/f/61945102-1420248902-c5a99e?p2707 (访问密码: 2707) 一、目的 理解路由表的概念 会使用基础命令 根据需求正确配置静态路由 二、准备实验 1.实验要求 让PC0、PC1、PC2三台电脑…

logback日志级别动态切换四种方案

生产环境中经常有需要动态修改日志级别。 现在就介绍几种方案 方案一&#xff1a;开启logback的自动扫描更新 配置如下 <?xml version"1.0" encoding"UTF-8"?> <configuration scan"true" scanPeriod"60 seconds" debug…