[CUDA 学习笔记] 矩阵转置算子优化

矩阵转置算子优化

矩阵转置是一种基础的矩阵操作, 即将二维矩阵的行列进行反转.
本文主要围绕行主序的二维单精度矩阵的转置考虑相关的优化.

以下 kernel 笔者均是在 NVIDIA V100 (7.0 算力) 上进行测试的, 且选择矩阵的行列维度大小为 M=2300 N=1500.

Version 0. 朴素实现

__global__ void mat_transpose_kernel_v0(const float* idata, float* odata, int M, int N) {const int x = blockIdx.x * blockDim.x + threadIdx.x;const int y = blockIdx.y * blockDim.y + threadIdx.y;if (y < M && x < N) {odata[x * M + y] = idata[y * N + x];}
}void mat_transpose_v0(const float* idata, float* odata, int M, int N) {constexpr int BLOCK_SZ = 16;dim3 block(BLOCK_SZ, BLOCK_SZ);dim3 grid((N + BLOCK_SZ - 1) / BLOCK_SZ, (M + BLOCK_SZ - 1) / BLOCK_SZ));mat_transpose_kernel_v0<<<grid, block>>>(idata, odata, M, N);
}

矩阵转置的朴素实现非常直观, 思路即使用二维的线程/线程块排布, 让每个线程负责一个矩阵元素的转置. 实现上, 只需要将矩阵的行列索引 x y 进行反转即可.
需要注意的是 gridblock 的中维度设置与多维数组中的表示是相反的, 即 grid.x 应该对应 N 维度, grid.y 应该对应 M 维度.

Version用时(us)内存带宽(GB/s)带宽利用率(%)加速比
v084.90399.9256.38

结合矩阵转置的逻辑以及 Nsight Compute 容易判断出, 矩阵转置本身是一个 memory-bound 的 kernel, 因为其核心是完成矩阵内存排布的转换, 这个过程基本不涉及计算, 因此对该 kernel 优化很重要的一点就是提高访存性能.
朴素实现直接操作矩阵所在的 GMEM, 直观看来, 矩阵转置不会涉及数据的重用, 直接操作 GMEM 本身没有问题, 但此时应该注意 GMEM 的访存特性, 其中很重要的即 GMEM 的访存合并, 即连续线程访问的 GMEM 中的数据地址是连续的, 可以将多个线程的内存访问合并为一个(或多个)内存访问, 从而减少访存次数, 提高带宽利用率.
在 Version 0 的 kernel 中, 容易看出读取时 idata[y * N + x] 是访存合并的, 因为连续线程对应的 x 是连续的, 即访问矩阵同一行连续的列; 但是写入时 odata[x * M + y] 并不是访存合并的, 因为转置后连续线程写入的是同一列连续的行, 但由于内存布局是行主序的, 因此此时每个线程访问的地址实际上并不连续, 地址差 N, 因此对 GMEM 访存性能有很大影响.

Version 1. 利用共享内存合并访存

template <int BLOCK_SZ>
__global__ void mat_transpose_kernel_v1(const float* idata, float* odata, int M, int N) {const int bx = blockIdx.x, by = blockIdx.y;const int tx = threadIdx.x, ty = threadIdx.y;__shared__ float sdata[BLOCK_SZ][BLOCK_SZ];int x = bx * BLOCK_SZ + tx;int y = by * BLOCK_SZ + ty;if (y < M && x < N) {sdata[ty][tx] = idata[y * N + x];}__syncthreads();x = by * BLOCK_SZ + tx;y = bx * BLOCK_SZ + ty;if (y < N && x < M) {odata[y * M + x] = sdata[tx][ty];}
}void mat_transpose_v1(const float* idata, float* odata, int M, int N) {constexpr int BLOCK_SZ = 16;dim3 block(BLOCK_SZ, BLOCK_SZ);dim3 grid(Ceil(N, BLOCK_SZ), Ceil(M, BLOCK_SZ));mat_transpose_kernel_v1<BLOCK_SZ><<<grid, block>>>(idata, odata, M, N);
}

