Vitis HLS 学习笔记--通道的FIFO/PIPO选择

目录

1. 简介

2. 代码详解

2.1 FIFO 通道示例

2.1.1 配置默认通道

2.1.2 kernel 代码 

2.1.3 综合报告

2.1.4 depth = 32 解析

2.1.5 FIFO 通道分类

2.2 PIPO

2.2.1 配置默认通道

2.2.2  kernel 代码 

2.2.3 综合报告

2.2.4 PIPO 通道分类

3. 综合对比

3.1 数据类型和接口

3.2 数据流和数据流控制

3.3 计算函数和计算方式

4. 总结


1. 简介

本文通过调整 Vitis HLS 编译器的默认设置,选择FIFO或者PIPO作为通道缓存。首先,我们来复习一下这两种缓存的区别:

想象一下,你在一家快餐店,FIFO 缓冲器就像是排队等候的顾客队伍。第一个进入队伍的顾客会是第一个得到服务的人。这个系统的好处是,顾客(生产者)一旦排队,厨师(使用者)就可以开始准备他们的食物。但如果厨师做饭的速度赶不上顾客排队的速度,或者反过来,就会造成混乱,有可能导致整个队伍停滞不前,这就是所谓的“死锁”。

现在,让我们来看看乒乓缓冲器。这就像是有两个窗口的快餐店。当一个窗口正在为顾客准备食物时,另一个窗口就在接待新的顾客。一旦一个窗口的食物准备好了,顾客就可以从那里取餐,而厨师则转到另一个窗口继续工作。这样,即使顾客来得很快,也不会有人等得太久,因为总有一个窗口是在准备食物的。

PIPO 缓冲器的工作方式类似于一个自动调节的快餐店,无论顾客来得多快,厨师都能以相同的速度做饭,确保每个人都能及时得到食物,不会有任何拥堵或延误。

在这两种情况下,无论是 FIFO 还是 PIPO,关键点都是顾客(生产者)将订单(数据块)传递给厨师(使用者)。订单可以是一个汉堡包(单个值),也可以是一整个家庭套餐(一组 N 个值)。订单越大,厨师需要的准备空间就越多。

本文内容会参照《Vitis HLS 学习笔记--控制驱动TLP-处理deadlock_vitis hls数据流处理-CSDN博客》中对于 FIFO /PIPO 类型的分类。

2. 代码详解

2.1 FIFO 通道示例

2.1.1 配置默认通道

首先配置 FIFO 缓存,可以通过如下指令进行设定:

# Create a solution
open_solution -reset solution1 -flow_target vitis
config_dataflow -default_channel fifo -fifo_depth 2

或者通过 UI 界面设置:

2.1.2 kernel 代码 

#include <hls_stream.h>
#include <hls_vector.h>

extern "C" {

void diamond(hls::vector<uint32_t, 16>* vecIn, hls::vector<uint32_t, 16>* vecOut, int size) {
// The depth setting is required for pointer to array in the interface.
#pragma HLS INTERFACE m_axi port = vecIn depth = 32
#pragma HLS INTERFACE m_axi port = vecOut depth = 32

    hls::stream<hls::vector<uint32_t, 16>> c0, c1, c2, c3, c4, c5;
    assert(size % 16 == 0);

#pragma HLS dataflow
    load(vecIn, c0, size);
    compute_A(c0, c1, c2, size);
    compute_B(c1, c3, size);
    compute_C(c2, c4, size);
    compute_D(c3, c4, c5, size);
    store(c5, vecOut, size);
}
}

void load(hls::vector<uint32_t, 16>* in, hls::stream<hls::vector<uint32_t, 16>>& out, int size) {
Loop_Ld:
    for (int i = 0; i < size; i++) {
#pragma HLS performance target_ti = 32
#pragma HLS LOOP_TRIPCOUNT max = 32
        out.write(in[i]);
    }
}

void compute_A(hls::stream<hls::vector<uint32_t, 16>>& in, hls::stream<hls::vector<uint32_t, 16>>& out1,
               hls::stream<hls::vector<uint32_t, 16>>& out2, int size) {
Loop_A:
    for (int i = 0; i < size; i++) {
#pragma HLS performance target_ti = 32
#pragma HLS LOOP_TRIPCOUNT max = 32
        hls::vector<uint32_t, 16> t = in.read();
        out1.write(t * 3);
        out2.write(t * 3);
    }
}

