Random Notes on Coding

Some C++

C++ Basics

inline 关键字

  • 允许重复定义 (如果你要在头文件里定义 (而不是声明) 函数最好加上 inline):

    add.cpp
    inline int add(int a, int b) {return a + b;}
  • 避免函数调用开销, 如果 add()Listing lst-addcpp 来定义, 则:

    int x = add(3, 5);

    会被编译器展开成:

    int x = 3 + 5;

    (当然这个函数由于过于简单, 如果不用 inline, 编译器也会自动优化掉函数调用开销).

命令行参数

  • argc: Argument Count, 参数个数
  • argv: Argument Vector, 参数列表 (字符串数组)
int main(int argc, char* argv[]) {
    return 0;
}

编译运行:

g++ main.cpp -o main
./main abc 999 test

会有:

argc = 4
argv[0] = "./main"
argv[1] = "abc"
argv[2] = "999"
argv[3] = "test"

Memory Management 内存管理

new 关键字

new 用来在堆上分配内存:

int* p = new int[10]; // Allocate 10 integers on heap
for (int i = 0; i < 10; i++) {
    p[i] = i * i; // Assign values
}
delete[] p; // Don't forget to free the memory!

分配单个对象. 注意下面两行代码的唯一区别是 malloc 没有调用 Constructor!

MyClass* obj = new MyClass(); // Allocate single object
MyClass* obj2 = (MyClass*)malloc(sizeof(MyClass)); // C-style allocation
delete obj; // Free the memory!
free(obj2); // Free C-style allocated memory

Smart Pointers 智能指针

Smart Pointers = new without delete. 即不需要担心内存泄漏问题.

  • std::unique_ptr<T>: 不能被复制:

    std::unique_ptr<int> p1 = std::make_unique<int>(42); // Create unique_ptr
    // std::unique_ptr<int> p2 = p1; // Error: cannot copy unique_ptr
  • std::shared_ptr<T>: 可以被复制 (用过引用计数来管理内存).

  • std::weak_ptr<T>: 不会增加引用计数, 用于解决 shared_ptr 循环引用问题 (Figure fig-cirref, a1 表示 a 的引用计数为 1 (a.use_count() == 1)):

Figure 1: 循环引用问题
sharedptr.cpp
#include <memory>
#include <iostream>

class B; // Let A know B

class A {
public:
    std::shared_ptr<B> bptr;
    ~A() { std::cout << "A destroyed\n"; }
};

class B {
public:
    std::shared_ptr<A> aptr;
    ~B() { std::cout << "B destroyed\n"; }
};

int main() {
    auto a = std::make_shared<A>(); // a1
    auto b = std::make_shared<B>(); // b1

    a->bptr = b; // b2
    b->aptr = a; // a2

    // No destroy messages!!! (Memory leak)
} // b1 a1 (not 0!)
weakptr.cpp
#include <memory>
#include <iostream>

class B; // Let A know B

class A {
public:
    std::weak_ptr<B> bptr;
    ~A() { std::cout << "A destroyed\n"; }
};

class B {
public:
    std::weak_ptr<A> aptr;
    ~B() { std::cout << "B destroyed\n"; }
};

int main() {
    auto a = std::make_shared<A>(); // a1
    auto b = std::make_shared<B>(); // b1

    a->bptr = b; // b1 still
    b->aptr = a; // a1 still

    // Destroy properly
} // a0 b0

Class 类

Constructor, Init List 构造函数与初始化列表

  • Constructor: 相当于 Python 里的 __init__ 方法. 每次创建对象时会被调用. 可以有多个 Constructor (根据是否带参数区分). 名字是 ClassName().
    • Destructor: 相当于 Python 里的 __del__ 方法. 每次对象被销毁时会被调用. 名字是 ~ClassName().
  • Constructor member initializer list: 用于在 Constructor 体执行前初始化成员变量 (用 : 引出). 那跟放在 Constructor 体里初始化有什么区别呢? 见下面例子:
constructor.cpp
#include <iostream>
using namespace std;

class Small 
{
public:
    Small()
    {
        cout << 1 << endl;
    }

    Small(int in)
    {
        cout << 2 << endl;
    }
};

class Big
{
private:
    Small small;
public:
    Big(int in)
    {
        small = Small(in);
        cout << 3 << endl; 
    }
};

int main()
{
    Big big(7); // Output: 1 2 3
    return 0;
}
constructor_init.cpp
#include <iostream>
using namespace std;

