CUDA高级优化实战:Stream、特殊内存与卷积优化—Week3学习总结

CUDA高级优化实战:Stream、特殊内存与卷积优化---Week3学习总结

一、写在前面

前两周跟着课程学完基础知识和Shared Memory优化后,说实话,当时看到矩阵乘法从几百ms优化到几十ms,那种成就感真的很爽。Week 3进入了更硬核的内容,学完这周我感觉自己对GPU的理解又上了一个台阶。

这周主要搞了三个方向的东西:

首先是CUDA Stream,这玩意儿让我意识到GPU不只是能并行计算,连数据传输都能和计算同时进行。

然后是Constant Memory和Texture Memory这两种特殊内存。之前只知道Global Memory和Shared Memory,这周发现GPU还藏了不少"黑科技"------Constant Memory能把小数据广播给整个warp,Texture Memory针对2D访问有专门的硬件优化。

最后拿2D卷积做了个综合练习,从naive的版本一路优化到结合各种技术的版本,性能提升了3.6倍。这个过程让我真正理解了"性能优化不是单点突破,而是系统工程"这句话的含义。

这篇文章记录了我这周的学习过程和踩过的坑,希望对同样在学CUDA的朋友有帮助。

二、CUDA Stream:让GPU"一心多用"

2.1 Stream到底是个啥

刚开始看到Stream这个概念的时候,我是有点懵的。后来想明白了,其实就是GPU上的一个任务队列。

你可以这么理解:默认情况下,所有CUDA操作都在一条"流水线"(默认Stream 0)上排队执行,前一个任务不结束,后一个任务就得等着。这就像食堂只开了一个窗口,大家只能排队。

而创建多个Stream就像开了多个窗口,可以:

  • 边传数据边计算:一个Stream在跑kernel的时候,另一个Stream可以同时往GPU传数据
  • 多个kernel同时跑:只要GPU资源够,不同Stream里的kernel可以并发执行
  • 更灵活的任务调度:想让哪个任务先跑就先跑哪个

2.2 代码实战:怎么用Stream

代码其实不复杂,看个例子就懂了:

cpp 复制代码
// 创建Stream
cudaStream_t stream1, stream2;
cudaStreamCreate(&stream1);
cudaStreamCreate(&stream2);

// 在Stream中执行操作
cudaMemcpyAsync(d_data1, h_data1, size, cudaMemcpyHostToDevice, stream1);
kernel1<<<grid, block, 0, stream1>>>(d_data1);

cudaMemcpyAsync(d_data2, h_data2, size, cudaMemcpyHostToDevice, stream2);
kernel2<<<grid, block, 0, stream2>>>(d_data2);

// 同步Stream
cudaStreamSynchronize(stream1);
cudaStreamSynchronize(stream2);

// 销毁Stream
cudaStreamDestroy(stream1);
cudaStreamDestroy(stream2);

这里有几个坑要注意

  • 一定要用cudaMemcpyAsync,不要用cudaMemcpy。后者会把整个设备都block住,就失去并发的意义了
  • Host端的内存必须用cudaMallocHost分配(叫pinned memory),普通malloc出来的内存没法真正异步传输
  • Kernel启动时第四个参数指定用哪个Stream,别忘了加

2.3 流水线模式:传输和计算同时搞

这个技巧特别实用。思路就是把数据切成几块,每块走"传到GPU → 计算 → 传回CPU"这个流程。关键是不同块可以同时处于流程的不同阶段,就像工厂流水线一样。

cpp 复制代码
const int nStreams = 4;
const int chunkSize = N / nStreams;
cudaStream_t streams[nStreams];

for (int i = 0; i < nStreams; i++) {
    cudaStreamCreate(&streams[i]);
}

for (int i = 0; i < nStreams; i++) {
    int offset = i * chunkSize;
    
    // H2D传输
    cudaMemcpyAsync(&d_data[offset], &h_data[offset], 
                    chunkSize * sizeof(float), 
                    cudaMemcpyHostToDevice, streams[i]);
    
    // Kernel计算
    kernel<<<grid, block, 0, streams[i]>>>(&d_data[offset], chunkSize);
    
    // D2H传输
    cudaMemcpyAsync(&h_result[offset], &d_result[offset], 
                    chunkSize * sizeof(float), 
                    cudaMemcpyDeviceToHost, streams[i]);
}

cudaDeviceSynchronize();

2.4 实测数据:效果怎么样

我在RTX 4060上测了下(处理256MB数据):

