罗列的博客

这是一个罗列发呆的地方

0%

主要是在使用 SystemC 的时候遇到的,一般 C++ 的文件组织形式都是 h 文件放在 include/ 下,但是在使用 SystemC 时,每个 .h.cpp 都是成套出现的,这里就想要按照每个 module 一起的组织的方式。

即想组织成如下结构

1
2
3
4
5
6
7
8
9
10
11
.
├── main.cpp
├── CMakeLists.txt
├── Module1
│   ├── module1.h
│   ├── module1.cpp
│   └── CMakeLists.txt
└── Module2
   ├── module2.h
   ├── module2.cpp
   └── CMakeLists.txt

下面开始吧

文件组织

1
2
3
4
5
6
7
8
9
10
11
.
├── main.cpp
├── CMakeLists.txt
├── hello
│   ├── CMakeLists.txt
│   ├── hello.cpp
│   └── hello.h
└── world
├── CMakeLists.txt
├── world.cpp
└── world.h

顶层目录

1
2
3
4
5
6
7
8
9
10
// main.cpp
#include "hello.h"
#include "world.h"
#include <stdio.h>

int main() {
hello();
world();
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# CMakeLists.txt

cmake_minimum_required(VERSION 2.8)
project(helloworld)

# Add the source in project root directory
aux_source_directory(. DIRSRCS)

# Add header file include directories
include_directories(
./
./hello
./world
)

# Add block directories
add_subdirectory(hello)
add_subdirectory(world)

# Target
add_executable(helloworld ${DIRSRCS})
target_link_libraries(helloworld hello world)

hello/

1
2
3
4
5
6
7
8
// hello.h
#ifndef HELLO_H
#define HELLO_H

#include <stdio.h>
void hello();

#endif
1
2
3
// hello.cpp
#include "hello.h"
void hello() { printf("hello\n"); }
1
2
3
4
# hello/CMakeLists.txt

aux_source_directory(. DIR_HELLO_SRCS)
add_library(hello ${DIR_HELLO_SRCS})

world/

1
2
3
4
5
6
7
8
// world.h
#ifndef WORLD_H
#define WORLD_H

#include <stdio.h>
void world();

#endif
1
2
3
// world.cpp
#include "world.h"
void world() { printf("world\n"); }
1
2
3
4
# world/CMakeLists.txt

aux_source_directory(. DIR_WORLD_SRCS)
add_library(world ${DIR_WORLD_SRCS})

原理

其实主要看其中的三个 CMakeLists.txt 的文件就可以了

在子模块中的 CMake 就是把当前目录下所有文件写入对应名字的library中,如helloworld

然后顶层目录文件需要做三件事

  1. include_directories:将 #include 的目录放进来:
  2. add_subdirectory:将刚才写好的library放进来(找到对应目录下的 sub-library)
  3. target_link_libraries:将刚才的library和主文件链接上

即可

systemC 中变量分三种

  • 变量(variable):普通 C 变量
  • 信号(signal):组件内连接(sc_signal
  • 端口(port):I/O 口(sc_in / sc_out

注意这里也有“变量”,所以在 systemC 中一般把常规意义上的“变量”称呼为“值保持器”

1
2
3
4
5
6
7
8
9
10
11
// 1. 变量
// type v1, v2;
int v1;

// 2. 信号
// sc_signal<type> s1, s2;
sc_signal<bool> s1;

// 3. 端口
// sc_in<type>, sc_out<type>, sc_inout<type>
sc_in<bool>

注意这里信号和端口都是用来描述硬件结构的,所有想要对于“值保持器”的操作都应该传递给“变量”后再操作,具体见下节。

所有的值保持器都可以作为 C 语法中的“变量”,使用数组、指针等。

在 SC 中,特色的可作为左值和右值的有五种:

  • 变量
  • 信号
  • 端口
  • 位选取结果 []
  • 位区间选取结果 .range()

位选取[]和位区间选取.range()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sc_signal<sc_bv<4>> dval;
sc_in<sc_bv<8>> addr;

sc_bv<4> var_dval;
sc_bv<8> var_addr;
bool ready;

// ...

// for sc_in port, need to use read()
var_addr = addr.read();
// do something, eg:
ready = var_addr[2];

var_dval = dval;
var_dval.range(2, 0) = "011";
dval = var_dval;

还有一种比较有特色的分配方式,只有当是位向量 vector 类型时(如 sc_bv 或者 sc_lv 等)

1
2
3
4
sc_bv<8> ctrl_bus;
sc_bv<4> mult;

mult = (ctrl_bus[0], ctrl_bus[2], ctrl_bus[0], ctrl_but[2])

位 / 逻辑类型

此类型只能做逻辑运算与或非,不能做算数运算

位类型 ('1' / '0')

1
2
3
4
// 1 bit
bool v1;
// vector
sc_bv<4> v2;

逻辑类型 ('1' / '0' / 'X' / 'Z')

1
2
3
4
sc_logic_0 // '0'
sc_logic_1 // '1'
sc_logic_X // 'X'
sc_logic_Z // 'Z'
1
2
3
4
// 1 bit
sc_logic v1;
// vector
sc_lv<8> v2;

有/无符号整形

一个读入文件的方法,这里调用了iostream 中的 getline 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <fstream>
#include <iostream>

using namespace std;

int main() {
char buff[100];
ifstream infile("t.txt");
cout << "reading from t.txt" << endl;

cout << "the data is: " << endl;
while (!infile.eof()) {
infile.getline(buff, 100);
cout << buff << endl;
}
cout << "END!!" << endl;
infile.close();
return 0;
}

CMake 语法

CMake 语句主要有 3 类用法:

  • 设置变量:

    • set
    • file
    • list
    • find_library
    • aux_source_directory
    • $<...>: generator expressions
  • 设置target: 构建的目标(一般来说就是库或者可执行文件)

    • add_library
    • add_executable
  • 设置target的属性: 定义如何生成 target(源文件的路径、编译选项、要链接的库…)

    • add_definitions
    • target_link_libraries
    • link_directories
    • include_directories
    • target_include_directories

预处理

  1. project

设置项目的名字

1
2
3
4
5
6
7
project(SYSZUXrtp)

# or more parm
project(
libdeepvac
LANGUAGES CXX
VERSION "1.0.0")

设置变量

  1. set(var content)

其中 content 可以有空格换行等,第一个空格前是变量名

1
2
3
4
5
6
7
set(SYSZUX_HEADERS
include/detail/class.h
include/detail/common.h
include/detail/descr.h
include/detail/init.h
include/internals.h
include/detail/typeid.h)
  1. file

使用正则匹配文件,并将文件路径赋值给第一个参数(为变量)

  1. list

针对list进行各种操作,如增删改查

  1. find_library

寻找一个库,将找到的库的绝对路径赋值给变量。如

1
find_library(LIBGEMFIELD_PATH libgemfield.so PATHS ${CUDA_TOOLKIT_ROOT_DIR}/lib64/)
  1. aux_source_directory(<dir> <var>)

找到dir下所有的源文件赋值给var,如

1
aux_source_directory(${gemfield_root}/include gemfield_src)
  1. $<...>: generator expressions

生成表达式,暂略

设置target

  1. add_library
1
2
3
4
5
6
add_library(<name> [STATIC | SHARED | MODULE]
[EXCLUDE_FROM_ALL]
source1 [source2 ...])

# eg
add_library(gemfield_static STATIC ${gemfield_src_list})

将一系列库整合命名为<name>

  1. add_executable
1
2
3
4
5
6
add_executable(<name> [WIN32] [MACOSX_BUNDLE]
[EXCLUDE_FROM_ALL]
source1 [source2 ...])

# eg
add_executable(gemfield_proxy ${gemfield_src_list})

将源文件生成可执行文件<name>

设置target属性

  1. add_definitions

定义一些属性

1
add_definitions(-DENABLE_GEMFIELD)
  1. target_link_libraries

链接

1
2
3
4
5
6
7
8
9
10
target_link_libraries(<target> [item1 [item2 [...]]]
[[debug|optimized|general] <item>] ...)

# eg
target_link_libraries(
gemfield_proxy
shared_static
json_static
mpeg_static
${LINK_LIB_LIST})

链接gemfield_proxy的时候需要有后面的库

CMake 控制

if-else

1
2
3
4
5
6
7
if (xx AND aa)
# something
elseif(NOT yy)
# something
else(zz)
# something
endif()

for

1
2
3
foreach(i list_i)
# something
endforeach()

内置变量

系统变量

  • MAKE_MAJOR_VERSION : major version number for CMake, e.g. the “2” in CMake 2.4.3
  • CMAKE_MINOR_VERSION : minor version number for CMake, e.g. the “4” in CMake 2.4.3
  • CMAKE_PATCH_VERSION : patch version number for CMake, e.g. the “3” in CMake 2.4.3
  • CMAKE_TWEAK_VERSION : tweak version number for CMake, e.g. the “1” in CMake X.X.X.1. Releases use tweak < 20000000 and development versions use the date format CCYYMMDD for the tweak level.
  • CMAKE_VERSION : The version number combined, eg. 2.8.4.20110222-ged5ba for a Nightly build. or 2.8.4 for a Release build.
  • CMAKE_GENERATOR : the generator specified on the commandline.
  • BORLAND : is TRUE on Windows when using a Borland compiler
  • WATCOM : is TRUE on Windows when using the Open Watcom compiler
  • MSVC, MSVC_IDE, MSVC60, MSVC70, MSVC71, MSVC80, CMAKE_COMPILER_2005, MSVC90, MSVC10 (Visual Studio 2010) : Microsoft compiler
  • CMAKE_C_COMPILER_ID : one of “Clang”, “GNU”, “Intel”, or “MSVC”. This works even if a compiler wrapper like ccache is used.
  • CMAKE_CXX_COMPILER_ID : one of “Clang”, “GNU”, “Intel”, or “MSVC”. This works even if a compiler wrapper like ccache is used;
  • cmake_minimum_required:设置所需 CMake 的最小版本;

编译相关变量!!

  • CMAKE_CXX_STANDARD:设置 C++ 标准;
  • CMAKE_CXX_FLAGS:设置 C++ 编译参数;
  • CMAKE_C_FLAGS:设置 C 编译参数
1
2
3
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -w")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -w")
  • BUILD_SHARED_LIBS : if this is set to ON, then all libraries are built as shared libraries by default. SET(BUILD_SHARED_LIBS ON) ;
  • CMAKE_BUILD_TYPE : A variable which controls the type of build when using a single-configuration generator like the Makefile generator. It is case-insensitive;If you are using the Makefile generator, you can create your own build type like this:
1
2
3
set(CMAKE_BUILD_TYPE distribution)
set(CMAKE_CXX_FLAGS_DISTRIBUTION "-O3")
set(CMAKE_C_FLAGS_DISTRIBUTION "-O3")

参考文献

关键词

https://www.cnblogs.com/binbinjx/p/5626916.html

多源文件

https://blog.csdn.net/dyyzlzc/article/details/105189374

一些内容

https://elloop.github.io/tools/2016-04-10/learning-cmake-2-commands

https://wangpengcheng.github.io/2019/08/13/learn_cmake/

sons 和 makefile 类似,可以编译多种源文件,不过它编译的脚本叫 SConstruct。

小试一下

创建一个名为 SConstruct 的文件,按照 python 语法,写入如下内容。

1
print("... Hello, Scons !")

运行一下

1
2
3
4
5
6
7
$ scons
scons: Reading SConscript files ...
... Hello, Scons !
scons: done reading SConscript files.
scons: Building targets ...
scons: `.' is up to date.
scons: done building targets.

教程

简单编译

首先写一个简单的 C 文件

1
2
3
4
5
6
7
8
// hello.cpp
#include<iostream>
using namespace std;

int main(){
cout << "Hello, World!" << endl;
return 0;
}

然后只需要在 SConstruct 中编写

1
Program('hello.cpp')

这个短小的配置文件给了 SCons 两条信息:

  • hello.cpp: 你想编译什么(一个可执行程序),你编译的输入文件(hello.cpp)。
  • Program: 一个编译器方法(builder_method),一个 Python 调用告诉 SCons,你想编译一个可执行程序。Program 编译方法是 SCons 提供的许多编译方法中一个。

调用 Program 编译方法的的时候,它编译出来的程序名字是和源文件名是一样的。

从 hello.cpp 源文件编译一个可执行程序的调用将会编译出一个名为 hello 的可执行程序,在 windows 系统里会编译出一个名为 hello.exe 的可执行程序。

如果想编译出来的程序的名字与源文件名字不一样,只需要在源文件名的左边声明一个目标文件的名字就可以了:

1
Program('new_hello','hello.cpp')

SConstruct 文件实际上就是一个 Python 脚本。可以在 SConstruct 文件中使用 Python 的 # 注释:

重要的一点是 SConstruct 文件并不完全像一个正常的 Python 脚本那样工作,其工作方式更像一个 Makefile,那就是在 SConstruct 文件中 SCons 函数被调用的顺序并不影响 SCons 你实际想编译程序和目标文件的顺序。换句话说,当你调用 Program 方法,你并不是告诉 SCons 在调用这个方法的同时马上就编译这个程序,而是告诉 SCons 你想编译这个程序:

1
2
3
4
5
print "Calling Program('hello.c')"
Program('hello.c')
print "Calling Program('goodbye.c')"
Program('goodbye.c')
print "Finished calling Program()"

并不会顺序执行,因为 scons 是并行执行的这点要特别注意,是不同的线程进行编译的。

多个源文件

如果编译的源文件有多个.c 文件。可以这样写:

1
Program('hello_world', ['test.c', 'test1.c', 'test2.c'])

也可以使用 Glob 函数,定义一个匹配规则来指定源文件列表,比如*,?等标准的 shell 模式。如下所示:

1
Program('program', Glob('*.cpp'))

为了更容易处理文件名长列表,SCons 提供了一个 Split 函数,这个 Split 函数可以将一个用引号引起来,并且以空格或其他空白字符分隔开的字符串分割成一个文件名列表,示例如下:

1
Program('program', Split('main.cpp  file1.cpp  file2.cpp'))

或者

1
2
src_files=Split('main.cpp  file1.cpp  file2.cpp')
Program('program', src_files)

SCons 也允许使用 Python 关键字参数来标识输出文件和输入文件。输出文件是 target,输入文件是 source,示例如下:

1
2
src_files=Split('main.cpp  file1.cpp  file2.cpp')
Program(target='program', source=src_files)

多个程序之间共享源文件是很常见的代码重用方法。一种方式就是利用公共的源文件创建一个库文件,然后其他的程序可以链接这个库文件。另一个更直接,但是不够便利的方式就是在每个程序的源文件列表中包含公共的文件,示例如下:

1
2
3
4
5
common=['common1.cpp', 'common2.cpp']
foo_files=['foo.cpp'] + common
bar_files=['bar1.cpp', 'bar2.cpp'] + common
Program('foo', foo_files)
Program('bar', bar_files)

编译和链接库

静态库

可以使用 Library 方法来编译库文件:

1
Library('foo', ['f1.cpp', 'f2.cpp', 'f3.cpp'])

除了使用源文件外,Library 也可以使用目标文件

1
Library('foo', ['f1.c', 'f2.o', 'f3.c', 'f4.o'])

甚至可以在文件 List 里混用源文件和目标文件

1
Library('foo', ['f1.cpp', 'f2.o', 'f3.c', 'f4.o'])

使用 StaticLibrary 显示编译静态库

1
StaticLibrary('foo', ['f1.cpp', 'f2.cpp', 'f3.cpp'])

动态库

如果想编译动态库(在 POSIX 系统里)或 DLL 文件(Windows 系统),可以使用 SharedLibrary:

1
SharedLibrary('foo', ['f1.cpp', 'f2.cpp', 'f3.cpp'])

链接库

链接库文件的时候,使用$LIBS 变量指定库文件,使用$LIBPATH 指定存放库文件的目录:

1
2
Library('foo', ['f1.cpp', 'f2.cpp', 'f3.cpp'])
Program('prog', LIBS=['foo', 'bar'], LIBPATH='.')

注意到,你不需要指定库文件的前缀(比如 lib)或后缀(比如.a 或.lib),SCons 会自动匹配。

默认情况下,链接器只会在系统默认的库目录中寻找库文件。SCons 也会去$LIBPATH 指定的目录中去寻找库文件。$LIBPATH 由一个目录列表组成,如下所示:

1
Program('prog', LIBS='m', LIBPATH=['/usr/lib', '/usr/local/lib'])

节点对象

所有编译方法会返回一个节点对象列表,这些节点对象标识了那些将要被编译的目标文件。这些返回出来的节点可以作为参数传递给其他的编译方法。例如,假设我们想编译两个目标文件,这两个目标有不同的编译选项,并且最终组成一个完整的程序。这意味着对每一个目标文件调用 Object 编译方法,如下所示:

1
2
3
Object('hello.cpp', CCFLAGS='-DHELLO')
Object('goodbye.cpp', CCFLAGS='-DGOODBYE')
Program(['hello.o', 'goodbye.o'])

这样指定字符串名字的问题就是我们的 SConstruct 文件不再是跨平台的了。因为在 Windows 里,目标文件成为了 hello.obj 和 goodbye.obj。一个更好的解决方案就是将 Object 编译方法返回的目标列表赋值给变量,这些变量然后传递给 Program 编译方法:

1
2
3
hello_list = Object('hello.cpp', CCFLAGS='-DHELLO')
goodbye_list = Object('goodbye.c', CCFLAGS='-DGOODBYE')
Program(hello_list + goodbye_list)

显示创建文件和目录节点

在 SCons 里,表示文件的节点和表示目录的节点是有清晰区分的。SCons 的 File 和 Dir 函数分别返回一个文件和目录节点:

1
2
hello_c=File('hello.cpp')
Program(hello_c)

通常情况下,你不需要直接调用 File 或 Dir,因为调用一个编译方法的时候,SCons 会自动将字符串作为文件或目录的名字,以及将它们转换为节点对象。只有当你需要显示构造节点类型传递给编译方法或其他函数的时候,你才需要手动调用 File 和 Dir 函数。有时候,你需要引用文件系统中一个条目,同时你又不知道它是一个文件或一个目录,你可以调用 Entry 函数,它返回一个节点可以表示一个文件或一个目录:

1
xyzzy=Entry('xyzzy')

将一个节点的文件名当作一个字符串

如果你不是想打印文件名,而是做一些其他的事情,你可以使用内置的 Python 的 str 函数。例如,你想使用 Python 的 os.path.exists 判断一个文件是否存在:

1
2
3
4
5
import os.path
program_list=Program('hello.cpp')
program_name=str(program_list[0])
if not os.path.exists(program_name):
print(program_name, "does not exist!")

依赖性

隐式依赖:$CPPPATH Construction 变量

1
2
3
4
5
6
7
8
#include <iostream>
#include "hello.h"
using namespace std;

int main(){
cout << "Hello, " << string << endl;
return 0;
}

并且,hello.h 文件如下:

1
#define string "world"

在这种情况下,我们希望 SCons 能够认识到,如果 hello.h 文件的内容发生改变,那么 hello 程序必须重新编译。我们需要修改 SConstruct 文件如下:

1
2
# CPPPATH 告诉 SCons 去当前目录('.') 查看那些被 C 源文件(.c或.h文件)包含的文件。
Program('hello.cpp', CPPPATH='.')

就像$LIBPATH 变量,$CPPPATH 也可能是一个目录列表,或者一个被系统特定路径分隔符分隔的字符串。

1
Program('hello.cpp', CPPPATH=['include', '/home/project/inc'])

环境

外部环境

外部环境指的是在用户运行 SCons 的时候,用户环境中的变量的集合。这些变量在 SConscript 文件中通过 Python 的os.environ字典可以获得。你想使用外部环境的 SConscript 文件需要增加一个import os语句。

构造环境

一个构造环境是在一个 SConscript 文件中创建的一个唯一的对象,这个对象包含了一些值可以影响 SCons 编译一个目标的时候做什么动作,以及决定从那一个源中编译出目标文件。SCons 一个强大的功能就是可以创建多个构造环境,包括从一个存在的构造环境中克隆一个新的自定义的构造环境。

创建一个构造环境:Environment 函数

默认情况下,SCons 基于你系统中工具的一个变量集合来初始化每一个新的构造环境。当你初始化一个构造环境时,你可以设置环境的构造变量来控制一个是如何编译的。例如:

1
2
3
4
5
env=Environment(CC='gcc', CCFLAGS='-O2')
env.Program('foo.c')
# or
env=Environment(CXX='/usr/local/bin/g++', CXXFLAGS='-02')
env.Program('foo.cpp')

从一个构造环境中获取值

你可以使用访问 Python 字典的方法获取单个的构造变量:

1
2
3
env=Environment()
print("CC is:", env['CC'])
print("CXX is:", env['CXX'])

一个构造环境实际上是一个拥有方法的对象。如果你想直接访问构造变量的字典,你可以使用 Dictionary 方法:

1
2
3
4
env=Environment(FOO='foo', BAR='bar')
dict=env.Dictionary()
for key in ['OBJSUFFIX', 'LIBSUFFIX', 'PROGSUFFIX']:
print("key=%s, value=%s" % (key,dict[key]))

默认的构造环境:DefaultEnvironment 函数

可以控制默认构造环境的设置,使用 DefaultEnvironment 函数:

1
DefaultEnvironment(CC='/usr/local/bin/gcc')

这样配置以后,所有 Program 或者 Object 的调用都将使用/usr/local/bin/gcc 编译目标文件。注意到 DefaultEnvironment 返回初始化了的默认构造环境对象,这个对象可以像其他构造环境一样被操作。所以如下的代码和上面的例子是等价的:

1
2
env=DefaultEnvironment()
env['CC']='/usr/local/bin/gcc'

多个构造环境

构造环境的真正优势是你可以创建你所需要的许多不同的构造环境,每一个构造环境对应了一种不同的方式去编译软件的一部分或其他文件。比如,如果我们需要用-O2 编译一个程序,编译另一个用-g,我们可以如下做:

1
2
3
4
opt=Environment(CCFLAGS='-O2')
dbg=Environment(CCFLAGS='-g')
opt.Program('foo','foo.cpp')
dbg.Program('bar','bar.cpp')

参考文献

https://blog.csdn.net/MOU_IT/article/details/95229790

尝试在 mac 安装 systemC,可能因为有安装 gem5 的经历,导致对 mac 完全不是 linux 这件事有了深刻的心理阴影。所以在安装 systemC 的时候潜意识也会觉得有一堆问题甚至到最后发现完全不能安装(为此可能也潜意识放弃了很多尝试解决的办法)。今天意外安装成功了,特此记录一下。

CXX 环境问题

其实最关键的原因就是 mac 默认的 C 编译环境是 clang (无论是输入 gcc 还是 clang 结果都是使用 clang,如果要用 brew 安装的 gcc,具体需要用 g++-10 之类的命令)

安装 systemc

安装 gcc 环境

我此时的版本是 gcc version 10.2.0 (Homebrew GCC 10.2.0_4) 但是我觉得应该各个版本都可以

1
brew install gcc

但是注意这个时候输入 gcc 或者 g++ 都还是 clang,要想使用 gcc 应该用 gcc-10 或者 g++-10

然后设置 make 中的 CXX 环境

1
export CXX=g++-10

官网下载 systemc 安装包

https://www.accellera.org/downloads/standards/systemc

我下载的是 SystemC 2.3.3 (Includes TLM),其中的 zip 和 tar.gz 都可以。

然后解压缩到以后都会保存到的位置,比如我个人习惯在

1
/Users/luolie/Documents/systemc-2.3.3

新建配置目录

进入相关目录下 (systemc-2.3.3)

1
cd systemc-2.3.3

新建配置临时目录

1
2
mkdir objdir
cd objdir

进行相关配置

主要是配置 ../configure [option]

这里的 [option] 建议加

  1. --with-arch-suffix= : 要不然生成的文件会是 lib-macosx64 这样的,如果加上这个参数,则直接就是 lib
  2. 还可以设置一下以下参数
1
2
3
4
5
6
7
--disable-shared        do not build shared library (libsystemc.so)
--enable-debug include debugging symbols
--disable-optimize disable compiler optimization
--disable-async-updates disable request_async_update support
--enable-pthreads use POSIX threads for SystemC processes
--enable-phase-callbacks
enable simulation phase callbacks (experimental)

make

然后 make 就好啦,中间会有一些 c 文件的问题,但是不是 error。

1
2
3
make
make check // 运行example文件检查一下
make install

然后可以 rm -rf objdir 移除临时目录,但个人建议不要,这个可以留着以后 make uninstall

自己测试一下

编写代码

随便找个位置(不一定在 systemc 目录)编写如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// hello.h

#include "systemc.h"

// Hello_world is module name
SC_MODULE(hello_world) {
SC_CTOR(hello_world){
// Nothing in constructor
}
void say_hello() {
//Print "Hello World" to the console.
cout << "Hello World.\n";
}
};
1
2
3
4
5
6
7
8
9
10
// hello.cpp
#include "hello.h"

// sc_main in top level function like in C++ main
int sc_main(int argc, char *argv[]){
hello_world hello("HELLO");
// Print the hello world
hello.say_hello();
return (0);
}

特别注意,这里主程序虽然和 C++ 的main 函数的输入参数相同,一般 main 我们会省略这两个参数,但是这里这两个参数不能省略!!!
即不能 int sc_mian() {...}
不然会报错

测试

  1. 首先添加一个 bash 的全局环境变量$SYSTEMC_HOME(这里用 export 但实际使用建议写入 bash)
1
2
3
4
# export SYSTEMC_HOME=path/to/systemc-2.3.3
# eg.
export SYSTEMC_HOME=/Users/conflux/Downloads/systemc-2.3.3
echo $SYSTEMC_HOME
  1. 编译(后面可以为此做一个 make 或者 scons)
1
g++-10 hello.cpp -o hello.o -L $SYSTEMC_HOME/lib -I $SYSTEMC_HOME/include -l systemc

这里逐个参数讲解一下

  • g++-10: 使用 g++ version10 作为编译器
  • hello.cpp: 编译的主文件
  • -o hello.o: 输出文件名
  • -L $SYSTEMC_HOME/lib: 第一个寻找的库文件目录
  • -I $SYSTEMC_HOME/include: 第一个寻找的头文件目录
  • -l systemc: 在上面库文件寻找名叫 libsystemc.a 的动态库文件
  1. 运行一下
1
./hello.o

输出

1
2
3
4
        SystemC 2.3.3-Accellera --- Apr 26 2021 16:46:59
Copyright (c) 1996-2018 by all Contributors,
ALL RIGHTS RESERVED
Hello World.

耶!

参考文献

https://github.com/accellera-official/systemc/blob/master/INSTALL.md

https://stackoverflow.com/questions/25961573/how-to-use-and-install-systemc-in-terminal-mac-os-x

官方文档 https://www.gem5.org/documentation/learning_gem5/part1/building/

ubuntu18.05 版本

1
sudo apt install build-essential git m4 scons zlib1g zlib1g-dev libprotobuf-dev protobuf-compiler libprotoc-dev libgoogle-perftools-dev python-dev python

最后 编译

1
scons build/X86/gem5.opt -j <NUMBER OF CPUs ON YOUR PLATFORM>

出现的错误:

  1. 权限问题
1
2
3
/tmp/tmpi2synp81: 1: util/cpt_upgrader.py: Permission denied
scons: *** [build/X86/sim/tags.cc] Error 126
scons: building terminated because of errors.

解决: 给该文件权限 777

1
sudo chmod 777 ./util/cpt_upgrader.py

参考文献

https://yuchen112358.github.io/2016/03/21/gem5-install/

其实之前(2004 年)华为和中兴基本是半分天下的,两个公司体量差不多。
但是华为企业文化比较“狼”,决定做手机就投入一切去做,而中兴则相对保守,毕竟自己是做交换机起家的,还是会担心转型失败的代价。

这也和两个企业本身的企业结构有关,中兴是上市企业,那个时候还是很担心企业的股市影响。而且中兴主要是国资委控股,四舍五入算是半个国企。
这也使得企业固然相对笨重,主要还是靠国家给的单子(三大通信企业一年 500 亿订单)。这样企业不会太腾飞。但是也能长久的养老。

可能也正是看见了中兴上市之后的笨手笨脚,华为才能做出永不上市的决定(截止至 2020 年,华为的营收是中兴的 8 倍)。

而华为激进的狼性,至少在手机的战场上是打赢了的。

不得不承认华为到现在 2020 年的国民品牌,一个是靠着华为的运营。另一个也是华为开始做和民众相关的项目,手机。

而华为也享受着大体量带来的优势,决定投入智能手机行业,接着就是自主研发芯片。而 麒麟 芯片的问世和一代代迭代。让华为手机逐渐走上人们的视野。

而中兴的努比亚,还在襁褓当中(因为企业的特性,和政府关系很好,彭丽媛就用的努比亚)(中兴在深圳市的地产话语权也很大,这也是权力的一部分,虽然就价值来说中兴确实没有那么盈利)

但是贸易战开打之后,华为很难长时间继续做手机,目前华为的下一个目光投入的是汽车。但是具体汽车什么时候能成型,还有待商榷。

14 年中兴的蓝剑计划还能给人才公寓。

在 04 年的时候,整个深圳还是华为中兴的时代。中兴大楼在深圳最好的位置,当时的腾讯还是小弟弟。但是现在深圳最高的楼已经是腾讯大楼了。

oppo 是一个很有南方小企业家特质的公司,格局不大但是非常能赚钱。

这个公司只做利润率高的产品。这也是这个企业一直以来的商业法宝,本分(to do list & not to do list)的企业文化。

而足够赚钱的企业,也不需要上市来吸引现金流。

找工作时遇到了两个差不多,或者都还可以的选择,一个是华为,一个是 OPPO

华为在北京 OPPO 在上海

公司

华为是一个体量大,业务多的公司,但是可能较长时间不会涉及手机业务了,接下来涉及的还有车用、家用产品的生态。之前我也是不太想去华为的,因为觉得整个公司的企业文化都传播的很恐怖,加班严重还不给加班工资。但是现在自己实际感受起来好像也没有那么严重,而且加班文化确实是分部门的,我能去的这个部门现在看来也是没有这些问题的。

OPPO 的芯片部门是个初创公司,虽然我之前是不太想去初创公司,但是现在动摇了,思考了一下动摇原因,因为觉得别的小初创公司一个是业务能力不行,一个是业务范围比较窄,还有就是从概率角度这些公司都很难成长起来。但是 OPPO 恰巧这三点都不符合。

城市

北京这个城市现在想来也很符合想象中的样子了。整个城市结构规矩方正,这里的人和事物也都规很具传统的东方色彩,规矩保守官僚。城市干燥,每天洗澡会身上痒,抹身体乳护肤乳之后倒是还好,就是会有点麻烦。另外晾衣服很快就能干这点我真的是太爱了,而且城市很少下雨,我确实也不太喜欢总是湿漉漉的城市。坏处就是只有两季,虽然不下雨,但是冬天夏天在外面玩都挺痛苦的。

上海虽然只有去旅游的经历(还有和同学聊天或者网上查询得来的二手经历),但是总体感受是更像我的家一样,城市结构错综复杂(看地铁图就知道),不是很好认路,但是在南方小城生活过的人应该都觉得这也不是太大的问题。上海是中国接受新文化的第一站,里面的人充满了现代思想,自由精致自私。城市沿海,整天湿漉漉的,屋子里容易发霉,所以如果在上海以后一定要买抽湿机,但是潮湿的环境,鼻子和皮肤都比较舒服,相对气候也比较温润,不会有比较极端的温度(但是下雨出去玩也挺不方便)。

发展

单说个人能力成长很难定义,在华为必然会有更好的培训和更多的项目接触。但是在我读了这么多年书之后,确实也觉得过多的项目未必能给自身带来成长。

想到了江源师兄给我说过的,很多时候有些人我们觉得很厉害,并非是因为他做了很好的项目(大多数人也遇不见很好的项目),而是因为他们在自己的时间里能做些什么,这些往往才能他们成长的很出色。

我现在不能很确定自己是不是内驱力很强的人。在 OPPO 有更多的时间空间的自由环境就像一把双刃剑,可能能给我沉淀的时间让我更多的思考成长甚至未来自己的创业,当然也可能成为让我颓废和偷懒的温床,很容易荒废掉一些时间。而在华为真的是能逼着做一些公司指定了方向的成长,可能会有很多时间在做小意义低成长的事情,但是能让我更多的忙起来,至少能持续处于工作的状态。

薪酬

在此之前我会觉得可能更多会借助来回跳槽来让自己工资成长,但是现在看来未必是这样,说实话现在的薪酬已经算是基本工资中比较高的了,再成长的收益也不大(因为会更多的用于缴税),以后薪酬的成长来源于股票分红的话,那么还是依赖于在一个公司长期工作下去。

两家长期来看都挺好的,在华为工作 10 年以上的人基本上所有人都过的很不错,不过我不知道这是不是因为过去的 10 年恰好是中国信息产业狂增的浪潮导致的,我也不知道这样的动力对于华为能否再坚持下一个 10 年。OPPO 刚好是一个新的初创机会,我如果能在里面做 10 年,也算是元老级员工,那么可能能遇见像是华为上个 10 年一样的成长。(当然也可能会最后做不出很有竞争力的产品,虽然我觉得这个概率不高)

人生意义

我觉得华为还是确实在做一些,很有大公司风范的事情的,虽然里面的个人都是一块垫脚石,但是项目最后都是有很大格局考量的项目,参与其中也确实会给精神带给较大的满足。

OPPO 上下都离不开一句话:“OPPO 不缺钱”,这个公司确实还是很有钱的。以后肯定也会贯穿于我自己,主要是“钱”确实是个人生活的核心,能让我更多的专注于自己的生活,而工作不过是赚钱的工具,精神满足需要更多来源于生活的方方面面。

城市经济

城市的未来我是真的看不透,我不知道中国会不会和美国有相同的发展,上海和北京会未来变成纽约和华盛顿么,如果是的话上海肯定更好,但是不同国家还是很不一样的,北京确实在降低经济职能,但是现在北京的经济已经很稳定了,不可能选择直接倒逼经济(也很难实现)。

那么北京会成为纽约而让上海成为西雅图么?我不知道,感觉大概率会有中国自己特色的、和美国完全不一样的城市组成结构。

GPU 是显卡(Video card、Display card、Graphics card)最核心的部件。
但除了 GPU,显卡还有扇热器、通讯元件、与主板和显示器连接的各类插槽。

History

GPU 自从上世纪 90 年代出现雏形以来,经过 20 多年的发展,已经发展成不仅仅是渲染图形这么简单,
还包含了数学计算、物理模拟、AI 运算等功能。(主要是因为对于数据密集任务的高效处理能力)

NV GPU 架构发展

众所周知,CPU 的发展符合摩尔定律:每 18 个月速度翻倍。

NVIDIA 创始人黄仁勋在很多年前曾信誓旦旦地说,GPU 的速度和功能要超越摩尔定律,每 6 个月就翻一倍。
NV 的 GPU 发展史证明,他确实做到了!GPU 的提速幅率远超 CPU。

NVIDIA GPU 架构历经多次变革,从起初的 Tesla 发展到最新的 Turing 架构,发展史可分为以下时间节点:

  • 2008 - Tesla

    Tesla 最初是给计算处理单元使用的,应用于早期的 CUDA 系列显卡芯片中,并不是真正意义上的普通图形处理芯片。

  • 2010 - Fermi

    Fermi 是第一个完整的 GPU 计算架构。首款可支持与共享存储结合纯 cache 层次的 GPU 架构,支持 ECC 的 GPU 架构。

  • 2012 - Kepler

    Kepler 相较于 Fermi 更快,效率更高,性能更好。

  • 2014 - Maxwell

    其全新的立体像素全局光照 (VXGI) 技术首次让游戏 GPU 能够提供实时的动态全局光照效果。基于 Maxwell 架构的 GTX 980 和 970 GPU 采用了包括多帧采样抗锯齿 (MFAA)、动态超级分辨率 (DSR)、VR Direct 以及超节能设计在内的一系列新技术。

  • 2016 - Pascal

    Pascal 架构将处理器和数据集成在同一个程序包内,以实现更高的计算效率。1080 系列、1060 系列基于 Pascal 架构

  • 2017 - Volta

    Volta 配备 640 个 Tensor 核心,每秒可提供超过 100 兆次浮点运算(TFLOPS) 的深度学习效能,比前一代的 Pascal 架构快 5 倍以上。

  • 2018 - Turing

    Turing 架构配备了名为 RT Core 的专用光线追踪处理器,能够以高达每秒 10 Giga Rays 的速度对光线和声音在 3D 环境中的传播进行加速计算。Turing 架构将实时光线追踪运算加速至上一代 NVIDIA Pascal™ 架构的 25 倍,并能以高出 CPU 30 多倍的速度进行电影效果的最终帧渲染。2060 系列、2080 系列显卡也是跳过了 Volta 直接选择了 Turing 架构。

GPU 功能

现代 GPU 除了绘制图形外,还担当了很多额外的功能,综合起来如下几方面:

  • 图形绘制。

    这是 GPU 最传统的拿手好戏,也是最基础、最核心的功能。为大多数 PC 桌面、移动设备、图形工作站提供图形处理和绘制功能。

  • 物理模拟。

    GPU 硬件集成的物理引擎(PhysX、Havok),为游戏、电影、教育、科学模拟等领域提供了成百上千倍性能的物理模拟,使得以前需要长时间计算的物理模拟得以实时呈现。

  • 海量计算。

    计算着色器及流输出的出现,为各种可以并行计算的海量需求得以实现,CUDA 就是最好的例证。

  • AI 运算。

    近年来,人工智能的崛起推动了 GPU 集成了 AI Core 运算单元,反哺 AI 运算能力的提升,给各行各业带来了计算能力的提升。

  • 其它计算。

    音视频编解码、加解密、科学计算、离线渲染等等都离不开现代 GPU 的并行计算能力和海量吞吐能力。

物理架构

由于纳米工艺的引入,GPU 可以将数以亿记的晶体管和电子器件集成在一个小小的芯片内。从宏观物理结构上看,现代大多数桌面级 GPU 的大小跟数枚硬币同等大小,部分甚至比一枚硬币还小(下图)。

GPU-size

当 GPU 结合散热风扇、PCI 插槽、HDMI 接口等部件之后,就组成了显卡。

显卡不能独立工作,需要装载在主板上,结合 CPU、内存、显存、显示器等硬件设备,组成完整的 PC 机。

GPU 的微观结构因不同厂商、不同架构都会有所差异,但核心部件、概念、以及运行机制大同小异。下面将展示部分架构的 GPU 微观物理结构。

NV Tesla

Tesla

Tesla 微观架构总览图如上。下面将阐述它的特性和概念:

  • 拥有 7 组 TPC(Texture/Processor Cluster,纹理处理簇)
  • 每个 TPC 有两组 SM(Stream Multiprocessor,流多处理器)每个 SM 包含:
    • 6 个 SP(Streaming Processor,流处理器)
    • 2 个 SFU(Special Function Unit,特殊函数单元)
    • L1 缓存、MT Issue(多线程指令获取)、C-Cache(常量缓存)、共享内存
  • 除了 TPC 核心单元,还有与显存、CPU、系统内存交互的各种部件。

NV Fermi

NV-Fermi

拥有 16 个 SM

  • 每个 SM:
    • 2 个 Warp(线程束)
    • 两组共 32 个 Core
    • 16 组加载存储单元(LD/ST)
    • 4 个特殊函数单元(SFU)
  • 每个 Warp:
    • 16 个 Core
    • Warp 编排器(Warp Scheduler)
    • 分发单元(Dispatch Unit)
  • 每个 Core:
    • 1 个 FPU(浮点数单元)
    • 1 个 ALU(逻辑运算单元)

NV Maxwell

NV-maxwell

采用了 Maxwell 的 GM204,拥有 4 个 GPC,每个 GPC 有 4 个 SM,对比 Tesla 架构来说,在处理单元上有了很大的提升

NV Kepler

NV-Kepler

Kepler 除了在硬件有了提升,有了更多处理单元之外,还将 SM 升级到了 SMX。SMX 是改进的架构,支持动态创建渲染线程(下图),以降低延迟。

NV-Kerpler-Threads

NV Turing

NV-Turing

上图是采纳了 Turing 架构的 TU102 GPU,它的特点如下:

  • 6 GPC(图形处理簇)
  • 36 TPC(纹理处理簇)
  • 72 SM(流多处理器)
  • 每个 GPC 有 6 个 TPC,每个 TPC 有 2 个 SM
  • 4,608 CUDA 核
  • 72 RT 核
  • 576 Tensor 核
  • 288 纹理单元
  • 12x32 位 GDDR6 内存控制器 (共 384 位)

单个 SM 的结构图如下:

NV-Turing-SM

每个 SM 包含:

  • 64 CUDA 核
  • 8 Tensor 核
  • 256 KB 寄存器文件

TU102 GPU 芯片实物图:

NV-Turing-physical

GPU 架构共性

纵观上一节的所有 GPU 架构,可以发现它们虽然有所差异,但存在着很多相同的概念和部件:

  • GPC
  • TPC
  • Thread
  • SM、SMX、SMM
  • Warp
  • SP
  • Core
  • ALU
  • FPU
  • SFU
  • ROP
  • Load/Store Unit
  • L1 Cache
  • L2 Cache
  • Memory
  • Register File

以上各个部件的用途将在下一章详细阐述。

GPU 为什么会有这么多层级且有这么多雷同的部件?答案是 GPU 的任务是天然并行的,现代 GPU 的架构皆是以高度并行能力而设计的。

GPU 运行机制

渲染总览

由上一章可得知,现代 GPU 有着相似的结构,有很多相同的部件,在运行机制上,也有很多共同点。下面是 Fermi 架构的运行机制总览图:

Fermi-run

从 Fermi 开始 NVIDIA 使用类似的原理架构,使用一个 Giga Thread Engine 来管理所有正在进行的工作,GPU 被划分成多个 GPCs (Graphics Processing Cluster),每个 GPC 拥有多个 SM(SMX、SMM)和一个光栅化引擎 (Raster Engine),它们其中有很多的连接,最显著的是 Crossbar,它可以连接 GPCs 和其它功能性模块(例如 ROP 或其他子系统)。

程序员编写的 shader 是在 SM 上完成的。每个 SM 包含许多为线程执行数学运算的 Core (核心)。例如,一个线程可以是顶点或像素着色器调用。这些 Core 和其它单元由 Warp Scheduler 驱动,Warp Scheduler 管理一组 32 个线程作为 Warp (线程束)并将要执行的指令移交给 Dispatch Units。

GPU 中实际有多少这些单元(每个 GPC 有多少个 SM,多少个 GPC …)取决于芯片配置本身。例如,GM204 有 4 个 GPC,每个 GPC 有 4 个 SM,但 Tegra X1 有 1 个 GPC 和 2 个 SM,它们均采用 Maxwell 设计。 SM 设计本身(内核数量,指令单位,调度程序 …)也随着时间的推移而发生变化,并帮助使芯片变得如此高效,可以从高端台式机扩展到笔记本电脑移动。

Fermi-SM

如上图,对于某些 GPU(如 Fermi 部分型号)的单个 SM,包含:

  • 32 个运算核心 (Core,也叫流处理器 Stream Processor)
  • 16 个 LD/ST(load/store)模块来加载和存储数据
  • 4 个 SFU(Special function units)执行特殊数学运算(sin、cos、log 等)
  • 128KB 寄存器(Register File)
  • 64KB L1 缓存
  • 全局内存缓存(Uniform Cache)
  • 纹理读取单元
  • 纹理缓存(Texture Cache)
  • PolyMorph Engine:多边形引擎负责属性装配(attribute Setup)、顶点拉取(VertexFetch)、曲面细分、栅格化(这个模块可以理解专门处理顶点相关的东西)。
  • 2 个 Warp Schedulers:这个模块负责 warp 调度,一个 warp 由 32 个线程组成,warp 调度器的指令通过 Dispatch Units 送到 Core 执行。
  • 指令缓存(Instruction Cache)
  • 内部链接网络(Interconnect Network)

GPU 运行逻辑

了解上一节的部件和概念之后,可以深入阐述 GPU 的渲染过程和步骤。下面将以 Fermi 家族的 SM 为例,进行逻辑管线的详细说明。

Fermi-logic

  1. 程序通过图形 API(DX、GL、WEBGL)发出 drawcall 指令,指令会被推送到驱动程序,驱动会检查指令的合法性,然后会把指令放到 GPU 可以读取的 Pushbuffer 中。

  2. 经过一段时间或者显式调用 flush 指令后,驱动程序把 Pushbuffer 的内容发送给 GPU,GPU 通过主机接口(Host Interface)接受这些命令,并通过前端(Front End)处理这些命令。

  3. 在图元分配器(Primitive Distributor)中开始工作分配,处理 indexbuffer 中的顶点产生三角形分成批次(batches),然后发送给多个 PGCs。这一步的理解就是提交上来 n 个三角形,分配给这几个 PGC 同时处理。

Fermi-SM-run

  1. 在 GPC 中,每个 SM 中的 Poly Morph Engine 负责通过三角形索引(triangle indices)取出三角形的数据(vertex data),即图中的 Vertex Fetch 模块。

  2. 在获取数据之后,在 SM 中以 32 个线程为一组的线程束(Warp)来调度,来开始处理顶点数据。Warp 是典型的单指令多线程(SIMT,SIMD 单指令多数据的升级)的实现,也就是 32 个线程同时执行的指令是一模一样的,只是线程数据不一样,这样的好处就是一个 warp 只需要一个套逻辑对指令进行解码和执行就可以了,芯片可以做的更小更快,之所以可以这么做是由于 GPU 需要处理的任务是天然并行的。

  3. SM 的 warp 调度器会按照顺序分发指令给整个 warp,单个 warp 中的线程会锁步(lock-step)执行各自的指令,如果线程碰到不激活执行的情况也会被遮掩(be masked out)。被遮掩的原因有很多,例如当前的指令是 if(true)的分支,但是当前线程的数据的条件是 false,或者循环的次数不一样(比如 for 循环次数 n 不是常量,或被 break 提前终止了但是别的还在走),因此在 shader 中的分支会显著增加时间消耗,在一个 warp 中的分支除非 32 个线程都走到 if 或者 else 里面,否则相当于所有的分支都走了一遍,线程不能独立执行指令而是以 warp 为单位,而这些 warp 之间才是独立的。

  4. warp 中的指令可以被一次完成,也可能经过多次调度,例如通常 SM 中的 LD/ST(加载存取)单元数量明显少于基础数学操作单元。

  5. 由于某些指令比其他指令需要更长的时间才能完成,特别是内存加载,warp 调度器可能会简单地切换到另一个没有内存等待的 warp,这是 GPU 如何克服内存读取延迟的关键,只是简单地切换活动线程组。为了使这种切换非常快,调度器管理的所有 warp 在寄存器文件中都有自己的寄存器。这里就会有个矛盾产生,shader 需要越多的寄存器,就会给 warp 留下越少的空间,就会产生越少的 warp,这时候在碰到内存延迟的时候就会只是等待,而没有可以运行的 warp 可以切换。

GPU-store

  1. 一旦 warp 完成了 vertex-shader 的所有指令,运算结果会被 Viewport Transform 模块处理,三角形会被裁剪然后准备栅格化,GPU 会使用 L1 和 L2 缓存来进行 vertex-shader 和 pixel-shader 的数据通信。

GPU-triangle

  1. 接下来这些三角形将被分割,再分配给多个 GPC,三角形的范围决定着它将被分配到哪个光栅引擎(raster engines),每个 raster engines 覆盖了多个屏幕上的 tile,这等于把三角形的渲染分配到多个 tile 上面。也就是像素阶段就把按三角形划分变成了按显示的像素划分了。

GPU-distribution-crossbar

  1. SM 上的 Attribute Setup 保证了从 vertex-shader 来的数据经过插值后是 pixel-shade 是可读的。

  2. GPC 上的光栅引擎(raster engines)在它接收到的三角形上工作,来负责这些这些三角形的像素信息的生成(同时会处理裁剪 Clipping、背面剔除和 Early-Z 剔除)。

  3. 32 个像素线程将被分成一组,或者说 8 个 2x2 的像素块,这是在像素着色器上面的最小工作单元,在这个像素线程内,如果没有被三角形覆盖就会被遮掩,SM 中的 warp 调度器会管理像素着色器的任务。

  4. 接下来的阶段就和 vertex-shader 中的逻辑步骤完全一样,但是变成了在像素着色器线程中执行。由于不耗费任何性能可以获取一个像素内的值,导致锁步执行非常便利,所有的线程可以保证所有的指令可以在同一点。

GPU-ROP

  1. 最后一步,现在像素着色器已经完成了颜色的计算还有深度值的计算,在这个点上,我们必须考虑三角形的原始 api 顺序,然后才将数据移交给 ROP(render output unit,渲染输入单元),一个 ROP 内部有很多 ROP 单元,在 ROP 单元中处理深度测试,和 framebuffer 的混合,深度和颜色的设置必须是原子操作,否则两个不同的三角形在同一个像素点就会有冲突和错误。

GPU 技术要点

SIMD & SIMT

SIMD(Single Instruction Multiple Data)是单指令多数据,在 GPU 的 ALU 单元内,一条指令可以处理多维向量(一般是 4D)的数据。比如,有以下 shader 指令:

1
float4 c = a + b; // a, b都是float4类型

对于没有 SIMD 的处理单元,需要 4 条指令将 4 个 float 数值相加,汇编伪代码如下:

1
2
3
4
ADD c.x, a.x, b.x
ADD c.y, a.y, b.y
ADD c.z, a.z, b.z
ADD c.w, a.w, b.w

但有了 SIMD 技术,只需一条指令即可处理完:

1
SIMD_ADD c, a, b

SIMT(Single Instruction Multiple Threads,单指令多线程)是 SIMD 的升级版,可对 GPU 中单个 SM 中的多个 Core 同时处理同一指令,并且每个 Core 存取的数据可以是不同的。

1
SIMT_ADD c, a, b

上述指令会被同时送入在单个 SM 中被编组的所有 Core 中,同时执行运算,但abc的值可以不一样:

这里应该指的是可以是不同类型

co-issue

co-issue是为了解决 SIMD 运算单元无法充分利用的问题。例如下图,由于 float 数量的不同,ALU 利用率从 100%依次下降为 75%、50%、25%。

为了解决着色器在低维向量的利用率低的问题,可以通过合并 1D 与 3D 或 2D 与 2D 的指令。例如下图,DP3指令用了 3D 数据,ADD指令只有 1D 数据,co-issue 会自动将它们合并,在同一个 ALU 只需一个指令周期即可执行完。

但是,对于向量运算单元(Vector ALU),如果其中一个变量既是操作数又是存储数的情况,无法启用 co-issue 技术:

于是标量指令着色器(Scalar Instruction Shader)应运而生,它可以有效地组合任何向量,开启 co-issue 技术,充分发挥 SIMD 的优势。

if-else 语句

如上图,SM 中有 8 个 ALU(Core),由于 SIMD 的特性,每个 ALU 的数据不一样,导致 if-else 语句在某些 ALU 中执行的是true分支(黄色),有些 ALU 执行的是false分支(灰蓝色),这样导致很多 ALU 的执行周期被浪费掉了(即 masked out),拉长了整个执行周期。最坏的情况,同一个 SM 中只有 1/8(8 是同一个 SM 的线程数,不同架构的 GPU 有所不同)的利用率。

同样,for 循环也会导致类似的情形,例如以下 shader 代码:

1
2
3
4
5
6
7
8
9
10
void func(int count, int breakNum)
{
for(int i=0; i<count; ++i)
{
if (i == breakNum)
break;
else
// do something
}
}

由于每个 ALU 的count不一样,加上有break分支,导致最快执行完 shader 的 ALU 可能是最慢的 N 分之一的时间,但由于 SIMD 的特性,最快的那个 ALU 依然要等待最慢的 ALU 执行完毕,才能接下一组指令的活!也就白白浪费了很多时间周期。

Early-Z

早期 GPU 的渲染管线的深度测试是在像素着色器之后才执行(下图),这样会造成很多本不可见的像素执行了耗性能的像素着色器计算。

后来,为了减少像素着色器的额外消耗,将深度测试提至像素着色器之前(下图),这就是 Early-Z 技术的由来。

Early-Z 技术可以将很多无效的像素提前剔除,避免它们进入耗时严重的像素着色器。Early-Z 剔除的最小单位不是 1 像素,而是像素块(pixel quad,2x2 个像素。

但是,以下情况会导致 Early-Z 失效:

开启 Alpha Test:由于 Alpha Test 需要在像素着色器后面的 Alpha Test 阶段比较,所以无法在像素着色器之前就决定该像素是否被剔除。

  • 开启 Alpha Blend:启用了 Alpha 混合的像素很多需要与 frame buffer 做混合,无法执行深度测试,也就无法利用 Early-Z 技术。
  • 开启 Tex Kill:即在 shader 代码中有像素摒弃指令(DX 的 discard,OpenGL 的 clip)。
  • 关闭深度测试。Early-Z 是建立在深度测试看开启的条件下,如果关闭了深度测试,也就无法启用 Early-Z 技术。
  • 开启 Multi-Sampling:多采样会影响周边像素,而 Early-Z 阶段无法得知周边像素是否被裁剪,故无法提前剔除。
  • 以及其它任何导致需要混合后面颜色的操作。

此外,Early-Z 技术会导致一个问题:深度数据冲突(depth data hazard)。

例子要结合上图,假设数值深度值 5 已经经过 Early-Z 即将写入 Frame Buffer,而深度值 10 刚好处于 Early-Z 阶段,读取并对比当前缓存的深度值 15,结果就是 10 通过了 Early-Z 测试,会覆盖掉比自己小的深度值 5,最终 frame buffer 的深度值是错误的结果。

避免深度数据冲突的方法之一是在写入深度值之前,再次与 frame buffer 的值进行对比:

统一着色器架构(Unified shader Architecture)

在早期的 GPU,顶点着色器和像素着色器的硬件结构是独立的,它们各有各的寄存器、运算单元等部件。这样很多时候,会造成顶点着色器与像素着色器之间任务的不平衡。对于顶点数量多的任务,像素着色器空闲状态多;对于像素多的任务,顶点着色器的空闲状态多(下图)。

于是,为了解决 VS 和 PS 之间的不平衡,引入了统一着色器架构(Unified shader Architecture)。用了此架构的 GPU,VS 和 PS 用的都是相同的 Core。也就是,同一个 Core 既可以是 VS 又可以是 PS。

这样就解决了不同类型着色器之间的不平衡问题,还可以减少 GPU 的硬件单元,压缩物理尺寸和耗电量。此外,VS、PS 可还可以和其它着色器(几何、曲面、计算)统一为一体。

像素块(Pixel Quad)

上一节步骤 13 提到:

32 个像素线程将被分成一组,或者说 8 个 2x2 的像素块,这是在像素着色器上面的最小工作单元,在这个像素线程内,如果没有被三角形覆盖就会被遮掩,SM 中的 warp 调度器会管理像素着色器的任务。

也就是说,在像素着色器中,会将相邻的四个像素作为不可分隔的一组,送入同一个 SM 内 4 个不同的 Core。

为什么像素着色器处理的最小单元是 2x2 的像素块?
笔者推测有以下原因:

  1. 简化和加速像素分派的工作。
  2. 精简 SM 的架构,减少硬件单元数量和尺寸。
  3. 降低功耗,提高效能比。
  4. 无效像素虽然不会被存储结果,但可辅助有效像素求导函数。详见 4.6 利用扩展例证。

这种设计虽然有其优势,但同时,也会激化过绘制(Over Draw)的情况,损耗额外的性能。比如下图中,白色的三角形只占用了 3 个像素(绿色),按我们普通的思维,只需要 3 个 Core 绘制 3 次就可以了。

但是,由于上面的 3 个像素分别占据了不同的像素块(橙色分隔),实际上需要占用 12 个 Core 绘制 12 次(下图)。

这就会额外消耗 300%的硬件性能,导致了更加严重的过绘制情况。

更多详情可以观看虚幻官方的视频教学:实时渲染深入探究。

GPU 资源机制

本节将阐述 GPU 的内存访问、资源管理等机制。

内存架构

部分架构的 GPU 与 CPU 类似,也有多级缓存结构:寄存器、L1 缓存、L2 缓存、GPU 显存、系统显存。

它们的存取速度从寄存器到系统内存依次变慢:

存储类型 寄存器 共享内存 L1 缓存 L2 缓存 纹理、常量缓存 全局内存
访问周期 1 1~32 1~32 32~64 400~600 400~600

由此可见,shader 直接访问寄存器、L1、L2 缓存还是比较快的,但访问纹理、常量缓存和全局内存非常慢,会造成很高的延迟。

上面的多级缓存结构可被称为“CPU-Style”,还存在 GPU-Style 的内存架构:

这种架构的特点是 ALU 多,GPU 上下文(Context)多,吞吐量高,依赖高带宽与系统内存交换数据

GPU Context 和延迟

由于 SIMT 技术的引入,导致很多同一个 SM 内的很多 Core 并不是独立的,当它们当中有部分 Core 需要访问到纹理、常量缓存和全局内存时,就会导致非常大的卡顿(Stall)。

例如下图中,有 4 组上下文(Context),它们共用同一组运算单元 ALU。

假设第一组 Context 需要访问缓存或内存,会导致 2~3 个周期的延迟,此时调度器会激活第二组 Context 以利用 ALU:

当第二组 Context 访问缓存或内存又卡住,会依次激活第三、第四组 Context,直到第一组 Context 恢复运行或所有都被激活:

延迟的后果是每组 Context 的总体执行时间被拉长了:

但是,越多 Context 可用就越可以提升运算单元的吞吐量,比如下图的 18 组 Context 的架构可以最大化地提升吞吐量:

CPU-GPU 异构系统

根据 CPU 和 GPU 是否共享内存,可分为两种类型的 CPU-GPU 架构:

上图左是分离式架构,CPU 和 GPU 各自有独立的缓存和内存,它们通过 PCI-e 等总线通讯。这种结构的缺点在于 PCI-e 相对于两者具有低带宽和高延迟,数据的传输成了其中的性能瓶颈。目前使用非常广泛,如 PC、智能手机等。

上图右是耦合式架构,CPU 和 GPU 共享内存和缓存。AMD 的 APU 采用的就是这种结构,目前主要使用在游戏主机中,如 PS4。

在存储管理方面,分离式结构中 CPU 和 GPU 各自拥有独立的内存,两者共享一套虚拟地址空间,必要时会进行内存拷贝。对于耦合式结构,GPU 没有独立的内存,与 GPU 共享系统内存,由 MMU 进行存储管理。

GPU 资源管理模型

下图是分离式架构的资源管理模型:

  • MMIO(Memory Mapped IO)

    • CPU 与 GPU 的交流就是通过 MMIO 进行的。CPU 通过 MMIO 访问 GPU 的寄存器状态。
    • DMA 传输大量的数据就是通过 MMIO 进行命令控制的。
    • I/O 端口可用于间接访问 MMIO 区域,像 Nouveau 等开源软件从来不访问它。
  • GPU Context

    • GPU Context 代表了 GPU 计算的状态。
    • 在 GPU 中拥有自己的虚拟地址。
    • GPU 中可以并存多个活跃态下的 Context。
  • GPU Channel

    • 任何命令都是由 CPU 发出。
    • 命令流(command stream)被提交到硬件单元,也就是 GPU Channel。
    • 每个 GPU Channel 关联一个 context,而一个 GPU Context 可以有多个 GPU channel。
    • 每个 GPU Context 包含相关 channel 的 GPU Channel Descriptors ,每个 Descriptor 都是 GPU 内存中的一个对象。
    • 每个 GPU Channel Descriptor 存储了 Channel 的设置,其中就包括 Page Table 。
    • 每个 GPU Channel 在 GPU 内存中分配了唯一的命令缓存,这通过 MMIO 对 CPU 可见。
    • GPU Context Switching 和命令执行都在 GPU 硬件内部调度。
  • GPU Page Table

    • GPU Context 在虚拟基地空间由 Page Table 隔离其它的 Context 。
    • GPU Page Table 隔离 CPU Page Table,位于 GPU 内存中。
    • GPU Page Table 的物理地址位于 GPU Channel Descriptor 中。
    • GPU Page Table 不仅仅将 GPU 虚拟地址转换成 GPU 内存的物理地址,也可以转换成 CPU 的物理地址。因此,GPU Page Table 可以将 GPU 虚拟地址和 CPU 内存地址统一到 GPU 统一虚拟地址空间来。
  • PCI-e BAR

    • GPU 设备通过 PCI-e 总线接入到主机上。 Base Address Registers(BARs) 是 MMIO 的窗口,在 GPU 启动时候配置。
    • GPU 的控制寄存器和内存都映射到了 BARs 中。
    • GPU 设备内存通过映射的 MMIO 窗口去配置 GPU 和访问 GPU 内存。
  • PFIFO Engine

    • PFIFO 是 GPU 命令提交通过的一个特殊的部件。
    • PFIFO 维护了一些独立命令队列,也就是 Channel。
    • 此命令队列是 Ring Buffer,有 PUT 和 GET 的指针。
    • 所有访问 Channel 控制区域的执行指令都被 PFIFO 拦截下来。
    • GPU 驱动使用 Channel Descriptor 来存储相关的 Channel 设定。
    • PFIFO 将读取的命令转交给 PGRAPH Engine。
  • BO

    • Buffer Object (BO),内存的一块(Block),能够用于存储纹理(Texture)、渲染目标(Render Target)、着色代码(shader code)等等。
    • Nouveau 和 Gdev 经常使用 BO。

Nouveau 是一个自由及开放源代码显卡驱动程序,是为 NVidia 的显卡所编写。
Gdev 是一套丰富的开源软件,用于 NVIDIA 的 GPGPU 技术,包括设备驱动程序。

CPU-GPU 数据流

下图是分离式架构的 CPU-GPU 的数据流程图:

  1. 将主存的处理数据复制到显存中。
  2. CPU 指令驱动 GPU。
  3. GPU 中的每个运算单元并行处理。此步会从显存存取数据。
  4. GPU 将显存结果传回主存。

显像机制

  • 水平和垂直同步信号

在早期的 CRT 显示器,电子枪从上到下逐行扫描,扫描完成后显示器就呈现一帧画面。然后电子枪回到初始位置进行下一次扫描。为了同步显示器的显示过程和系统的视频控制器,显示器会用硬件时钟产生一系列的定时信号。

当电子枪换行进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称 HSync

当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization),简称 VSync。

显示器通常以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。虽然现在的显示器基本都是液晶显示屏了,但其原理基本一致。

CPU 将计算好显示内容提交至 GPU,GPU 渲染完成后将渲染结果存入帧缓冲区,视频控制器会按照 VSync 信号逐帧读取帧缓冲区的数据,经过数据转换后最终由显示器进行显示。

  • 双缓冲

在单缓冲下,帧缓冲区的读取和刷新都都会有比较大的效率问题,经常会出现相互等待的情况,导致帧率下降。

为了解决效率问题,GPU 通常会引入两个缓冲区,即双缓冲机制。在这种情况下,GPU 会预先渲染一帧放入一个缓冲区中,用于视频控制器的读取。当下一帧渲染完毕后,GPU 会直接把视频控制器的指针指向第二个缓冲器。

  • 垂直同步

双缓冲虽然能解决效率问题,但会引入一个新的问题。当视频控制器还未读取完成时,即屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,视频控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象:

为了解决这个问题,GPU 通常有一个机制叫做垂直同步(简写也是 V-Sync),当开启垂直同步后,GPU 会等待显示器的 VSync 信号发出后,才进行新的一帧渲染和缓冲区更新。这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟。

Shader 运行机制

Shader 代码也跟传统的 C++等语言类似,需要将面向人类的高级语言(GLSL、HLSL、CGSL)通过编译器转成面向机器的二进制指令,二进制指令可转译成汇编代码,以便技术人员查阅和调试。

由高级语言编译成汇编指令的过程通常是在离线阶段执行,以减轻运行时的消耗。

在执行阶段,CPU 端将 shader 二进制指令经由 PCI-e 推送到 GPU 端,GPU 在执行代码时,会用 Context 将指令分成若干 Channel 推送到各个 Core 的存储空间。

对现代 GPU 而言,可编程的阶段越来越多,包含但不限于:

  • 顶点着色器(Vertex Shader)
  • 曲面细分控制着色器(Tessellation Control Shader)
  • 几何着色器(Geometry Shader)
  • 像素/片元着色器(Fragment Shader)
  • 计算着色器(Compute Shader)

这些着色器形成流水线式的并行化的渲染管线。下面将配合具体的例子说明。

下段是计算漫反射的经典代码:

1
2
3
4
5
6
7
8
9
10
11
sampler mySamp;
Texture2D<float3> myTex;
float3 lightDir;

float4 diffuseShader(float3 norm, float2 uv)
{
float3 kd;
kd = myTex.Sample(mySamp, uv);
kd *= clamp( dot(lightDir, norm), 0.0, 1.0);
return float4(kd, 1.0);
}

经过编译后成为汇编代码

1
2
3
4
5
6
7
8
9
10
<diffuseShader>:
sample r0, v4, t0, s0
mul r3, v0, cb0[0]
madd r3, v1, cb0[1], r3
madd r3, v2, cb0[2], r3
clmp r3, r3, l(0.0), l(1.0)
mul o0, r0, r3
mul o1, r1, r3
mul o2, r2, r3
mov o3, l(1.0)

在执行阶段,以上汇编代码会被 GPU 推送到执行上下文(Execution Context),然后 ALU 会逐条获取(Detch)、解码(Decode)汇编指令,并执行它们。

以上示例图只是单个 ALU 的执行情况,实际上,GPU 有几十甚至上百个执行单元在同时执行 shader 指令:

对于 SIMT 架构的 GPU,汇编指令有所不同,变成了 SIMT 特定指令代码:

1
2
3
4
5
6
7
8
9
10
<VEC8_diffuseShader>:
VEC8_sample vec_r0, vec_v4, t0, vec_s0
VEC8_mul vec_r3, vec_v0, cb0[0]
VEC8_madd vec_r3, vec_v1, cb0[1], vec_r3
VEC8_madd vec_r3, vec_v2, cb0[2], vec_r3
VEC8_clmp vec_r3, vec_r3, l(0.0), l(1.0)
VEC8_mul vec_o0, vec_r0, vec_r3
VEC8_mul vec_o1, vec_r1, vec_r3
VEC8_mul vec_o2, vec_r2, vec_r3
VEC8_mov o3, l(1.0)

并且 Context 以 Core 为单位组成共享的结构,同一个 Core 的多个 ALU 共享一组 Context:

如果有多个 Core,就会有更多的 ALU 同时参与 shader 计算,每个 Core 执行的数据是不一样的,可能是顶点、图元、像素等任何数据:

利用扩展例证

略,详见参考文献

总结

CPU vs GPU

CPU GPU
延迟容忍度
并行目标 任务(Task) 数据(Data)
核心架构 多线程核心 SIMT 核心
线程数量级别 10 10000
吞吐量
缓存需求量
线程独立性

它们之间的差异(缓存、核心数量、内存、线程数等)可用下图展示出来:

Reference

https://www.cnoblogs.com/timlly/p/11471507.html