void compute_B(hls::stream<hls::vector<uint32_t, 16>>& in, hls::stream<hls::vector<uint32_t, 16>>& out,
               int size) {
Loop_B:
    for (int i = 0; i < size; i++) {
#pragma HLS performance target_ti = 32
#pragma HLS LOOP_TRIPCOUNT max = 32
        out.write(in.read() + 25);
    }
}

void compute_C(hls::stream<hls::vector<uint32_t, 16>>& in, hls::stream<hls::vector<uint32_t, 16>>& out,
               int size) {
Loop_C:
    for (unsigned int i = 0; i < size; i++) {
#pragma HLS performance target_ti = 32
#pragma HLS LOOP_TRIPCOUNT max = 32
        out.write(in.read() * 2);
    }
}
void compute_D(hls::stream<hls::vector<uint32_t, 16>>& in1, hls::stream<hls::vector<uint32_t, 16>>& in2,
               hls::stream<hls::vector<uint32_t, 16>>& out, int size) {
Loop_D:
    for (unsigned int i = 0; i < size; i++) {
#pragma HLS performance target_ti = 32
#pragma HLS LOOP_TRIPCOUNT max = 32
        out.write(in1.read() + in2.read());
    }
}

void store(hls::stream<hls::vector<uint32_t, 16>>& in, hls::vector<uint32_t, 16>* out, int size) {
Loop_St:
    for (int i = 0; i < size; i++) {
#pragma HLS performance target_ti = 32
#pragma HLS LOOP_TRIPCOUNT max = 32
        out[i] = in.read();
    }
}

数据流说明:

  • load:读取数组 vecIn -> c0;
  • compute_A:从流 c0 读取数据,计算后写入 c0 × 3 -> c1, c0 ×3 -> c2;
  • compute_B:从流 c1 读取数据,计算后写入 c1 + 25 -> c3;
  • compute_C:从流 c2 读取数据,计算后写入 c2 × 2 -> c4;
  • compute_D:从流 c3 和 c4 读取数据,计算后写入 c3 + c4 -> c5;
  • store:数组 c5 -> vecOut。 

2.1.3 综合报告

查看综合后的报告:

================================================================
== HW Interfaces
================================================================
* M_AXI
+------------+------------+---------------+---------+--------+----------+-----------+--------------+--------------+-------------+-------------+
| Interface  | Data Width | Address Width | Latency | Offset | Register | Max Widen | Max Read     | Max Write    | Num Read    | Num Write   |
|            | (SW->HW)   |               |         |        |          | Bitwidth  | Burst Length | Burst Length | Outstanding | Outstanding |
+------------+------------+---------------+---------+--------+----------+-----------+--------------+--------------+-------------+-------------+
| m_axi_gmem | 512 -> 512 | 64            | 64      | slave  | 0        | 512       | 16           | 16           | 16          | 16          |
+------------+------------+---------------+---------+--------+----------+-----------+--------------+--------------+-------------+-------------+

具有较大的位宽,512bit,符合 hls::vector<uint32_t, 16> 定义,512=32*16。

* SW-to-HW Mapping
+----------+---------------+-----------+----------+------------------------------------+
| Argument | HW Interface  | HW Type   | HW Usage | HW Info                            |
+----------+---------------+-----------+----------+------------------------------------+
| vecIn    | m_axi_gmem    | interface |          |                                    |
| vecIn    | s_axi_control | register  | offset   | name=vecIn_1 offset=0x10 range=32  |
| vecIn    | s_axi_control | register  | offset   | name=vecIn_2 offset=0x14 range=32  |
| vecOut   | m_axi_gmem    | interface |          |                                    |
| vecOut   | s_axi_control | register  | offset   | name=vecOut_1 offset=0x1c range=32 |
| vecOut   | s_axi_control | register  | offset   | name=vecOut_2 offset=0x20 range=32 |
| size     | s_axi_control | register  |          | name=size offset=0x28 range=32     |
+----------+---------------+-----------+----------+------------------------------------+

vecIn 和 vecOut 均是绑定到 m_axi_gmem 的 AXI 主接口。 

2.1.4 depth = 32 解析

