C++内存模型 —— 现代Architecture的妥协

介绍 什么是内存模型(Memory Model)呢?这里介绍的内存模型并非C++对象的内存排布模型,而是一个非编程语言层面的概念。我们知道在C++11中,标准引入了 std::atomic<>原子对象,同时还引入了 memory_order_relaxed memory_order_consume memory_order_acquire memory_order_release memory_order_acq_rel memory_order_seq_cst 这六种 memory order。引入可以让我们进行无锁编程,而如果你想要更高性能的程序,你就必须深挖这六种内存模型的含义并正确应用。(当然,在不显式指明memory order的情况下,你能保证获得正确的代码,但存在性能损失) 内存模型 在介绍C++ memory order之前,我们先回答另一个问题。你的计算机执行的程序就是你写的程序吗? —— 显然不是的。 原因也很简单,为了更高效的执行指令,编译器、CPU结构、缓存及其他硬件系统都会对指令进行增删,修改,重排。但要回答具体进行了什么样的修改,又是一个极其复杂的问题。或者说,整个现代体系结构,就是在保证程序正确性的前提下利用各种手段对程序优化。我们可以粗略的将其分成几个部分: source code order: 程序员在源代码中指定的顺序 program code order: 基本上可以看成汇编/机器码的顺序,它可以由编译器优化后得到 execution code order: CPU执行指令顺序也不见得与汇编相同,不同CPU在执行相同机器码时任然存在优化空间。 perceived order/physical order: 最终的执行顺序。即便CPU按照某种确定指令执行,物理时间上的执行顺序仍然可能不同。例如,在超标量CPU中,一次可以fetch and decode多个指令,这些指令之间的物理执行顺序就是不确定的;由于不同层级缓存之间延时不同,以及缓存之间的通信需要等带来的不确定的执行顺序等 上图简要说明了你的源代码可能经历的优化步骤。 这些优化的一个主要原因在于 掩盖memory access操作与CPU执行速度上的巨大鸿沟。如果没有cache,CPU每个访存指令都需要stall一两百个时钟周期,这是不可接受的。但是引入cache的同时又会带来 cache coherence等问题,这也是造成x初始为0,两个线程同时执行 x++,而x最终不一定为 2的元凶。而一个内存模型则对上述并发程序的同一块内存进行了一定的限制,它给出了在并发程序下,任意一组写操作时,可能读到的值。 不同体系结构(x86, arm, power…)通过不同的内存模型来保证程序的正确性。 bonus question: 不同等级的cache latency? answer: l1: 1ns, l2: 5ns, l3: 50~100ns, main memory: 200ns Sequential Consistency(SC) SC是最严格的内存模型,也被称作non-weak memory model。在该模型下,多线程程序执行的可做如下分析:对于每一步,随机选择一个线程,并执行该线程执行中的下一步(例如,按程序或编译的顺序)。重复这个过程,直到整个程序终止。这实际上等效于按照(程序或编译的)顺序执行所有线程的所有步骤,并以某种方式交错它们,从而产生所有步骤的单一总顺序。SC不允许重新排列线程的步骤。因此,每当访问对象时,都会检索该顺序中存储在对象中的最后一个值。(注意,内存模型中说的重新排列与编译器层面无关,编译器自然是可以讲没有data dependance的读写操作进行重排的,只要保证程序的正确性即可。内存模型中的重排指的是在硬件执行阶段,由于cache hierarchy等引发的一些问题导致指令物理执行顺序被改变)。...

September 1, 2023 · 3 min

C++ CRTP

原先只是了解这个名词,想着C++20后静态多态直接用 concept来实现就好了就没细看,没必要整这些模板元编程的奇技淫巧。没想到面试某量化C++开发的时候被狠狠拷打……. CRTP (curiously recurring template pattern) 一般认为,CRTP可以用来实现静态多态 template <typename T> class Base { void func() { static_cast<T*>(this)->funcImpl(); } }; class Derived : public Base<Derived> { void funcImpl() { // do works here } }; 通过CRTP可以使得类具有类似于虚函数的效果,同时又没有虚函数调用时的开销(虚函数调用需要通过虚函数指针查找虚函数表进行调用),同时类的对象的体积相比使用虚函数也会减少(不需要存储虚函数指针),但是缺点是无法动态绑定,感觉有点过于鸡肋。 有什么用呢?可以用来向纯虚类一样做接口:(以下类似的代码在大量数学库中出现) template <typename ChildType> struct VectorBase { ChildType &underlying() { return static_cast<ChildType &>(*this); } inline ChildType &operator+=(const ChildType &rhs) { this->underlying() = this->underlying() + rhs; return this->underlying(); } }; struct Vec3f : public VectorBase<Vec3f> { float x{}, y{}, z{}; Vec3f() = default; Vec3f(float x, float y, float z) : x(x), y(y), z(z) {} }; inline Vec3f operator+(const Vec3f &lhs, const Vec3f &rhs) { Vec3f result; result....

May 20, 2023 · 2 min