实现方式 执行时间 加速比
单Stream顺序执行 45.2ms 1.0x
4 Streams并发 28.7ms 1.57x
8 Streams并发 25.3ms 1.79x

几点经验

  • Stream不是越多越好,我试过16个Stream反而变慢了,因为调度开销太大
  • 每个chunk的数据量要够大,太小的话kernel启动开销就占主导了
  • 可以用cudaDeviceSetCacheConfig调整L1 Cache配置,有时候能再榨点性能

三、特殊内存:GPU的"私房菜"

3.1 Constant Memory:专为小数据优化的缓存

之前只知道Global Memory和Shared Memory,这周发现NVIDIA还藏了个好东西------Constant Memory。这块内存只有64KB,但有三个很厉害的特性:

  • 专属缓存:有自己的constant cache,速度贼快
  • 广播机制:一个warp里的所有线程读同一个地址,只需要一次内存访问。特别适合卷积核这种所有线程都要用的数据
  • 只读:kernel里只能读不能写,数据必须从CPU端写进去

声明与使用

cpp 复制代码
// 设备端声明(文件作用域)
__constant__ float const_kernel[KERNEL_SIZE];

// Host端初始化
float h_kernel[KERNEL_SIZE] = {/* 卷积核数据 */};
cudaMemcpyToSymbol(const_kernel, h_kernel, 
                   KERNEL_SIZE * sizeof(float));

// Kernel中使用
__global__ void convKernel(float* output, float* input) {
    // 直接访问,无需传参
    float value = input[idx] * const_kernel[0];
}

什么时候用它

  • 卷积核、滤波器这种小查找表(反正也就几KB)
  • 物理常数、数学系数之类的
  • 所有线程都要频繁访问的相同数据

3.2 Texture Memory:图像处理的神器

Texture Memory原本是为图形渲染设计的,但用来做通用计算也很香。它有几个硬件级的优化:

  • 2D/3D访问优化:专门针对空间局部性设计的缓存,比如访问图像相邻像素这种场景
  • 自动插值:硬件直接支持线性插值、双线性插值,省得自己写代码算了
  • 边界处理:越界访问可以自动Clamp(边缘拉伸)或Wrap(平铺/重复),不用手写一堆if判断

用法稍微复杂点(现代CUDA用Texture对象,不是老的Texture Reference):

cpp 复制代码
// 1. 分配内存并填充数据
float* d_input;
cudaMalloc(&d_input, width * height * sizeof(float));

// 2. 创建资源描述符
cudaResourceDesc resDesc = {};
resDesc.resType = cudaResourceTypePitch2D;
resDesc.res.pitch2D.devPtr = d_input;
resDesc.res.pitch2D.width = width;
resDesc.res.pitch2D.height = height;
resDesc.res.pitch2D.pitchInBytes = width * sizeof(float);
resDesc.res.pitch2D.desc = cudaCreateChannelDesc<float>();

// 3. 创建纹理描述符
cudaTextureDesc texDesc = {};
texDesc.addressMode[0] = cudaAddressModeClamp;
texDesc.addressMode[1] = cudaAddressModeClamp;
texDesc.filterMode = cudaFilterModeLinear;  // 线性插值
texDesc.readMode = cudaReadModeElementType;
texDesc.normalizedCoords = false;

// 4. 创建纹理对象
cudaTextureObject_t texObj;
cudaCreateTextureObject(&texObj, &resDesc, &texDesc, nullptr);

// 5. Kernel中使用
__global__ void texKernel(float* output, cudaTextureObject_t tex) {
    float value = tex2D<float>(tex, x, y);  // 硬件插值
}

性能对比(图像卷积,2048x2048):

访问方式 带宽利用率 执行时间
Global Memory 62% 3.8ms
Texture Memory 89% 2.1ms

3.3 使用场景对比

特性 Constant Memory Texture Memory Shared Memory
容量 64KB 无限制(使用Global) 48KB/SM
访问模式 所有线程访问相同数据 2D/3D空间局部性 任意模式
缓存策略 广播 硬件优化的2D缓存 用户管理
典型应用 卷积核、系数 图像处理、采样 块内数据共享

四、原子操作和Warp Shuffle

4.1 原子操作:多线程抢同一块地的解决方案

写并行代码时经常遇到一个问题:好几个线程同时要改同一个内存位置的值,怎么办?直接写肯定会出问题(race condition)。这时候就要用原子操作。