void diamond(vecOf16Words* vecIn, vecOf16Words* vecOut, int size) {
#pragma HLS INTERFACE m_axi port = vecIn depth = 32
#pragma HLS INTERFACE m_axi port = vecOut depth = 32

    hls::stream<vecOf16Words> c0, c1, c2, c3, c4, c5;
    assert(size % 16 == 0);

#pragma HLS dataflow
    load(vecIn, c0, size);
    compute_A(c0, c1, c2, size);
    compute_B(c1, c3, size);
    compute_C(c2, c4, size);
    compute_D(c3, c4, c5, size);
    store(c5, vecOut, size);
}

#pragma HLS INTERFACE m_axi port = vecIn depth = 32 指令用于设置接口属性。这里的 depth=32 是一个指令选项,它指定了AXI接口的深度,也就是FPGA与外部内存交互时可以访问的数据量。这个数值应该与设计打算在单次事务中处理的数据量相匹配。由于 TB 中的 std::vector 容器 test 和 outcome 的大小都被初始化为32,这表明设计打算在一个事务中处理32个向量,因此 depth 被设置为32。

这个深度值对于优化数据传输非常重要,因为它影响了接口生成的FIFO缓冲区的大小,以及FPGA与外部内存之间的突发传输能力。正确设置深度可以帮助提高性能,减少延迟,并确保数据的连续流动。

2.1.5 FIFO 通道分类

FIFO 有三种类型:

  • Streams (including hls::streams and streamed arrays),用户创建。
  • Scalar propagation FIFOs,工具推断。
  • Streams of blocks,用户创建。

对照 Dataflow 报告,本 kernel 的 FIFO 通道有两种类型 Stream 和 ScalarProp,从变量的名字也可以看出,c0、c1、c2、c3、c4、c5是由用户创建的,而其余的变量则由 HLS 工具推断生成。

2.2 PIPO

2.2.1 配置默认通道

首先配置 PIPO 缓存,可以通过如下指令进行设定:

# Create a solution

open_solution -reset solution1 -flow_target vitis

或者通过 UI 界面设置:

2.2.2  kernel 代码 

void diamond(unsigned char vecIn[100], unsigned char vecOut[100]) {
    unsigned char c1[100], c2[100], c3[100], c4[100];
#pragma HLS dataflow
    funcA(vecIn, c1, c2);
    funcB(c1, c3);
    funcC(c2, c4);
    funcD(c3, c4, vecOut);
}

void funcA(unsigned char* in, unsigned char* out1, unsigned char* out2) {
Loop0:
    for (int i = 0; i < 100; i++) {
#pragma HLS pipeline rewind
#pragma HLS unroll factor = 2
        unsigned char t = in[i] * 3;
        out1[i] = t;
        out2[i] = t;
    }
}

void funcB(unsigned char* in, unsigned char* out) {
Loop0:
    for (int i = 0; i < 100; i++) {
#pragma HLS pipeline rewind
#pragma HLS unroll factor = 2
        out[i] = in[i] + 25;
    }
}

void funcC(unsigned char* in, unsigned char* out) {
Loop0:
    for (unsigned char i = 0; i < 100; i++) {
#pragma HLS pipeline rewind
#pragma HLS unroll factor = 2
        out[i] = in[i] * 2;
    }
}

void funcD(unsigned char* in1, unsigned char* in2, unsigned char* out) {
Loop0:
    for (int i = 0; i < 100; i++) {
#pragma HLS pipeline rewind
#pragma HLS unroll factor = 2
        out[i] = in1[i] + in2[i] * 2;
    }
}

数据流说明:

  • compute_A:计算后写入 vecIn × 3 -> c1, vecIn ×3 -> c2;
  • compute_B:计算后写入 c1 + 25 -> c3;
  • compute_C:计算后写入 c2 × 2 -> c4;
  • compute_D:计算后写入 c3 + c4 × 2 -> vecOut.

2.2.3 综合报告

