[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;数据库里还…

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中继&…

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; // 计算开始日期到结束日期之间的每一天 …

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枚硬币交换一张没有获得过的卡。 小林会一直抽卡&…

智能面试——录音及播放下载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…

【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安全漏洞导致挖矿事件 某公司…

Harmony鸿蒙南向驱动开发-Watchdog

看门狗&#xff08;Watchdog&#xff09;&#xff0c;又称看门狗计时器&#xff08;Watchdog timer&#xff09;&#xff0c;是一种硬件计时设备。一般有一个输入、一个输出&#xff0c;输入叫做喂狗&#xff0c;输出连接到系统的复位端。当系统主程序发生错误导致未及时清除看…

【带源码】如何开发一个视频打赏,付费观看视频的系统?

【带源码】如何开发一个视频打赏&#xff0c;付费观看视频的系统&#xff1f;开发指南来了 最近非常火爆的视频打赏系统&#xff0c;有用户端&#xff0c;管理端&#xff0c;代理端 风口来了&#xff0c;系统部署简单&#xff0c;需要详细部署教程的可以留下评论哦&#xff01…

Calico IPIP和BGP TOR的数据包走向

IPIP Mesh全网互联 文字描述 APOD eth0 10.7.75.132 -----> APOD 网关 -----> A宿主机 cali76174826315网卡 -----> Atunl0 10.7.75.128 封装 ----> Aeth0 10.120.181.20 -----> 通过网关 10.120.181.254 -----> 下一跳 BNODE eth0 10.120.179.8 解封装 --…

“FM”、“AM”信号如何解调?

同学们大家好&#xff0c;今天我们继续学习杨欣的《电子设计从零开始》&#xff0c;这本书从基本原理出发&#xff0c;知识点遍及无线电通讯、仪器设计、三极管电路、集成电路、传感器、数字电路基础、单片机及应用实例&#xff0c;可以说是全面系统地介绍了电子设计所需的知识…

蓝桥杯 每日2题 day4

碎碎念&#xff1a;好难好难&#xff0c;&#xff0c;发呆两小时什么也写不出来&#xff0c;&#xff0c;&#xff0c;周六大寄了 10.阶乘约数 - 蓝桥云课 (lanqiao.cn) 暴力跑了两个小时没出来结果&#xff0c;&#xff0c;去看题解要用数学&#xff1a;约数定理&#xff0c…

pygame发射子弹后绘制射线

import pygame import sys import mathpygame.init()screen pygame.display.set_mode((800, 600)) pygame.display.set_caption("Rotate and Shoot Bullets")# 定义子弹类 class Bullet:def __init__(self, x, y, angle):self.x xself.y yself.angle angleself.s…

经典本地影音播放器纯净无广告版

MPC-BE&#xff08;Media Player Classic Black Edition&#xff09;是来自 MPC-HC&#xff08;Media Player Classic Home Cinema&#xff09;的俄罗斯开发者重新编译优化后的一款经免费的经典全能影音播放器&#xff0c;纯净无广告&#xff0c;启动速度快&#xff0c;占用消耗…

功能测试如何到自动化测试,看这篇就够了。

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 关注公众号&#xff1a;互联网杂货铺&#xff0c;回复1 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;薪资嘎嘎涨 本帖不仅给大家介绍自动化测试&#xff0c;更会提供大…

MATLAB 点云体素滤波 (58)

MATLAB 体素滤波 (58) 一、基本原理二、算法实现1.代码数据的海量性始终是点云处理时需要面临的一个大问题,严重的时间消耗和内存占用影响了点云处理的发展,当然了,点云数量主要应该看项目的实际需求,若是对细节要求较高,那么点云数量不可过少,但是要求过低时,我们就可…