原子操作保证了"读-改-写"这个过程是一气呵成的,中间不会被别的线程插队。

cpp 复制代码
__global__ void histogramKernel(int* hist, int* data, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        int bin = data[idx];
        atomicAdd(&hist[bin], 1);  // 原子加法
    }
}

常用原子操作

  • atomicAdd/Sub/Exch/Min/Max
  • atomicCAS(Compare-And-Swap):实现复杂原子逻辑的基础
  • atomicAnd/Or/Xor:位操作

关于性能的坑

  • 原子操作本身不慢,慢的是冲突。如果一堆线程都在抢同一个位置,那就得排队等,性能就掉下来了
  • 冲突严重的时候,可以先在Shared Memory里做块内聚合,最后再一次性原子更新全局结果
  • 新卡(Compute Capability 6.0之后)的原子操作比老卡快多了,所以别被网上老文章吓到

一个实际优化案例

cpp 复制代码
// 优化前:直接原子操作
__global__ void histNaive(int* hist, int* data) {
    int idx = threadIdx.x + blockIdx.x * blockDim.x;
    atomicAdd(&hist[data[idx]], 1);  // 全局原子冲突高
}

// 优化后:两级聚合
__global__ void histOptimized(int* hist, int* data) {
    __shared__ int s_hist[NUM_BINS];
    
    // 块内初始化
    if (threadIdx.x < NUM_BINS) s_hist[threadIdx.x] = 0;
    __syncthreads();
    
    // 块内原子累加
    int idx = threadIdx.x + blockIdx.x * blockDim.x;
    atomicAdd(&s_hist[data[idx]], 1);
    __syncthreads();
    
    // 块间原子更新
    if (threadIdx.x < NUM_BINS) {
        atomicAdd(&hist[threadIdx.x], s_hist[threadIdx.x]);
    }
}

性能提升:3.2x(实测数据,1M元素,256 bins)

4.2 Warp Shuffle:warp内部的"传纸条"

这个功能我觉得设计得很巧妙。同一个warp里的线程可以直接交换寄存器数据,不用经过Shared Memory。

想象一下,32个人站成一排,每个人手里有个数字。用Shuffle指令,第1个人可以直接看到第17个人手里的数,不需要把数字写到黑板上(Shared Memory)再读回来。

cpp 复制代码
// Warp内规约求和
__inline__ __device__ float warpReduce(float val) {
    for (int offset = 16; offset > 0; offset >>= 1) {
        val += __shfl_down_sync(0xffffffff, val, offset);
    }
    return val;
}

// 完整的块规约
__global__ void reduceKernel(float* g_out, float* g_in, int n) {
    __shared__ float s_data[32];  // 每warp一个元素
    
    int tid = threadIdx.x;
    int idx = blockIdx.x * blockDim.x + tid;
    
    float sum = (idx < n) ? g_in[idx] : 0.0f;
    
    // Warp内规约
    sum = warpReduce(sum);
    
    // 每warp的首线程写入Shared Memory
    if (tid % 32 == 0) {
        s_data[tid / 32] = sum;
    }
    __syncthreads();
    
    // 最后一个warp处理块间结果
    if (tid < 32) {
        sum = s_data[tid];
        sum = warpReduce(sum);
        if (tid == 0) g_out[blockIdx.x] = sum;
    }
}

优势

  • 无需Shared Memory,节省资源
  • 延迟极低(仅1-2个时钟周期)
  • 代码简洁,易于维护

五、卷积优化:把前面学的东西串起来

卷积是深度学习里最常见的操作,优化好了能直接影响整个模型的训练/推理速度。这周正好拿2D卷积练手,把Stream、Constant Memory、Texture Memory这些技术都用上了。

从最简单的版本一直优化到综合各种技术的版本,最后性能提升了3.6倍。这个过程让我深刻体会到:优化不是靠某个单一技巧,而是要系统性地分析瓶颈、选择合适的方法。

5.1 问题定义

输入矩阵:input[HEIGHT][WIDTH]

卷积核:kernel[KERNEL_SIZE][KERNEL_SIZE](如5x5)

输出:output[HEIGHT][WIDTH]

基本计算:

复制代码
output[y][x] = Σ Σ input[y+j][x+i] * kernel[j][i]

5.2 Version 1:最朴素的实现

先写个最简单的版本当baseline:

cpp 复制代码
__global__ void convNaive(float* output, float* input, 
                          float* kernel, int width, int height) {
    int x = blockIdx.x * blockDim.x + threadIdx.x;
    int y = blockIdx.y * blockDim.y + threadIdx.y;
    
    if (x < width && y < height) {
        float sum = 0.0f;
        int half = KERNEL_SIZE / 2;
        
        for (int ky = 0; ky < KERNEL_SIZE; ky++) {
            for (int kx = 0; kx < KERNEL_SIZE; kx++) {
                int ix = x + kx - half;
                int iy = y + ky - half;
                
                // 边界检查
                if (ix >= 0 && ix < width && iy >= 0 && iy < height) {
                    sum += input[iy * width + ix] * 
                           kernel[ky * KERNEL_SIZE + kx];
                }
            }
        }
        output[y * width + x] = sum;
    }
}

问题在哪儿

  • Global Memory访问太多:每个输出像素要读25次输入(5x5的核)
  • 数据重复读取:相邻的线程会读到很多重叠的数据,但每个线程都傻傻地自己读一遍
  • 没利用任何缓存

跑了个2048x2048的图像,5x5的核,花了12.6ms。这就是我们的baseline。

5.3 Version 2:用上Constant Memory

想到卷积核很小(5x5才25个float),而且所有线程都要用,这不就是Constant Memory的典型场景吗?

cpp 复制代码
__constant__ float const_kernel[KERNEL_SIZE * KERNEL_SIZE];

// Host端初始化
cudaMemcpyToSymbol(const_kernel, h_kernel, 
                   KERNEL_SIZE * KERNEL_SIZE * sizeof(float));

__global__ void convConstant(float* output, float* input, 
                             int width, int height) {
    int x = blockIdx.x * blockDim.x + threadIdx.x;
    int y = blockIdx.y * blockDim.y + threadIdx.y;
    
    if (x < width && y < height) {
        float sum = 0.0f;
        int half = KERNEL_SIZE / 2;
        
        for (int ky = 0; ky < KERNEL_SIZE; ky++) {
            for (int kx = 0; kx < KERNEL_SIZE; kx++) {
                int ix = x + kx - half;
                int iy = y + ky - half;
                
                if (ix >= 0 && ix < width && iy >= 0 && iy < height) {
                    // 从Constant Memory读取核
                    sum += input[iy * width + ix] * 
                           const_kernel[ky * KERNEL_SIZE + kx];
                }
            }
        }
        output[y * width + x] = sum;
    }
}

改完测了下,10.8ms,快了1.17倍。提升不算特别大,但考虑到只改了几行代码,性价比还行。

5.4 Version 3:Shared Memory发力

这个优化是大头。核心思路是:把一块输入数据(连带边界)先加载到Shared Memory,然后这个block里的所有线程都从Shared Memory读,避免重复访问Global Memory。

这就是所谓的Tiling策略。

cpp 复制代码
#define TILE_SIZE 16
#define BLOCK_SIZE (TILE_SIZE + KERNEL_SIZE - 1)  // 含边界:16+4=20

__global__ void convShared(float* output, float* input, 
                           int width, int height) {
    __shared__ float s_input[BLOCK_SIZE][BLOCK_SIZE];
    
    int tx = threadIdx.x;
    int ty = threadIdx.y;
    int x = blockIdx.x * TILE_SIZE + tx;
    int y = blockIdx.y * TILE_SIZE + ty;
    int half = KERNEL_SIZE / 2;
    
    // 协作加载数据到Shared Memory(含边界)
    for (int i = ty; i < BLOCK_SIZE; i += blockDim.y) {
        for (int j = tx; j < BLOCK_SIZE; j += blockDim.x) {
            int gx = blockIdx.x * TILE_SIZE + j - half;
            int gy = blockIdx.y * TILE_SIZE + i - half;
            
            if (gx >= 0 && gx < width && gy >= 0 && gy < height) {
                s_input[i][j] = input[gy * width + gx];
            } else {
                s_input[i][j] = 0.0f;  // 边界填充
            }
        }
    }
    __syncthreads();
    
    // 计算(从Shared Memory读取)
    if (tx < TILE_SIZE && ty < TILE_SIZE && x < width && y < height) {
        float sum = 0.0f;
        for (int ky = 0; ky < KERNEL_SIZE; ky++) {
            for (int kx = 0; kx < KERNEL_SIZE; kx++) {
                sum += s_input[ty + ky][tx + kx] * 
                       const_kernel[ky * KERNEL_SIZE + kx];
            }
        }
        output[y * width + x] = sum;
    }
}