================================================================
== HW Interfaces
================================================================
* M_AXI
+------------+------------+---------------+---------+--------+----------+-----------+--------------+--------------+-------------+-------------+
| Interface  | Data Width | Address Width | Latency | Offset | Register | Max Widen | Max Read     | Max Write    | Num Read    | Num Write   |
|            | (SW->HW)   |               |         |        |          | Bitwidth  | Burst Length | Burst Length | Outstanding | Outstanding |
+------------+------------+---------------+---------+--------+----------+-----------+--------------+--------------+-------------+-------------+
| m_axi_gmem | 512 -> 512 | 64            | 64      | slave  | 0        | 512       | 16           | 16           | 16          | 16          |
+------------+------------+---------------+---------+--------+----------+-----------+--------------+--------------+-------------+-------------+
* SW-to-HW Mapping
+----------+---------------+-----------+----------+------------------------------------+
| Argument | HW Interface  | HW Type   | HW Usage | HW Info                            |
+----------+---------------+-----------+----------+------------------------------------+
| vecIn    | m_axi_gmem    | interface |          |                                    |
| vecIn    | s_axi_control | register  | offset   | name=vecIn_1 offset=0x10 range=32  |
| vecIn    | s_axi_control | register  | offset   | name=vecIn_2 offset=0x14 range=32  |
| vecOut   | m_axi_gmem    | interface |          |                                    |
| vecOut   | s_axi_control | register  | offset   | name=vecOut_1 offset=0x1c range=32 |
| vecOut   | s_axi_control | register  | offset   | name=vecOut_2 offset=0x20 range=32 |
| size     | s_axi_control | register  |          | name=size offset=0x28 range=32     |
+----------+---------------+-----------+----------+------------------------------------+

报告中 HW Interface 与 Mapping 具有相同的内容,这里不再赘述。

2.2.4 PIPO 通道分类

PIPO 有三种类型:

  • PIPO,用户创建。
  • Task Level FIFOs (TLF),工具推断。
  • Input and output ports to the upper level,用户创建。

对照 Dataflow 报告,本 kernel 的 PIPO 通道类型只有一种:PIPO,c1、c2、c3、c4均由用户创建。变量 vecOut_c 则由 HLS 工具推断生成为 FIFO 类型。

 

3. 综合对比

3.1 数据类型和接口

第一段代码:

  • 使用 hls::vector<uint32_t, 16> 类型来表示向量。
  • 使用 hls::stream 数据结构来处理数据流。
  • 显式指定 AXI 主接口。

第二段代码:

  • 使用基本的 unsigned char 类型的数组,每个数组包含 100 个元素。
  • 没有使用 hls::stream,而是使用普通的数组传递数据。
  • 输入输出接口使用普通的 C-style 指针和数组,没有显式的接口设置。

3.2 数据流和数据流控制

