OKR(Objectives and Key Results)目标与关键成果法,是一套明确和跟踪目标及其完成情况的管理工具和方法。
编写 OKR
Objective
- 描述目标时要具体且有方向性
- 制定目标时要有挑战性
1 | 要具体且有方向性 |
Key Result
- KR 要能支持目标实现
- 定量或定性地描述 KR
1 | 定量或定性 |
OKR(Objectives and Key Results)目标与关键成果法,是一套明确和跟踪目标及其完成情况的管理工具和方法。
1 | 要具体且有方向性 |
1 | 定量或定性 |
以一次 RPC 调用为线索,细究整个过程。
作用
原理
注意
提醒编译器使用 volatile 声明的变量随时可能改变,在编译期间不进行指令重排优化。C++ 标准保证 volatile 变量之间不会重排,不保证 volatile 和非 volatile 变量的重排优化。
此外,还有硬件级别的指令重排,volatile 无法保证。
1 | const_cast<std::string &>(service); // const std::string &service; |
glibc 维护一个内存池,分配内存时优先从内存池获取,分配失败再向操作系统分配内存。
概念
Heap 是进程可用的虚拟地址空间中的动态内存区域,Arena 是 Heap 内部的内存池,Chunk 是 Arena 中的内存分配单元。
获取内存的系统调用
分配流程
1 | class SampleClass { |
std::vector
的元素类型没有默认构造函数或者是不可复制的类型std::vector
的元素类型是具有状态的类(即具有非平凡的构造函数和析构函数)std::vector
不能持有引用类型的元素示例
1 | void run { |
trivially copyable 类型包括如下:
Lock Free 是一种并发编程的概念,用于描述一种编写多线程代码的技术或算法,该技术或算法在没有使用传统的互斥锁(mutex)机制的情况下实现了线程安全。
Memory Order(内存序)是用于指定原子操作和多线程间内存可见性的概念。
memory_order_relaxed
: 最轻量级的内存序,没有任何同步或顺序要求,允许重排和乱序执行memory_order_acquire
: 获取语意memory_order_release
: 释放语意memory_order_acq_rel
: 结合了memory_order_acquire
和memory_order_release
,同时有获取和释放的语义。适用于具有获取和释放语义的原子操作。memory_order_seq_cst
: 最严格的内存序,提供全局顺序一致性。保证所有线程对原子操作的执行都具有相同的全局顺序memory_order_consume 实现成本高,使用复杂,通常被
memory_order_acquire
替代
1 | // 变量定义, 但未初始化 |
以标量为例,未初始化的变量,其值是未定义的,具体行为取决于编译器。
数据结构 | 底层实现 | 是否连续内存 | 备注 |
---|---|---|---|
vector | 数组 | 是 | |
list | 双向链表 | 否 | |
map / set | 红黑树 | 否 | 平衡树,更新后进行平衡 |
unordered_map | 哈希表 | 否 |
1 | v.at(index); |
1 | q.front(); |
1 | s.top(); |
1 | std::vector<int> v1 = {1, 3, 2, 4, 6, 5}; |
1 |
|
1 | void payload() { |
1 | struct Arg { |
1 | include "mutex" |
std::lock_guard
是一个轻量级的互斥锁封装,它提供了一种方便的方式来管理互斥锁的锁定和释放。std::unique_lock
是一个更加灵活和功能强大的互斥锁封装,提供了与 std::lock_guard
类似的锁定和释放互斥锁的功能,但还可以进行更多的操作。std::unique_lock
允许在构造函数中选择锁定模式,包括延迟锁定、递归锁定和尝试锁定等。
Tips
1 | // wait |
说明
示例
1 | using namespace std::literals; |
锁 | 说明 | 备注 |
---|---|---|
std::mutex | 互斥锁 | |
std::shared_mutex | 读写锁(共享互斥锁) | |
std::recursive_mutex | 可重入锁(递归锁) | 需要程序确保每次上锁都会释放 |
std::timed_mutex | 计时互斥锁 | |
std::recursive_timed_mutex | 计时递归锁 | 需要程序确保每次上锁都会释放 |
可以通过原子操作和循环来实现自旋锁。
1 | include <atomic> |
std::atomic
提供了一种机制,使得多线程环境下对特定类型的变量进行原子操作成为可能。通过使用 std::atomic
创建的原子类型,我们可以确保在多线程环境中读写这些变量时,不会出现数据竞争的问题。这为并发编程提供了更高的可靠性和可预测性。
对于 atomic 模板类型,要求必须是 trivially-copyable,可以通过 std::is_trivially_copyable<T>
判断。
atomic<T>::is_lock_free
is_lock_free
用来检查特定类型的原子操作是否为无锁。
1 | // CASE 1 |
1 | std::atomic<int> a; |
1 | std::atomic<int> a; |
store
exchange
compare_exchange_weak
compare_exchange_strong
load
或 ()
操作符来获取值C++ 20 引入了协程,详见 cppreference - coroutines。在此之前的标准,可以通过 makecontext()/swapcontext()
来手动管理线程的 Context 切换,实现协程。或者使用其他库的实现,比如 boost::corountines
、brpc 等。
boost::corountines
1 | char *strchr(const char *string, int c); // NULL / 第一个位置 |
在计算机编程中,同步、异步、阻塞和非阻塞是描述程序或进程之间交互方式的概念,它们的区别如下:
总结:
同步和异步关注的是程序执行的顺序和等待机制。
阻塞和非阻塞关注的是程序在等待某个操作完成时是否会被挂起。
需要注意的是,同步和异步、阻塞和非阻塞并不是互斥的概念。一个操作可以是同步阻塞的,也可以是异步非阻塞的,具体取决于程序设计和所使用的接口或协议的特性。
现代处理器的内存子系统仅限以字大小(word size)的粒度和对齐的方式访问内存,内存对齐是指数据在内存中存放时,起始位置是某个数值的倍数,这个倍数通常是计算机的字大小。
内存对齐的目的是提高计算机系统访问数据的效率,减少访问内存的次数。当访问未对齐的内存数据且数据横跨两个计算机可以读取的字时,需要访问两次内存并做额外的 shift 操作才能读取到完整的数据。
1 |
|
输出如下。
1 | 1 8 |
写屏障(Store Barrier)、读屏障(Load Barrier)和全屏障(Full Barrier)。
内存分级。
参考 GPU Performance Background User’s Guide,NVIDIA Hopper Architecture In-Depth。
每个 SM 都有自己的指令调度器和各种指令执行管道。乘加是现代神经网络中最常见的运算,充当全连接层和卷积层的构建块,这两个层都可以被视为向量点积的集合。下表显示了 NVIDIA 最新 GPU 架构上各种数据类型的单个 SM 每个时钟的乘加运算。每个乘加都包含两个运算,因此可以将表中的吞吐量乘以 2,以获得每个时钟的 FLOP 计数。要获得 GPU 的 FLOPS 速率,需要将其乘以 SM 数量和 SM 时钟速率。例如,具有 108 个 SM 和 1.41 GHz 时钟频率的 A100 GPU 的峰值密集吞吐量为 156 TF32 TFLOPS 和 312 FP16 TFLOPS(应用程序实现的吞吐量取决于本文档中讨论的许多因素)。
1 | __global__ void function_name(...) { |
__global__
限定词void
cudaDeviceSynchronize()
来做同步核(Kernel)是执行在 GPU 上的函数。应用程序的并行部分由K个不同的CUDA线程并行执行K次,而不是像常规的C/C++函数那样只执行一次。
每个CUDA内核都有一个__global__
声明说明符。程序员通过使用内置变量为每个线程提供一个唯一的全局ID。
一组线程称为CUDA块(CUDA Block)。CUDA块被分组到一个网格(Grid)中。内核(Kernel)作为线程块网格(A Grid of Blocks of Threads)执行。
每个 CUDA 块被一个流式多处理器(Streaming Multiprocessor,SM)执行,不能被迁移到其他 SMs 处理(抢占、调试、CUDA动态并行期除外)。一个SM可以运行多个并发CUDA块,具体取决于CUDA块所需的资源。每个内核在一个设备上执行,CUDA支持一次在一个设备上运行多个内核。
上图展示了内核执行和GPU中可用硬件资源的映射。
限制。
__syncthreads
进行同步。使用 __syncthreads
块中的所有线程都必须等待<<<…>>>
语法中指定的每个块的线程数和每个网格的块数可以是int或dim3类型。这些三尖括号标记从主机代码到设备代码的调用。它也称为 Kernel Launch示例。
1 | // Kernel - Adding two matrices MatA and MatB |
CUDA为线程和块定义了内置的3D变量。线程使用内置的3D变量threadIdx进行索引。三维索引提供了一种自然的方式来索引向量、矩阵和体积中的元素,并使CUDA编程更容易。类似地,块也使用称为block Idx的内置3D变量进行索引。示例的 CUDA 程序用于两个矩阵相加,显示了多维 blockIdx 和 threadIdx 以及其他变量(如 blockDim)。选择 2D 块是为了便于索引,每个块有 256 个线程,其中 x 和 y 方向各有 16 个线程。使用数据大小除以每个块的大小来计算块的总数。
核函数调用需要指定线程模型。
1 | kernel_function<<<grid, block>>>(); |
重要概念
grid,网格
block,线程块
配置线程 <<<grid_size, block_size>>>
最大允许线程块大小 1024
最大允许网格大小 2^32 - 1
每个线程在核函数中都有一个唯一标识
内建变量(build-in variable)
<<<grid_size, block_size>>>
确定,grid_size、block_size 保存在内建变量(build-in variable)
[0, gridDim.x)
[0, blockDim.x)
多维线程
[1, 2^31)
[1, 2^16)
[1, 2^16)
1 | dim3 grid_size(g_x, g_y, g_z); |
__host__
)代码__global__
)代码-arch=compute_XY
选项,指定虚拟架构的计算能力,用于确定代码中能够使用的 CUDA 功能-code=sm_ZW
选项,指定一个真实架构的计算能力,用以确定可执行文件能够使用的 GPU参考文档。
cudaGetDeviceCount
可以获取 GPU 设备数量cudaSetDevice
来设置使用的设备基于 <<<grid, block>>>
的 cuda 程序内存模型。内存可见性及生命周期如下。
cudaMalloc
,对应标准库 malloc
cudaMemcpy
,对应标准库 memcpy
memset
,对应标准库 cudaMemset
cudaFree
,对应标准库 free
__device__
修饰)
__global__
修饰)
__global__
修饰符不能和 __host__
、__device__
同时用__host__
修饰或无修饰)
说明
__host__
和 __device__
同时修饰一个函数来减少冗余,编译器会针对主机和设备分别编译CUDA (Compute Unified Device Architecture)是英伟达开发的并行计算平台,为使用 GPU 加速的程序提供开发环境(当前仅针对英伟达设备)。
CUDA 提供两层 API,驱动(Driver) API 及运行时(Runtime) API。Runtime API 是基于 Driver API 编写的、使用更为简便的 API。
英伟达每款 GPU 会有一个叫做 Compute Capability 的版本号,用于标识设备的计算平台兼容性。其直译 计算能力 略有歧义,并非代表设备的计算性能。