几个关键点

  • 数据重用率高:一个输入元素会被多个输出元素用到,现在只需要从Global Memory读一次
  • Global Memory访问量暴降:从每个输出读25次降到每个输出读1次(均摊下来)
  • 要注意Bank Conflict:我调整了下Shared Memory的布局,避免bank conflict

这版跑出来4.2ms,比baseline快了3倍!这才对嘛。

5.5 Version 4:试试Texture Memory

Texture Memory天生就是为2D访问优化的,图像卷积正好符合这个场景。而且Texture的边界处理是硬件自动做的,省了不少代码。

cpp 复制代码
__global__ void convTexture(float* output, cudaTextureObject_t texInput,
                            int width, int height) {
    int x = blockIdx.x * blockDim.x + threadIdx.x;
    int y = blockIdx.y * blockDim.y + threadIdx.y;
    
    if (x < width && y < height) {
        float sum = 0.0f;
        int half = KERNEL_SIZE / 2;
        
        for (int ky = 0; ky < KERNEL_SIZE; ky++) {
            for (int kx = 0; kx < KERNEL_SIZE; kx++) {
                // Texture fetch,自动处理边界
                float val = tex2D<float>(texInput, 
                                         x + kx - half, 
                                         y + ky - half);
                sum += val * const_kernel[ky * KERNEL_SIZE + kx];
            }
        }
        output[y * width + x] = sum;
    }
}

测了下3.8ms,比Shared Memory版本还快一点(3.3倍加速)。

Shared Memory vs Texture怎么选

  • Shared Memory:代码写起来稍微麻烦点,但你能精确控制每一步
  • Texture Memory:硬件帮你优化好了,写代码简单,但相对"黑盒"一些
  • 实际项目里看情况选。我个人倾向于先试Texture,不行再上Shared Memory手动优化

5.6 完整性能对比

版本 执行时间 加速比 带宽利用率 关键优化
Naive 12.6ms 1.0x 45% -
Constant Memory 10.8ms 1.17x 52% 卷积核缓存
Shared Memory 4.2ms 3.0x 78% 数据重用
Texture Memory 3.8ms 3.3x 81% 硬件缓存
Shared + Texture 3.5ms 3.6x 85% 混合策略

测试环境:RTX 3060, 2048x2048图像, 5x5卷积核

六、性能分析工具:不能只靠猜

之前优化主要靠"感觉",这周学会了用专业工具定位问题。有工具和没工具,效率差太多了。

6.1 nvprof:命令行下的性能分析神器

nvprof是NVIDIA自带的profiler,虽然新版推荐用Nsight Compute,但nvprof简单粗暴,适合快速查问题。

基本用法:

bash 复制代码
# 基础性能分析
nvprof ./my_cuda_app

# 详细kernel信息
nvprof --print-gpu-trace ./my_cuda_app

# 度量特定指标
nvprof --metrics achieved_occupancy,gld_efficiency ./my_cuda_app

# 事件分析
nvprof --events l1_cache_global_hit_rate ./my_cuda_app

看哪些指标

  • Occupancy(占用率):有多少warp在同时跑

    • 一般大于50%就行,不用追求100%(有时候反而慢)
    • 太低的话可能是寄存器或Shared Memory用太多了
  • Memory Throughput(内存吞吐)

    • gld_efficiency:Global Load效率
    • gst_efficiency:Global Store效率
    • 这俩指标低说明内存访问有问题,可能没对齐或者有bank conflict
  • Instruction Throughput

    • ipc(每周期执行多少指令)
    • IPC低说明在等内存或者指令之间有依赖

6.2 Nsight Compute:深度剖析利器

这个工具比nvprof强大多了,信息详细到有点吓人。不过习惯了之后真香。

Nsight Compute提供了更详细的分析界面:

bash 复制代码
# GUI模式
ncu-ui

# 命令行模式
ncu --set full -o profile_output ./my_cuda_app

最有用的几个功能

  1. Roofline分析:一眼看出你的kernel是算得慢还是内存慢
  2. Memory Workload分析:L1/L2 Cache命中率、带宽用了多少
  3. Compute Workload分析:指令吞吐、warp调度效率
  4. Source View:能把性能数据直接标到源代码上,哪行慢一目了然

实际案例

我用Nsight Compute分析卷积kernel,发现:

  • L1 Cache命中率只有68%,这说明该用Shared Memory了
  • Memory Replay是1.8x,这是bank conflict的表现
  • 改完之后L1命中率上升到92%,Replay降到1.05x,性能立刻上去了