Version 0 的 kernel 存在的问题是写入 GMEM 时访存不合并, 因此需要一种方式让写入 GMEM 时仍然保持线程的访存连续, 在 Version 1 中, 便使用了 SMEM 用来中转来实现访存合并.
在这里插入图片描述
Version 1 的核心思想可以使用上图进行表示, 中间的 “tile” 即可理解为存在 SMEM 的数据分片.
在读取矩阵阶段, 操作与 Version 0 一致, 区别在于将数据直接写入 SMEM 中, 对应上图橙色部分. 接着通过设置 x = by * BLOCK_SZ + tx; y = bx * BLOCK_SZ + ty; 两条语句进行了索引的重计算, 进行了线程块索引 bxby 交换, 对应上图右上角的数据分片转置后成为了左下角的数据分片. 由于此时 txty 并没有交换, 因此按照 odata[y * M + x] 写入 GMEM 时, 访存是合并的, 但需要读取 SMEM 时 txty 进行交换, 实现数据分片内的转置, 对应上图绿色部分.

Version用时(us)内存带宽(GB/s)带宽利用率(%)加速比
v084.90399.9256.38
v147.49610.5771.071.79

可以看到, Version 1 相比于 Version 0 性能有了很大提升. 如下图所示, 通过 Nsight Compute 也能看到 Version 1 (右) 比 Version 0 (左) 在读取写入 GMEM 的带宽上都有所提升.
在这里插入图片描述 在这里插入图片描述
在 Version 1 中, 引入了对 SMEM 的访存, 因此需要特别关注 bank conflict 的问题. 对于 SMEM 的写入, sdata[ty][tx], 由于 BLOCK_SZ 为 16, 32 个线程负责 SMEM 矩阵分片的两行 32 个元素, 对应 32 个 bank, 因此没有 bank conflict. 而对于 SMEM 的读取, sdata[tx][ty], 由于是按列读取 SMEM 的, threadIdx 差 1 的线程访问的数据差 BLOCK_SZ, 即导致threadIdx 差 2 的线程访问的数据落到同一 bank 的不同地址, 从而造成 16 路的 bank conflict.
因此, Version 1 中读取 SMEM 引起的 bank conflict 影响 SMEM 的访存效率, 进而影响 kernel 的性能.

Version 2. 利用 padding 解决 bank conflict

template <int BLOCK_SZ>
__global__ void mat_transpose_kernel_v2(const float* idata, float* odata, int M, int N) {const int bx = blockIdx.x, by = blockIdx.y;const int tx = threadIdx.x, ty = threadIdx.y;__shared__ float sdata[BLOCK_SZ][BLOCK_SZ+1];    // paddingint x = bx * BLOCK_SZ + tx;int y = by * BLOCK_SZ + ty;if (y < M && x < N) {sdata[ty][tx] = idata[y * N + x];}__syncthreads();x = by * BLOCK_SZ + tx;y = bx * BLOCK_SZ + ty;if (y < N && x < M) {odata[y * M + x] = sdata[tx][ty];}
}void mat_transpose_v2(const float* idata, float* odata, int M, int N) {constexpr int BLOCK_SZ = 16;dim3 block(BLOCK_SZ, BLOCK_SZ);dim3 grid(Ceil(N, BLOCK_SZ), Ceil(M, BLOCK_SZ));mat_transpose_kernel_v2<BLOCK_SZ><<<grid, block>>>(idata, odata, M, N);
}

Version 2 的代码相比于 Version 1 仅在 SMEM 内存分配时进行了变动, 将大小改为了 sdata[BLOCK_SZ][BLOCK_SZ+1], 即列维度上加入了 1 元素大小的 padding.
此时, 对于读取 SMEM 的 sdata[tx][ty], threadIdx 差 1 的线程访问的数据差 BLOCK_SZ+1, 即 17, 由于 17 与 32 互质, 因此不会有 bank conflict. 值得一提的是, 对于写入 SMEM 的 sdata[ty][tx], 由于有 1 个 padding, warp 中 lane 31 与 lane 0 访问的元素恰好差 31+1=32 个元素, 会有 1 个 bank conflict.
整体上, Version 2 通过 padding 的方法有效避免了读取 SMEM 时的 bank conflict.

