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 memorySmart 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_ptrstd::shared_ptr<T>: 可以被复制 (用过引用计数来管理内存).std::weak_ptr<T>: 不会增加引用计数, 用于解决shared_ptr循环引用问题 (Figure fig-cirref,a1表示a的引用计数为 1 (a.use_count() == 1)):
sharedptr.cpp
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().
- Destructor: 相当于
- 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 表示公开继承, 即 B 的 public 和 protected 成员在 A 里依然是 public 和 protected1.
1 protected 和 private 区别: 前者代表本类和子类可以访问, 后者只能本类访问 (子类不行).
friend 关键字
在一个类里面声明另一个类 (或函数) 为 friend, 表示同意它访问自己的 private 和 protected 成员 (注意是谁访问谁的私有成员!):
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;
}
typedef 和 using 关键字
下面两个都是给 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 cleanAppend:
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 endmoduleLatch 推断: 下面如果
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 100index 可以是负数:
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! endwire a; always @(*) begin a <= 1; // Not an error, `a` is viewed as a reg. endSynchronous and Asynchronous Reset:
synchronous reset:
always @(posedge clk) begin if (reset) ... endasynchronous reset:
always @(posedge clk or posedge reset) begin if (reset) ... end
or只能在always块中使用,if ()中要用||.Inference 和 Instantiation:
- Inference: 通过
always块的内容推断出一个模块的功能. - Instantiation: 显式地实例化一个模块, 通过
module_name instance_name (port_map)的方式.
- Inference: 通过
循环群结构 (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_a和io.in_b为4.W时, 则sum为5.W(带溢出).val sum = io.in_a +& io.in_b允许多个
:=赋值到同一个输出:io.out := 0.U io.out := 1.U // 覆盖上一个if和when的区别: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