class Small 
{
public:
    Small()
    {
        cout << 1 << endl;
    }

    Small(int in)
    {
        cout << 2 << endl;
    }
};

class Big
{
private:
    Small small;
public:
    Big(int in) 
        : small(in)
    {
        cout << 3 << endl; 
    }
};

int main()
{
    Big big(7); // Output: 2 3
    return 0;
}

this 指针

就是 Python 里的 self (当前对象的地址):

class A {
private:
    int x;

public:
    void setX(int x) {
        this->x = x;   // Left: member variable; Right: parameter
    }
};

类继承

class A : public B

上面代码表示让 class A 继承 class B. public 表示公开继承, 即 Bpublicprotected 成员在 A 里依然是 publicprotected1.

1 protectedprivate 区别: 前者代表本类和子类可以访问, 后者只能本类访问 (子类不行).

friend 关键字

在一个类里面声明另一个类 (或函数) 为 friend, 表示同意它访问自己的 privateprotected 成员 (注意是谁访问谁的私有成员!):

friend.cpp
#include <iostream>

class B;  // Tell compiler that B exists

class A {
private:
    int secret = 7;

    friend class B; // B is an intimate friend of A
    // friend B;    // Alternative
};

class B {
public:
    void reveal(A& a) {
        std::cout << a.secret << std::endl;
    }
};

int main() {
    A a;
    B b;
    b.reveal(a); // Outputs: 7
    return 0;
}

Alias 别名

Reference 引用

  • Reference 仅仅是 Syntax sugar!
  • 必须初始化. int& ref; 是错误的.
  • int& ref = a; 在编译时不会出现 ref 这个变量, 它只是 a 的别名 (alias).
change_val_ref.cpp
#include <iostream>
int main()
{
    int a = 5;
    int& ref = a; // ref is an alias to a
    ref = 10;     // Actually modifies a
    std::cout << "a = " << a << std::endl; // Outputs: a = 10
    return 0;
}
increment_val_ref.cpp
#include <iostream>
void increment(int& num) {num++;}
int main()
{
    int value = 5;
    increment(value); // Pass by reference
    std::cout << "value = " << value << std::endl; // Outputs: value = 6
    return 0;
}

typedefusing 关键字

下面两个都是给 std::vector<int> 起别名 IntList, 效果一样:

using IntList = std::vector<int>;
typedef std::vector<int> IntList;

Overloading 重载

  • 重载使得 +, * 这些运算符可以用于自定义 class 之间的运算 (而不仅限于 int, float 等).
  • Python 里面也有 __add__ 这种魔法方法实现重载.
  • 重载是不好的编程习惯! 但在写 API 的时候要提供重载和非重载两种接口供开发者使用 (比如下面的 Add()operator+() 方法), 定义的时候可以用重载定义非重载, 也可以反过来:
overload_op_calls_method.cpp
class Point
{
public:
    double x, y;

    Point(double x_coor, double y_coor)
        : x(x_coor), y(y_coor) {}

    Point Add(const Point& other) const
    {
        return Point(x + other.x, y + other.y);
    }

    Point operator+(const Point& other) const
    {
        return Add(other);
    }
};
overload_method_calls_op.cpp
class Point
{
public:
    double x, y;

    Point(double x_coor, double y_coor)
        : x(x_coor), y(y_coor) {}

    Point Add(const Point& other) const
    {
        return *this + other;
    }

    Point operator+(const Point& other) const
    {
        return Point(x + other.x, y + other.y);
    }
};

这样就可以直接这样来将两个 Point 对象相加:

overload_main.cpp
#include <iostream>
#include "overload_op_calls_method.cpp"

int main()
{
    Point p1(1.0, 2.0);
    Point p2(3.0, 4.0);

    Point p3 = p1 + p2;  // i.e., p3 = p1.operator+(p2);
    std::cout << "p3: (" << p3.x << ", " << p3.y << ")\n"; // p3: (4, 6)

    return 0;
}

Namespace 命名空间

  • 我们希望在不同的场景中给功能类似的函数起完全相同的名字 (比如下面例子的 print() 函数), 为了避免命名冲突, 我们可以使用 namespace:
namespace.cpp
#include <iostream>

namespace AppleSpace { namespace GoodApple {
    void print(const char* msg)
    {
        std::cout << "Goodapple " << msg << std::endl;
    }
}   namespace BadApple {
    void print(const char* msg)
    {
        std::cout << "Badapple " << msg << std::endl;
    }
} }