七、经验总结

7.1 各种技术该什么时候用

学了这么多技术,什么场景用什么很重要。我整理了个表格:

技术 适用场景 性能收益 复杂度
CUDA Stream 大数据量处理,可分块 中-高
Constant Memory 小型只读数据,所有线程访问 低-中 极低
Texture Memory 2D/3D空间局部性访问
Shared Memory 块内数据重用
Atomic操作 并发写入,低冲突 低-中
Warp Shuffle Warp内通信

7.2 我的优化套路

经过这周的学习,总结了一套比较靠谱的优化流程:

内存优化优先级(从高到低):

  1. 先搞定访问模式

    • 保证内存访问是合并的(coalesced access)
    • 干掉bank conflict
    • 能用向量类型(float4)就用
  2. 利用数据局部性

    • 热点数据扔Shared Memory缓存起来
    • Tiling策略减少Global Memory访问
  3. 特殊内存该用就用

    • 小的只读数据 → Constant Memory
    • 2D/3D访问 → Texture Memory
  4. 异步执行

    • 多个Stream让传输和计算同时跑
    • 流水线并行

计算优化

  • Occupancy优化:线程数、寄存器、Shared Memory三者要平衡
  • 指令优化 :用内置函数(__fdividef, rsqrtf),比自己写快
  • 分支优化:尽量减少warp内的分支分化

我的优化流程

复制代码
1. 先用nvprof看看哪里慢
   ↓
2. 大概率是内存问题,先优化内存(收益最大)
   ↓
3. 再看计算部分
   ↓
4. Nsight Compute深度分析
   ↓
5. 继续迭代

说实话,内存优化的收益远大于计算优化,所以内存问题一定要先解决。

八、写在最后

8.1 这周学到了什么

Week 3的内容确实比前两周硬核多了。Stream、Constant Memory、Texture Memory这些特性之前只是听说过,这周终于真正理解并用上了。

最大的收获不是学会了几个API,而是建立起了系统性的优化思维。比如做卷积优化,一开始我只想着"用Shared Memory肯定快",但实际测下来发现Texture Memory也很香,关键是要根据具体场景选择。从12.6ms优化到3.5ms,这3.6倍的提升背后是对GPU硬件特性的深入理解。

几点体会:

  • 并发思维:Stream让我意识到GPU的潜力远不止并行计算,还能做很多overlapping的事情
  • 内存层次很重要:寄存器、Shared Memory、Constant/Texture Memory、Global Memory,每一层的性能差异都是数量级的
  • 工具必不可少:有profiler和没profiler完全是两个效率。盲目优化就是浪费时间
  • 没有银弹:每种优化技术都有适用场景,要学会权衡

8.2 下一步计划

Week 3结束,感觉CUDA的基础优化技术基本掌握了。接下来要进入更专业的领域:

Week 4-5的目标

  • Flash Attention(这个在LLM推理里太重要了)
  • Fused Kernel(把多个操作合并减少访存)
  • 量化和混合精度

路还很长,但每周都能看到自己的进步,这种感觉挺爽的。继续加油吧!


代码和实验数据我都放到了GitHub,有问题欢迎一起讨论!

觉得有帮助的话,点个赞呗 👍

有问题评论区见~

相关推荐
txinyu的博客3 小时前
std::function
服务器·开发语言·c++
学嵌入式的小杨同学4 小时前
【嵌入式 C 语言实战】交互式栈管理系统:从功能实现到用户交互全解析
c语言·开发语言·arm开发·数据结构·c++·算法·链表
txinyu的博客4 小时前
static_cast、const_cast、dynamic_cast、reinterpret_cast
linux·c++
“αβ”4 小时前
TCP相关实验
运维·服务器·网络·c++·网络协议·tcp/ip·udp
孞㐑¥5 小时前
算法—滑动窗口
开发语言·c++·经验分享·笔记·算法
一分之二~5 小时前
二叉树--求最小深度(迭代和递归)
数据结构·c++·算法·leetcode·深度优先
骥龙5 小时前
第一篇:背景篇 - 为什么医院需要自己的超算?
云计算·aigc·gpu算力
智者知已应修善业6 小时前
【输出一个N*N的01矩阵,表示最后的汉字点阵图】2024-10-22
c语言·数据结构·c++·经验分享·笔记·算法·矩阵
苏宸啊6 小时前
C++string(一)
开发语言·c++