第一段代码:

  • 使用 HLS dataflow pragma (#pragma HLS dataflow) 来启用数据流优化
  • 显式指定使用 hls::stream 作为函数间的通信方式,函数之间的数据通过流进行传递。

第二段代码:

  • 同样使用 HLS dataflow pragma (#pragma HLS dataflow) 来启用数据流优化,但数据传递是通过普通的数组来进行。
  • 函数间的数据通过数组传递,没有使用 hls::stream。

3.3 计算函数和计算方式

第一段代码:

  • 函数更为通用,接受 hls::vector 类型的数据,并且处理过程中使用了 HLS 的各种特性如 pipeline 和 unroll。
  • 每个函数处理 hls::vector<uint32_t, 16> 类型的数据,并将结果写入另一个 hls::vector。

第二段代码:

  • 函数接受基本类型的指针和数组作为输入输出,处理过程中使用 pipeline 和 unroll 进行优化。
  • 每个函数处理 unsigned char 类型的数据,并将结果写入另一个 unsigned char 数组。

4. 总结

本文对比了在 Vitis HLS 编译器中配置 FIFO 和 PIPO 缓冲器的方法和效果。通过两段代码示例,我们展示了使用这两种通道缓存的不同实现方式。

在 FIFO 示例中,代码使用 hls::vector<uint32_t, 16> 和 hls::stream 数据结构,通过 AXI 主接口进行数据传输,并用 #pragma HLS dataflow 启用数据流优化,每个函数通过流进行通信。这种方式更通用,但需要显式指定流和接口设置。

在 PIPO 示例中,代码采用 unsigned char 数组进行数据传递,使用 C-style 指针和数组,没有显式接口设置,通过数组传递数据,同样使用 #pragma HLS dataflow 启用数据流优化。这种方式更简单直接,但缺乏流的灵活性。

PIPO 缓冲器可以自动调节,确保任务之间的重叠执行,而不会出现死锁。而显式手动串流的 FIFO 通道虽然可以更快地开始重叠执行,但需要小心调整队列大小,以避免死锁问题。

综合来看,FIFO 适用于复杂的通用数据处理场景,而 PIPO 更适合简单的数据传递需求。正确选择和配置这两种缓冲器,可以优化系统性能,减少延迟。本文帮助读者理解 FIFO 和 PIPO 的适用场景和配置方法,从而在设计中做出最佳选择。

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

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

相关文章

WSDM 2023 推荐系统相关论文整理(三)

WSDM 2023的论文录用结果已出&#xff0c;推荐系统相关的论文方向包含序列推荐&#xff0c;点击率估计等领域&#xff0c;涵盖图学习&#xff0c;对比学习&#xff0c;因果推断&#xff0c;知识蒸馏等技术&#xff0c;累计包含近四十篇论文&#xff0c;下文列举了部分论文的标题…

Element UI上传图片和PDF,支持预览,并支持复制黏贴上传

背景 如上图&#xff0c;使用Element UI的el-upload组件&#xff0c;并且预览的时候可以展示图片和PDF格式文件&#xff1b; 做法 index.vue <template><div><el-uploadv-model"diaForm.list":limit"5":on-exceed"handleExceed"…

2.6倍!WhaleTunnel 客户POC实景对弈DataX

作为阿里早期的开源产品&#xff0c;DataX是一款非常优秀的数据集成工具&#xff0c;普遍被用于多个数据源之间的批量同步&#xff0c;包括类似Apache DolphinScheduler的Task类型也对DataX进行了适配和增强&#xff0c;可以直接在DolphinScheduler里面利用通用的数据源调用Dat…

UE4 使用自带的插件制作音频可视化

1.插件默认为开启 2.新建共感NRT&#xff0c;选择要使用的音频 3.添加音频组件&#xff0c;添加共感NRT变量&#xff0c;选择新建的共感NRT对象 4.编写蓝图

【机器学习】YOLOv10与YOLOv8分析

YOLOv10与YOLOv8&#xff1a;实时目标检测技术的演进与对比 一、YOLOv8与YOLOv10的概述二、YOLOv8的特点与优势三、YOLOv10的改进与创新四、YOLOv10与YOLOv8的性能对比五、总结与展望 随着深度学习技术的飞速发展&#xff0c;实时目标检测技术已成为计算机视觉领域的研究热点。…

爬山算法:启发式搜索的简单而有效的方法

1. 概述 爬山算法是一种在人工智能和优化领域广泛使用的启发式搜索方法。它从一个初始解开始&#xff0c;逐步选择邻域内的最优解&#xff0c;直到找到目标点或无法进一步改进为止。该算法的核心在于通过逐渐逼近的方式寻找问题的最优解或近似最优解。 2. 背景 在许多实际问…

Spring Boot 实现动态数据源配置

前言 之前在CSDN博客以及某站看了教程&#xff0c;不免觉得有点不知如何下手&#xff0c;好在最后融合了以下&#xff0c;得出了一个比较简单的配置动态数据源的过程。 首先项目是Spring Boot的单体项目&#xff0c;我们的需求是要连接多个数据库&#xff0c;那么就需要配置多个…

Source Insight 4.0安装和使用

文章目录 一、前言二、新建工程2.1 新建工程2.2 同步工程 3 Source Insight怎么生成函数调用关系图&#xff1f;3.1 打开关系窗口3.2 打开关系函数3.3 修改关系属性3.4设置 Relation Window Options3.5 设置Levels3.6 修改显示模式 4 下载地址 一、前言 Source Insight 4.0 是每…

Flutter开发效率提升1000%,Flutter Quick教程之定义Api(三)

将tab键切换到Response&#xff0c;会出现这么一个界面 这是添加api返回的json数据。比如我们添加一个json数据。 添加完json数据后&#xff0c;右上角有一个删除按钮。要换json数据的话&#xff0c;可以点击清除再重新输入。 这时候&#xff0c;左边的面板上还会显示出 这个的…

【TB作品】msp430g2553单片机,家用可燃气体监测报警器,MQ-2,MQ5,PWM风扇

功能 //家用可燃气体监测报警器 //硬件&#xff1a;MQ-2 MQ5 OLED 蜂鸣器 按键 风扇 //1 OLED显示天然气、液化气浓度 //2 OLED显示可燃气体报警临界值 //3 2个按键用于修改可燃气体报警临界值 //4 MQ2检测到的浓度或者MQ7检测到的浓度 高于临界值时&#xff0c;蜂鸣器报警风扇…

AI Agent(人工智能代理)是一种能够感知环境、进行决策和执行动作的智能实体

在大模型平台中&#xff0c;“Agent”通常指的是基于大型预训练模型构建的一种智能实体或软件系统&#xff0c;它能够执行一系列复杂的任务。这些任务可能包括但不限于理解和生成自然语言、决策制定、交互式对话、执行具体操作&#xff08;如在GUI环境中操作&#xff09;、数据…

Java 垃圾回收

文章目录 1 Java 垃圾回收1.1 JVM1.2 Java 对象生命周期 2 如何判断一个对象可被回收2.1 引用计数算法2.2 可达性分析算法 3 垃圾回收过程3.1 总体过程3.2 为什么要进行世代垃圾回收&#xff1f;3.3 分代垃圾回收过程 在 C 和 C 中&#xff0c;许多对象要求程序员声明他们后为其…

微电子加速迈向后摩尔时代!复旦大学梅永丰课题组集成 DNN 与纳米薄膜技术,精准分析入射光角度

「二维纳米膜自组装成三维微结构」被认为是制造下一代微电子器件的重要途径&#xff0c;对于即将到来的先进电子和光电子应用至关重要。然而&#xff0c;二维纳米膜最终几何形状的形成受到蚀刻轨迹、化学反应、高宽比以及其他复杂因素影响&#xff0c;导致自组装器件在制造过程…

【调试笔记-20240604-Linux-为 OpenWrt LuCI 界面添加多语言支持】

调试笔记-系列文章目录 调试笔记-20240604-Linux-为 OpenWrt LuCI 界面添加多语言支持 文章目录 调试笔记-系列文章目录调试笔记-20240604-Linux-为 OpenWrt LuCI 界面添加多语言支持 前言一、调试环境操作系统&#xff1a;Ubuntu 22.04.4 LTS编译环境调试目标 二、调试步骤预…

【机器学习】GBDT (Gradient Boosting Decision Tree) 深入解析

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 GBDT (Gradient Boosting Decision Tree) 深入解析引言一、GBDT基础理论1.1 梯度…

SpringBoot整合Skywalking

下载Java Agent 官网&#xff1a;https://skywalking.apache.org/downloads/ 提示&#xff1a;Agent最好到网上找一找之前的版本&#xff0c;新版本可能有bug&#xff0c;如果出现了并且网上也几乎没有这个版本的解决方法那么就切换之前的版本 本地启动时 -javaagent:d:\opt\…

全志D1s软件入门之Tina Linux编译教程

编译 Tina Linux 在搭建好编译环境并下载好源码后&#xff0c;即可对源码进行编译&#xff0c;编译打包好后&#xff0c;即可将打包好的固件烧写到设备中去。本文主要介绍编译和烧写的方法。 Tina Linux 编译 Tina Linux 的编译大致分为以下流程&#xff1a; (1) source bu…

php7.3安装phalcon扩展

php7安装3.4版本的phalcon扩展 适用于Centos6.x和Centos7.x系统&#xff0c;php使用7.1版本&#xff0c;wlnmp一键包已支持该扩展 phalcon扩展包地址&#xff1a;https://github.com/phalcon/cphalcon &#xff08;git clone 有可能连接不上&#xff09; 1、安装所需依赖&a…

《深入浅出存储引擎》不同数据库背后的数据存储方案

在大数据和AI时代&#xff0c;数据库成为各类应用不可或缺的重要组成部分。而数据库中的数据依赖存储引擎进行管理&#xff0c;包括数据的存储、查询、更新和删除等。因此&#xff0c;在设计系统时&#xff0c;选择正确的数据库存储引擎方案变得尤为重要。这篇文章将以关系型、…

Intersection Observer API---交叉观察器 API

Intersection Observer API 交叉观察器 API&#xff08;Intersection Observer API&#xff09;提供了一种异步检测目标元素与祖先元素或顶级文档的视口相交情况变化的方法,例如&#xff0c;可以观察判断一个div&#xff08;有大小的&#xff09;盒子在是否出现在窗口&#xff…