namespace OrangeSpace {
    void print(const char* msg)
    {
        std::cout << "Orange " << msg << std::endl;
    }
}

int main() {
    AppleSpace::GoodApple::print("Hello");
    AppleSpace::BadApple::print("Hello");
    OrangeSpace::print("Hello");
    return 0;
}

输出:

Goodapple Hello
Badapple Hello
Orange Hello
  • 不要到处拉 using namespace xxx; 的 shit!

  • 全局作用域运算符:

globalnamespace.cpp
#include <iostream>

int value = 10;  // Global namespace value

namespace A {
    int value = 20;  // A::value

    void print() {
        std::cout << value << std::endl;      // Output 20
        std::cout << ::value << std::endl;    // Output 10
    }
}

int main() {
    A::print();
}
  • :: 可用于调用 namespace 里的函数 或 class 里的 static 变量或方法. 在外部定义类的方法实现时必须用 :: (调用时用 .).

常见库用法

std::copy()

#include <vector>
#include <algorithm>  // std::copy

int main() {
    std::vector<int> src = {1, 2, 3, 4, 5};
    std::vector<int> dst(5);   // Must arrange space in advance

    std::copy(src.begin(), src.end(), dst.begin()); // Copy src to dst
}

Pybind11

Pybind11 是一个 C++ 库, 使得我们可以在 Python 代码中调用 C++ 函数和类. 比如:

add_op.cpp
#include <pybind11/pybind11.h>

int add(int a, int b) {
    return a + b;
}

/// @param ops library name
PYBIND11_MODULE(ops, m) {
    m.def("add", &add);
}
main.py
import ops

print(ops.add(3, 5))

运行类似下面的命令:

c++ -O3 -Wall -shared -std=c++11 -fPIC -undefined dynamic_lookup \
    -I/opt/homebrew/anaconda3/lib/python3.12/site-packages/pybind11/include \
    -I/opt/homebrew/anaconda3/include/python3.12 \
    add_op.cpp -o ops.cpython-312-darwin.so

python main.py # output 8

第一个命令会在当前目录下产生一个 .so (shared object) 文件. 运行 python main.py 的时候在 import ops 的时候, Python 会在 .so 文件中找到 add 函数.

CMake

CMakeLists

Makefile

  • 格式:

    target: prerequisites
        recipe
  • 缺省规则

    .DEFAULT_GOAL := all
    all: ...
  • 伪规则

    .PHONY: all clean
  • Append:

    CFLAGS += -Wall -O2
  • 改后缀名:

    SRCS_ASM = start.S
    OBJS = $(SRCS_ASM:.S=.o)
  • 冒号前面和后面:

    %.o : %.c
        $(CC) $(CFLAGS) -c $< -o $@
    • % 为通配符, 意思是每当你需要一个 .o 文件, 并且当前目录下有对应的 .c 文件时, 就用下面的命令来生成它
    • $<: 第一个依赖文件
    • $@: 目标文件
    • $^: 所有依赖文件
    • $?: 所有比目标文件新的依赖文件

Coding Techniques

正负无穷

涉及最小/最大值初始化. 例如我们要找 arr 中的最大值:

#include <float.h>   // for FLT_MAX

int main() {
    float arr[] = {3.2, -1.5, 7.8, 0.0, 4.4};
    int n = sizeof(arr) / sizeof(arr[0]);
    float max_val = -FLT_MAX;    // Initialize to -inf
    for (int i = 0; i < n; i++) {
        if (arr[i] > max_val)   max_val = arr[i];
    }
    std::cout << "Max value: " << max_val << std::endl;
    return 0;
}

向上取整转为向下取整

可通过 \[\left\lceil \frac{x}{y} \right\rceil = \left\lfloor \frac{x + y - 1}{y} \right\rfloor\] 实现 (由于整数除法默认向下取整):

int ceil_div(int x, int y) {
    return (x + y - 1) / y; // Automatically does floor division
}

链式调用

返回 *this 的引用可以实现链式调用:

chaining.cpp
#include <iostream>

class Point {
private:
    int x_ = 0; int y_ = 0;

public:
    Point& setX(int x) {
        x_ = x;
        return *this; 
    }

    Point& setY(int y) {
        y_ = y;
        return *this;
    }
};

int main() {
    Point p;
    // Set values by chaining
    p.setX(10).setY(20);
}

Coding Habits

代码风格

  • 能引用尽量引用不要用指针!
  • 函数的参数尽量少.

getters 和 Setters