Version用时(us)内存带宽(GB/s)带宽利用率(%)加速比
v084.90399.9256.38
v147.49610.5771.071.79
v244.38627.3172.471.91

Version 2 相比 Version 1性能有了一定的提升. 如下图所示, 通过 Nsight Compute 也能看到 Version 2 (右) 比 Version 1 (左) 的 bank conflict 总数明显下降, 其中读取 SMEM 时的 bank conflict (第一行) 大幅降低, 而写入 SMEM 时的 bank conflict (第二行) 有一定上升, 也与上文分析的相匹配.
在这里插入图片描述 在这里插入图片描述

Version 3. 增加每个线程处理的元素个数

template <int BLOCK_SZ, int NUM_PER_THREAD>
__global__ void mat_transpose_kernel_v3(const float* idata, float* odata, int M, int N) {const int bx = blockIdx.x, by = blockIdx.y;const int tx = threadIdx.x, ty = threadIdx.y;__shared__ float sdata[BLOCK_SZ][BLOCK_SZ+1];int x = bx * BLOCK_SZ + tx;int y = by * BLOCK_SZ + ty;constexpr int ROW_STRIDE = BLOCK_SZ / NUM_PER_THREAD;if (x < N) {#pragma unrollfor (int y_off = 0; y_off < BLOCK_SZ; y_off += ROW_STRIDE) {if (y + y_off < M) {sdata[ty + y_off][tx] = idata[(y + y_off) * N + x]; }}}__syncthreads();x = by * BLOCK_SZ + tx;y = bx * BLOCK_SZ + ty;if (x < M) {for (int y_off = 0; y_off < BLOCK_SZ; y_off += ROW_STRIDE) {if (y + y_off < N) {odata[(y + y_off) * M + x] = sdata[tx][ty + y_off];}}}
}void mat_transpose_v3(const float* idata, float* odata, int M, int N) {constexpr int BLOCK_SZ = 32;constexpr int NUM_PER_THREAD = 4;dim3 block(BLOCK_SZ, BLOCK_SZ/NUM_PER_THREAD);dim3 grid(Ceil(N, BLOCK_SZ), Ceil(M, BLOCK_SZ));mat_transpose_kernel_v3<BLOCK_SZ, NUM_PER_THREAD><<<grid, block>>>(idata, odata, M, N);
}

Version 3 相比于 Version 2, 增加了每个线程处理的元素个数, 即由先前的每个线程处理 1 个元素的转置, 变为处理 NUM_PER_THREAD 个元素的转置. 该实现主要是参考了 英伟达的技术博客.
在实现上, 同样保持原本 256 线程的线程块大小, 设置每个线程处理 4 个元素, 则每个线程块数据分片的大小调整为 32×32, 而线程块的线程采取 8×32 的二维排布, 因此需要在行维度上需要迭代 4 次完成转置.

考虑 Version 3 相比于 Version 2 的优势, 主要是在保持线程块中线程数量不变的情况下, 处理的线程块数据分片大小变大, 这样会减少线程网格中启动的线程块数量, 而增大了每个线程的计算强度; 此外, 由于 BLOCK_SZ 变为 32, Version 2 中写入 SMEM 的 1 个 bank conflict 也可以被避免.
这让笔者想到了 Reduce 算子中也会考虑增加每个线程处理的元素来提高性能, 笔者主观的感觉是对于这种计算强度比较低的 kernel, 增加线程处理的元素个数即计算强度, 一定程度上能增大 GPU 中计算与访存的掩盖, 并配合循环展开提高指令级并行; 此外, 由于线程块数量的减少, 能在相对少的 wave 中完成计算, 减少 GPU 的线程块调度上可能也会带来性能的收益.

Version用时(us)内存带宽(GB/s)带宽利用率(%)加速比
v084.90399.9256.38
v147.49610.5771.071.79
v244.38627.3172.471.91
v339.68699.0581.432.14

通过测试可以看出, Version 3 相比 Version 2 又有了一定的性能提升, 内存带宽也有了进一步的提高.

Version 3.5 向量化读取

#define FETCH_CFLOAT4(p) (reinterpret_cast<const float4*>(&(p))[0])
#define FETCH_FLOAT4(p) (reinterpret_cast<float4*>(&(p))[0])template <int BLOCK_SZ>
__global__ void mat_transpose_kernel_v3_5(const float* idata, float* odata, int M, int N) {const int bx = blockIdx.x, by = blockIdx.y;const int tx = threadIdx.x, ty = threadIdx.y;__shared__ float sdata[BLOCK_SZ][BLOCK_SZ];int x = bx * BLOCK_SZ + tx * 4;int y = by * BLOCK_SZ + ty;if (x < N && y < M) {FETCH_FLOAT4(sdata[ty][tx * 4]) = FETCH_CFLOAT4(idata[y * N + x]);}__syncthreads();x = by * BLOCK_SZ + tx * 4;y = bx * BLOCK_SZ + ty;float tmp[4];if (x < M && y < N) {#pragma unrollfor (int i = 0; i < 4; ++i) {tmp[i] = sdata[tx * 4 + i][ty];}FETCH_FLOAT4(odata[y * M + x]) = FETCH_FLOAT4(tmp);}
}void mat_transpose_v3_5(const float* idata, float* odata, int M, int N) {constexpr int BLOCK_SZ = 32;dim3 block(BLOCK_SZ / 4, BLOCK_SZ);dim3 grid(Ceil(N, BLOCK_SZ), Ceil(M, BLOCK_SZ));mat_transpose_kernel_v3_5<BLOCK_SZ><<<grid, block>>>(idata, odata, M, N);
}

Version 3 是通过在行维度上迭代多次实现了每个线程处理多个元素的目的. 这里笔者尝试使用向量化访存的形式在列维度上让每个线程负责连续的多个元素.

在实现上, BLOCK_SZ 仍然是 32, 即线程块数据分片的大小保持 32×32, 而线程块的线程采取 32×8 的二维排布, 这是因为列维度上使用向量化访存一次读取 4 个元素.
但是, 向量化访存也带了一些问题, 首先向量化访存需要地址对齐, 因此不能使用 Version 2 中的 padding 方式, 因为 padding 的 1 个元素会导致访问元素并不是 4 个元素地址对齐的; 另一方面, 在从 SMEM 转置写入 GMEM 输出时, 是按列读取 SMEM 的, 无法直接使用向量化访存, 因此需要逐元素读取到寄存器后再向量化访存写入 GMEM.

Version用时(us)内存带宽(GB/s)带宽利用率(%)加速比
v084.90399.9256.38
v147.49610.5771.071.79
v244.38627.3172.471.91
v339.68699.0581.432.14
v3.542.46679.8082.642.00

通过测试可以看出, Version 3.5 的性能相比 Version 2 有一定提升, 但并不如 Version 3., 受益于向量化访存, 带宽相比于 Version 2 也有一定提升, 但由于无法 padding 避免 bank conflict, 也导致其性能并不如 Version 3.

Version 4. 减少条件分支

template <int BLOCK_SZ, int NUM_PER_THREAD>
__global__ void mat_transpose_kernel_v4(const float* idata, float* odata, int M, int N) {const int bx = blockIdx.x, by = blockIdx.y;const int tx = threadIdx.x, ty = threadIdx.y;__shared__ float sdata[BLOCK_SZ][BLOCK_SZ+1];int x = bx * BLOCK_SZ + tx;int y = by * BLOCK_SZ + ty;constexpr int ROW_STRIDE = BLOCK_SZ / NUM_PER_THREAD;if (x < N) {if (y + BLOCK_SZ <= M) {#pragma unrollfor (int y_off = 0; y_off < BLOCK_SZ; y_off += ROW_STRIDE) {sdata[ty + y_off][tx] = idata[(y + y_off) * N + x]; }} else {for (int y_off = 0; y_off < BLOCK_SZ; y_off += ROW_STRIDE) {if (ty + y_off < M) {sdata[ty + y_off][tx] = idata[(y + y_off) * N + x];}}}}__syncthreads();x = by * BLOCK_SZ + tx;y = bx * BLOCK_SZ + ty;if (x < M) {if (y + BLOCK_SZ <= N) {#pragma unrollfor (int y_off = 0; y_off < BLOCK_SZ; y_off += ROW_STRIDE) {odata[(y + y_off) * M + x] = sdata[tx][ty + y_off];}} else {for (int y_off = 0; y_off < BLOCK_SZ; y_off += ROW_STRIDE) {if (y + y_off < N) {odata[(y + y_off) * M + x] = sdata[tx][ty + y_off];}}}}
}void mat_transpose_v4(const float* idata, float* odata, int M, int N) {constexpr int BLOCK_SZ = 32;constexpr int NUM_PER_THREAD = 4;dim3 block(BLOCK_SZ, BLOCK_SZ/NUM_PER_THREAD);dim3 grid(Ceil(N, BLOCK_SZ), Ceil(M, BLOCK_SZ));mat_transpose_kernel_v4<BLOCK_SZ, NUM_PER_THREAD><<<grid, block>>>(idata, odata, M, N);
}

Version 4 参考了 CUTLASS 中 N C H W NCHW NCHW N H W C NHWC NHWC 格式转换的 kernel, 该 kernel 本质上就是 C C C H W HW HW 两个维度的矩阵转置.
在实现上, Version 4 的 kernel 与 Version 3 的核心思路是一致的, 仍然是每个线程需要行维度上迭代多次. 不同的是, 该实现对分支语句的判断进行了更加细致的处理. 具体而言, 在 Version 3 中, 每个线程的每次行迭代都要进行 y + y_off 的判断, 即矩阵元素的范围检查, 但考虑大部分线程块的数据分片都是在矩阵内部的, 即不会越界, 那么执行多次范围检查就会比较多余. 在 Version 4 中, 会先检查 y + BLOCK_SZ <= M, 即如果该线程块的数据分片并不在矩阵边缘, 那么每次迭代时便无需再进行越界检查; 反之, 若数据分片位于矩阵边缘, 则需要和 Version 3 一样每次迭代时越界检查.
整体看来, 该实现虽然增加了代码复杂程度, 但会简化大部分线程块中迭代时的逻辑, 减少冗余的条件分支的判断次数.

Version用时(us)内存带宽(GB/s)带宽利用率(%)加速比
v084.90399.9256.38
v147.49610.5771.071.79
v244.38627.3172.471.91
v339.68699.0581.432.14
v3.542.46679.8082.642.00
v440.42686.5080.252.10

有意思的是, 在实际测试时, Version 4 的性能与 Version 3 很接近, 但与 Version 3 仍有略微的性能差距, 且带宽上也并没有 Version 3 高.

参考文献

  • An Efficient Matrix Transpose in CUDA C/C++ | NVIDIA Technical Blog
  • CUDA 编程入门之矩阵转置 - 知乎
  • CUDA学习(二)矩阵转置及优化(合并访问、共享内存、bank conflict) - 知乎
  • NVIDIA/cutlass - device_nchw_to_nhwc.h

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

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

相关文章

处理慢查询时使用explain一般看哪些字段

explain之后会出现这些&#xff0c;一般就只看下面这几个字段 select_type就是查询类型&#xff0c;在我司的业务里基本上用的都是简单查询&#xff0c;在内存中处理逻辑&#xff0c;复杂查询的话排查问题比较麻烦&#xff0c;引起慢查询还会拖累数据库&#xff0c;数据库里还…

jvm运行情况预估

相关系统 jvm优化原则-CSDN博客 执行下面指令 jstat gc -pid 能计算出一些关键数据&#xff0c;有了这些数据&#xff0c;先给JVM参数一些的初始的&#xff0c;比堆内存大小、年轻代大小 、Eden和Srivivor的比例&#xff0c;老年代的大小&#xff0c;大对象的阈值&#xff0c;…

前端学习<四>JavaScript基础——05-变量的数据类型:基本数据类型和引用数据类型

数据分类 数据分为&#xff1a;静态数据、动态数据。 静态数据 静态数据是指一些永久性的数据。一般是以文件的形式存储在硬盘上&#xff0c;比如文档、照片、视频等文件。 电脑关闭后&#xff0c;静态数据仍然还在。只要不主动删掉数据或者硬盘没损坏&#xff0c;这些数据…

蓝桥杯c语言

蓝桥杯大赛是中国的一项全国性的IT竞赛&#xff0c;旨在激发大学生和中学生对计算机科学和技术的兴趣&#xff0c;提高他们的编程能力和创新思维。C语言是蓝桥杯大赛中的一个重要竞赛类别&#xff0c;通常包括程序设计、算法解决和数据结构实现等方面的内容。 ### 蓝桥杯C语言…

three.js尝试渲染gbl模型成功!(三)

参照教程&#xff1a;https://cloud.tencent.com/developer/article/2276766?areaSource102001.5&traceId88k805RaN_gYngNdKvALJ &#xff08;作者&#xff1a;九仞山&#xff09; 通过最近两天查three.js入门教程了解到 这玩应支持包括 .obj、.gltf等类型的模型结构。 g…

微服务-网关

在微服务架构中&#xff0c;每个服务都是一个可以独立开发和运行的组件&#xff0c;而一个完整的微服务架构由一系列独立运行的微服务组成。其中每个服务都只会完成特定领域的功能&#xff0c;比如订单服务提供与订单业务场景有关的功能、商品服务提供商品展示功能等。各个微服…

2.SpringBoot利用Thymeleaf实现页面的展示

什么是Thymeleaf&#xff1f; Thymeleaf是一个现代服务器端Java模板引擎&#xff0c;适用于Web和独立环境&#xff0c;能够处理HTML&#xff0c;XML&#xff0c;JavaScript&#xff0c;CSS甚至纯文本。 Thymeleaf的主要目标是提供一种优雅且高度可维护的模板创建方式。为实现这…

斐讯E1拆机焊接TTL救砖

从老家的柜子里翻出来一台斐讯E1&#xff0c;老家在用的是斐讯K2P&#xff0c;300M宽带&#xff0c;房间和大部分位置wifi5足够跑满了&#xff0c;一直懒得升级&#xff0c;也足够用了。 不过发现部分位置信号比较弱&#xff0c;都不到50M&#xff0c;考虑插上E1做个AP中继&…

CASL Tutorial Chinese

Attribute-based access control (ABAC) Role-based access control (RBAC) Policy-based access control (PBAC) Claims-based access control (CBAC) 通过研究CASL&#xff0c;设计一款VUE的认证和权限的产品 do - name of the action (e.g., read, update). Has an alia…

106. 跑步锻炼(结果填空)

public class Main { public static void main(String[] args) { int startYear 2000; int startMonth 1; int startDay 1; // 周六 int endYear 2020; int endMonth 10; int endDay 1; // 周四 int totalDistance 0; // 计算开始日期到结束日期之间的每一天 …

【LocalAI】(3):LocalAI本地使用Model gallery,对qwen模型进行配置,使用modescope源下载,本地运行速度快。特别简单!

1&#xff0c;关于localai LocalAI 是一个用于本地推理的&#xff0c;与 OpenAI API 规范兼容的 REST API。 它允许您在本地使用消费级硬件运行 LLM&#xff08;不仅如此&#xff09;&#xff0c;支持与 ggml 格式兼容的多个模型系列。支持CPU硬件/GPU硬件。 模型启动方法&am…

Spring与SpringBoot的区别

Spring是一个开源的Java应用程序框架&#xff0c;旨在简化企业级Java应用程序的开发。它提供了一个轻量级的容器&#xff0c;用于管理应用程序中的各个组件&#xff08;如依赖注入、AOP等&#xff09;&#xff0c;并提供了丰富的功能和模块&#xff0c;用于处理数据库访问、事务…

Acwing.4009 收集卡牌(期望dp)

题目 小林在玩一个抽卡游戏&#xff0c;其中有 n种不同的卡牌&#xff0c;编号为 1到 n。 每一次抽卡&#xff0c;她获得第 i种卡牌的概率为 pi。 如果这张卡牌之前已经获得过了&#xff0c;就会转化为一枚硬币。 可以用 k枚硬币交换一张没有获得过的卡。 小林会一直抽卡&…

【ZZULIOJ】1055: 兔子繁殖问题(Java)

目录 题目描述 输入 输出 样例输入 Copy 样例输出 Copy 提示 code 题目描述 这是一个有趣的古典数学问题&#xff0c;著名意大利数学家Fibonacci曾提出一个问题&#xff1a;有一对小兔子&#xff0c;从出生后第3个月起每个月都生一对兔子。小兔子长到第3个月后每个月又…

【2024】elasticsearch的安装及使用建议

为了方便本节将使用docker部署Elasticsearch&#xff0c;以下简称es。 Elasticsearch 1.基础理论部分1.1.简单介绍一下es1.2.es的基本概念和相关术语 2.elasticsearch安装部分3.elasticsearch配置及建议 1.基础理论部分 1.1.简单介绍一下es Elasticsearch是一个开源的、分布…

智能面试——录音及播放下载js-audio-recorder — post请求,formdata传参

录音插件 js-audio-recorder bug&#xff1a;本地调试调取不起来麦克风 浏览器配置安全域名 chrome://flags/Insecure origins treated as secure输入域名即可电脑需要连接上耳机 <template><div class"BaseRecorder"><div class"BaseRecorder-r…

C++递归递推混合

昆虫繁殖 题目描述&#xff1a;科学家在热带森林中发现了一种特殊的昆虫&#xff0c;这种昆虫的繁殖能力很强。每对成虫过x个月 产y对卵&#xff0c;每对卵要过两个月长成成虫。假设每个成虫不死&#xff0c;第一个月只有一对成虫&#xff0c;且卵 长成成虫后的第一个月不产卵…

【UE5 C++】各个头文件的含义

#pragma once 预处理程序指令 作用&#xff1a;保护同一个文件不会被多次包含&#xff0c;使得头文件只会被编译一次&#xff0c; #include “CoreMinimal.h” 包含了一套来自UE4的核心编程环境的普遍存在类型 #include “GameFramework/GameModeBase.h” 基于GameModeBas…

应急响应-挖矿脚本检测指南威胁情报样本定性文件清除入口修复

一、演示案例-挖矿样本-Win&Linux-危害&定性 危害&#xff1a;CPU拉满&#xff0c;网络阻塞&#xff0c;服务器卡顿等 定性&#xff1a;威胁情报平台上传解析分析&#xff0c;文件配置查看等windows样本 linux样本 二、演示案例-Linux-Web安全漏洞导致挖矿事件 某公司…

PyQt介绍——qrc资源文件使用介绍

使用介绍 在PyQt中&#xff0c;qrc文件是一种资源文件&#xff0c;用于将应用程序所需的资源&#xff08;如图像、音频文件、样式表等&#xff09;打包到一个单独的二进制文件中&#xff0c;以便在运行时轻松地加载和访问这些资源。qrc文件是用XML格式编写的&#xff0c;其中包…