学习笔记

每日学习卡片 · 按日期归档

星期二 7 张卡片

[[maybe_unused]] —— C++17 属性

C++编译器

告诉编译器「这个变量/函数/参数可能不会被使用,别报警告」。

常见场景:

  • 条件编译(debug-only 变量在 release 下没人用)
  • 断言专用变量(assert(x > 0),release 下 assert 被去掉,x 就成”未使用”)
  • 固定接口签名的回调参数(签名不能改,但本次实现用不到某个参)

cudaHostRegister —— Pinned Memory

CUDA内存

把一块已经存在的主机内存注册为页锁定(pinned)内存,锁在物理内存里不被换页,让 GPU 可以高效访问。

关键纠正:

  • 大小和显存无关,由你传入的 size 决定
  • 不是「GPU 通信的专用通道」,而是给某块具体数据贴上「快速传输」标签

三大好处:

  1. 省掉中转拷贝(默认 pageable 内存 → CUDA 需要先拷到内部 pinned buffer,再 DMA)
  2. 支持异步重叠(cudaMemcpyAsync 真正异步)
  3. 支持 zero-copy(GPU 直接读主机内存)

宏 vs 函数 —— 为什么 CUDA_CHECK 必须是宏

C++
差异函数
__FILE__ / __LINE__✅ 调用点的位置❌ 函数自己的位置
能从调用者作用域 return
类型检查
参数副作用⚠️ 可能多次求值✅ 只求值一次
调试友好度

CUDA_CHECK 必须用宏的核心理由: 要在调用现场拿到 __FILE____LINE__,函数永远拿不到调用者的位置。

现代 C++ 倾向于用 inline / constexpr / 模板 / std::source_location(C++20)替代宏,但「错误检查 + 位置打印」这个场景宏仍是最简洁的写法。

mmap —— 把文件当内存用

操作系统内存

操作系统系统调用,把文件直接映射到虚拟地址空间。

核心机制:按需加载——mmap 调用本身几乎不读数据,真正读取发生在第一次访问那个地址时(触发缺页中断,OS 把对应页搬进 page cache)。

好处:

  • 省内存(多个进程映射同一文件共享 page cache)
  • 零拷贝(绕过 read() 的 user/kernel buffer 拷贝)
  • 按需加载
  • 代码简洁(像访问数组一样访问文件)
  • 能 mmap 比物理内存还大的文件

LLM 推理里的典型用法:

mmap 模型权重 → cudaHostRegister pin 住 → cudaMemcpyAsync 喂给 GPU。

多线程的内存模型

操作系统多线程

一个进程 = 一张地址空间图,所有线程共享。

共享 🟢独立 🔴
代码段、静态数据栈(每个线程一块)
TLS(thread_local / errno
mmap 区域(含动态库)CPU 寄存器 / PC / SP
文件描述符表信号掩码
信号处理器

关键洞察:

  • 线程「栈独立」是约定,不是物理隔离——在同一张地址空间里,理论上能互相访问
  • 堆共享导致 malloc 内部必须加锁 → 高性能 allocator 用 per-thread cache
  • 多线程通信便宜(共享内存),但容易踩并发坑

进程内存布局 —— 五大区域

操作系统内存
高地址  ┌──────────────┐
        │ 5. 内核       │
        │ 4. 栈         │  ← 局部变量,自动管理
        │ 3. mmap       │  ← 文件映射、动态库、大块 malloc、线程栈
        │ 2. 堆         │  ← malloc / new
        │ 1. 静态区域    │  ← text / data / bss
        └──────────────┘
低地址

几个非显然的事实:

  • 静态区域内部还分 text(只读 + 可执行)/ data(已初始化)/ bss(未初始化),权限不同
  • 动态库 .so 是通过 mmap 加载的
  • 大块 malloc(≥128KB)底层用 mmap,不走传统堆
  • 新线程的栈也是 mmap 出来的

CUDA_CHECK 宏 —— 防御性错误检查

CUDAC++

CUDA API 错误默认不会抛异常,程序继续跑直到莫名其妙坏掉。这个宏包住每个 CUDA 调用:出错就立刻打印根因(函数名 + 文件 + 行号 + 错误描述)并 abort,让根因暴露在出错的那一行。

#define CUDA_CHECK(call) do {                                      \
  cudaError_t err = (call);                                        \
  if (err != cudaSuccess) {                                        \
    fprintf(stderr, "CUDA error at %s:%d in %s: %s\n",             \
            __FILE__, __LINE__, #call, cudaGetErrorString(err));   \
    abort();                                                       \
  }                                                                \
} while(0)

do { ... } while(0) 的作用:

  • 包多条语句成一块(适配 if (x) CUDA_CHECK(...); 这种用法)
  • 隔离作用域避免变量名冲突(err 不污染外层)