为了让外部代码访问和修改类的成员变量, 一种办法是将成员变量声明为 public, 但一旦外部代码写入了不合法的值, 就会导致类的状态变得不可预测. 因此我们用 private + getter/setter 的方式来控制对成员变量的访问和修改.

account_public.cpp
class Account {
public:
    double balance;
};

int main() {
    Account acc;
    acc.balance = -999999; // Not recommended
}
account_private.cpp
class Account {
private:
    double balance;

public:
    // const: no modification to member variables
    double getBalance() const { return balance; }

    void deposit(double amount) {
        if (amount > 0)
            balance += amount;
    }
};

常写 const

  • 能加上 const 尽量加上 (比如 Listing lst-account_private).

  • const 用法:

    const int *p1; // p1 本身可以修改, 但指向的内容不可以修改
    int * const p2; // p2 本身不可以修改, 但指向的内容可以修改
    const int * const p3; // 都不能修改
    
    int const& my_int; // 引用的内容不可以修改, 同 const int&
    // int &const // 不合法
  • Pytorch C++ API 中提供 TORCH_ARG 宏, 用于自动生成类成员变量的 getter 和 setter 方法:

    #include <torch/torch.h>
    TORCH_ARG(double, balance) = 0;

    自动展开为:

    private:
        double balance_ = 0;
    
    public:
        // getter
        const double& balance() const { return balance_; }
    
        // setter
        double& balance() { return balance_; }

    这种写法类似 Listing lst-account_private, const double& 意思是返回一个不可修改的引用 (若用值拷贝的话速度较慢). 第二个 const 表示 getter 方法本身不会修改这个类的所有成员变量 (balance_ in this case). 用户可以通过 obj.balance() 来获取余额, 也可以通过 obj.balance() = 100.0; 来修改余额.

Tips on Code Reading

First Encounter

  • Take it Easy: 一个 C++ project 不过是一堆 class 和一个 main.

  • 如果是 OOP, 关注类会改变哪些外部变量 (通常是引用传递) 而不是某个方法的具体实现.

Verilog

Change of Mind: Hardware does not “execute” the lines of code in sequence.

  • assign

    • 多个 assign 执行没有顺序, 同时进行.
    • assign 是 “continuous assignment”, 右值变化时, 左值跟着变化.
  • Operation 运算符: ~, ! (logical), &, && (logical), |, || (logical), ^ (XOR).

  • if, else if 是有顺序的!!!

  • (procedure 一定要放在 always 块中吗?)

  • (为什么 wire 类型不能在 always 里面被赋值?)

  • always 块中的代码是顺序执行的 (但在 always 块外的代码是并行执行的).

    module top (input my_in, output reg my_out);
        always @(*) begin
            my_out = 0;
            my_out = 1; // This is valid! (Always block 按顺序执行)
        end
    endmodule
    • Latch 推断: 下面如果 cpu_overheated = 0 则默认会让 shut_off_computer 保持上一个值, 这就是 latch 推断.

      always @(*) begin
          if (cpu_overheated)
              shut_off_computer = 1;
      end

      有时我们就是需要这种推断, 但为了避免, 可以利用always 的顺序性先提前赋值:

      always @(*) begin
          shut_off_computer = 0; // 先提前赋值
          if (cpu_overheated)
              shut_off_computer = 1;
      end
  • Concatenation:

    assign out = {tmp, {3{3'b100}}}; // Concatenation, out = 0000011 100 100 100
  • index 可以是负数:

    reg [5:-1] my_reg; // index 可以是负数.
    wire [0:3] my_wire; // Big-endian, mywire[0] is MSB, use my_wire[3:0] later is illegal!
  • input a 默认为 wire.

  • begin end 在只有一行代码时可以省略 (相当于 C 中的 {}).

  • wire 不能在 always 块中被赋值. reg 才能在 always 中被赋值.

    wire a;
    always @(*) begin
        assign a = 1; // Error!
    end
    wire a;
    always @(*) begin
        a <= 1; // Not an error, `a` is viewed as a reg.
    end
  • Synchronous and Asynchronous Reset:

    • synchronous reset:

      always @(posedge clk) begin
          if (reset)
              ...
      end
    • asynchronous reset:

      always @(posedge clk or posedge reset) begin
          if (reset)
              ...
      end
  • or 只能always 块中使用, if () 中要用 ||.

  • InferenceInstantiation:

    • Inference: 通过 always 块的内容推断出一个模块的功能.
    • Instantiation: 显式地实例化一个模块, 通过 module_name instance_name (port_map) 的方式.
  • 循环群结构 (Torus, etc) 如果用 % 运算符来处理会消耗大量资源, 尽量用 if 语句:

    if (mm == 8'd59) begin
        mm <= 8'd0;
    end
    • 如果用 % 运算符来处理:
      • -1 % 16 的结果是 -1, 而不是 15 (所以 (a-1)%16 应该写成 (a+15)%16).
      • 1~12 的循环先转换为 0~11 的循环, 再换元.
  • BCD (Binary-Coded Decimal): 一种从 0 到 9 的计数器, 输出是四位二进制编码的十进制数.

  • Blocking 和 Non-blocking assignments:

    • =: Blocking assignment, 若在 always 块中使用, 则必须按照顺序执行!
    • <=: Non-blocking assignment, 在 always 块中使用时, 会同时执行所有赋值. (一般 always 里面都用这个!)

Verilog Testbench

`timescale 1ns / 1ps // #1 代表 1ns, 最精确可以到 #1.001
`timescale 1ns / 1ns // #1 代表 1ns, #1.01 等是不合法的

$stop //停下来

Chisel

  • +&: 如果 io.in_aio.in_b4.W 时, 则 sum5.W (带溢出).

    val sum = io.in_a +& io.in_b
  • 允许多个 := 赋值到同一个输出:

    io.out := 0.U
    io.out := 1.U // 覆盖上一个
  • ifwhen 的区别:

    • if 用来在编译阶段决定电路是哪一种
    • when 用来生成固定的 verilog 电路, 相当于 verilog 的 if.
  • orR: reduction OR, 对所有位进行 OR 操作. 比如 GPR 读寄存器时, 只要地址不是全 0,就读出寄存器的值 (即创建了一个虚拟的 x0 寄存器,值恒为 0):

    io.rdata1 := Mux(io.raddr1.orR, regs(io.raddr1), 0.U)
    io.rdata2 := Mux(io.raddr2.orR, regs(io.raddr2), 0.U)

Area Optimization 面积优化方法

有时可读性强的代码会导致使用的逻辑门更多, 占用更多面积. 所以一个模块有时会写两种版本, 一种是可读性强的, 另一种是面积优化的版本.

CSE 公共子表达式消除

Common Subexpression Elimination 也是编译器优化的一种. 举个简单的例子:

int x = a + b;
int y = a + b;

可以优化为:

int temp = a + b;
int x = temp;
int y = temp;

这样就避免了重复计算 a + b 两次.

在 RTL 设计中, 比如 Listing lst-AddSubSimple 这个简单的加减法模块, 注意到 “减法” 其实等价于 “加上 B 的补码”, 可以得到更优化的版本 Listing lst-AddSubArea:

AddSubSimple.scala
class AddSubSimple extends Module {
  val io = IO(new Bundle {
    val a = Input(UInt(8.W))
    val b = Input(UInt(8.W))
    val sub = Input(Bool())   // true 表示减法
    val out = Output(UInt(8.W))
  })

  // 两个独立运算单元
  val addRes = io.a + io.b
  val subRes = io.a - io.b

  io.out := Mux(io.sub, subRes, addRes)
}
AddSubArea.scala
class AddSubOptimized extends Module {
  val io = IO(new Bundle {
    val a = Input(UInt(8.W))
    val b = Input(UInt(8.W))
    val sub = Input(Bool())
    val out = Output(UInt(8.W))
  })

  // 如果是减法,就取 -B;否则取 B
  val b_eff = Mux(io.sub, -io.b, io.b)
  io.out := io.a + b_eff
}

对比他们两个 sbt 出来的 verilog 代码:

AddSubSimple.v
module AddSubSimple(
  input        clock,
  input        reset,
  input  [7:0] io_a,
  input  [7:0] io_b,
  input        io_sub,
  output [7:0] io_out
);
  wire [7:0] addRes = io_a + io_b;
  wire [7:0] subRes = io_a - io_b;
  assign io_out = io_sub ? subRes : addRes;
endmodule
AddSubArea.v
module AddSubArea(
  input        clock,
  input        reset,
  input  [7:0] io_a,
  input  [7:0] io_b,
  input        io_sub,
  output [7:0] io_out
);
  wire [7:0] _b_eff_T_1 = 8'h0 - io_b;
  wire [7:0] b_eff = io_sub ? _b_eff_T_1 : io_b;
  assign io_out = io_a + b_eff;
endmodule