ISPC简要介绍
What is ISPC
-
编译器
基于LLVM
-
类c语言
SIMD code
-
CPU shader
Why ISPC
- 可以利用simd的特性来提高c/cpp程序的性能
- 更容易编写、维护
- 同一个程序可以编译成多个指令集
- 更容易整合进现有项目
How to use ISPC
-
安装编译器(https://ispc.github.io/downloads.html)
-
编译
simple.ispc示例代码
export void simple(uniform float vin[], uniform float vout[], uniform int count) { foreach (index = 0 ... count) { float v = vin[index]; if (v < 3.) v = v * v; else v = sqrt(v); vout[index] = v; } }
编译指令(https://ispc.github.io/ispc.html#Using-The-ISPC-Compiler)
ispc simple.ispc -O2 -o simple.obj -h simple.h --opt=fast-math --arch=x86-64 --target=sse2
编译后会生成simple.obj和simple.h文件
#ifdef __cplusplus extern "C" { #endif // __cplusplus extern void simple(float vin[], float vout[], int32_t count); #ifdef __cplusplus } #endif // __cplusplus
C++示例代码
#include <stdio.h> #include "simple.h" int main() { float vin[16], vout[16]; for (int i = 0; i < 16; ++i) vin[i] = i; simple(vin, vout, 16); for (int i = 0; i < 16; ++i) printf("%d: simple(%f) = %f\n", i, vin[i], vout[i]); }
ISPC工程编译流程图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VANScoz4-1632655362956)(image-20210926170950585.png)]
ISPC Programming
- Uniform
- Scalar Data
- Results in a scalar register (eax, ebx, ecx, etc…)
- 所有SIMD lanes共享同一个变量
- Scalar Data
- varing
- Vector data
- Results in a SIMD vector register (XMM, YMM, ZMM, etc.)
- 数据的默认类型
- 每个simd lane拥有一个独立数据
- Vector data
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KizfLZkM-1632655362958)(uniform and varing.png)]
-
Built in variables
- programCount
- programIndex
-
Control Flow
-
c/c++
- Conditionals
- if, else, switch
- Loops
- for, while, do…while
- Conditionals
-
ispc自定义的
-
foreach, foreach_active, foreach_tiled, foreach_unique
-
cif, cwhile, cdo, cfor
-
-
-
Arrays
-
Structures
-
Pointer
-
Memory Allocations
The ISPC Standard Library
- Logical operators
- Bit ops
- Math
- Clamping and Saturated Arithmetic
- Transcendental Operations
- RNG (Not the fastest!)
- Mask/Cross-lane Operations
- Reductions
- …
Other Feactures
- AoS to SoA Helper Functions
- C++ Like References
- Binary Operator Overloading for Structures (+, -, *, /, <<, >>)
- Built-in Task System
The ISPC Parallel Execution Model
概念
-
Program Instance
-
Gang
一组同时运行的程序实例,数量不超过SIMD位宽的2-4倍
-
program counter
gang中所有程序实例共享,指向下一条需要执行的指令
-
Execution Mask
程序实例独有,决定当前执行语句的结果是否作用于当前程序实例
从C/C++代码中调用ISPC函数时,程序执行模式从APP串行模式转换成了ISPC SPMD模式,这个过程中没有线程创建,也没有隐式的上下文转换。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KD6Y1iXB-1632655362960)(ExecutionMode.png)]
在ISPC函数中,如果程序实例想要执行某一条语句,那么program counter就会指向这条语句,也就是说,program counter执行的是程序实例执行语句的并集。这就会导致一个问题,因为程序实例根据自身条件语句的不同会执行不同的语句,如果program counter指示cpu执行了程序实例本身所不需要执行的语句,程序实例计算的结果就会出错。这个时候Execution Mask就起作用了,如果某个程序实例不需要某条语句执行的结果,则会将Execution Mask置为off,这样语句执行结果就不会作用到当前程序实例上,对结果就没有影响了。但是这样的情况越多,那么对SIMD的利用率就越低,程序效率就会降低。
SPMD on SIMD
每个Progam Instance对应一个SIMD lane
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X5QbAZ9B-1632655362962)(spmd ond simd.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kRNpgQ2u-1632655362963)(cudasimd.png)]
Control Flow Example: If Statements
float x = ..., y = ...;
bool test = (x < y);
mask originalMask = get_current_mask();
set_mask(originalMask & test);
if (any_mask_entries_are_enabled()) {
// true statements
}
set_mask(originalMask & ~test);
if (any_mask_entries_are_enabled()) {
// false statements
}
set_mask(originalMask);
上述伪代码很好的说明了Execution Mask的作用,程序计数器会指示cpu执行每一条语句,但当且仅当某个程序实例的mask在enabled的状态下,语句执行结果才能作用到当前程序实例上。还要说明的是,如果所有程序实例mask对于某条语句都为off,则CPU不会执行到这条语句。
Control Flow Example: Loops
int limit = ...;
for (int i = 0; i < limit; ++i) {
...
}
如上代码,对于每个程序实例来说,limit的值可能都不一样,这意味有的程序实例循环次数多,有的程序实例循环次数少。程序计数器会拿最大循环次数来执行这个循环体,当某个程序实例所需要的循环次数已经执行完毕时,剩下的循环中,它的mask将被置为off,以消除多余循环次数对该程序实例的影响。
对continue和break的处理有两种方式,一种是当程序实例执行到continue时将Execution Mask置为false,然后接着执行剩下的语句。另一种是如果所有程序在continue后都被禁用,则直接跳过剩下的循环体。
Gang Convergence Guarantees
由于program counter是gang中所有程序实例共享的,故提供了一种收敛保证,即所有执行相同的路径的程序实例都会被同时执行,如果两个程序实例执行不同的控制流路径,则它们会尽快收敛
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vpRk0m69-1632655362964)(image-20210924112121963.png)]
if (programIndex == 0) {
while (true) // infinite loop
;
}
print("hello, world\n");
由于这种模型,print语句将永远不会被执行。
Uniform变量
- 减小数据占用空间,降低对带宽的要求,减小对向量寄存器的要求
- 对控制流做简化
- 可以用向量处理指令
Uniform Control Flow
如果一个条件语句是基于Uniform变量作判断的,编译器就可以判定所有的程序实例都将执行相同的分支,节省了处理控制流分离,处理mask的开销
Data Races Within a Gang
side-effect:表达式求值过程中是否会改变引用变量的值,如果会改变,则有副作用,若未改变,则不具有副作用
表达式:求值,副作用。
sequence point:表达式的求值和副作用都已经结束。
- 表达式结尾
- 函数第一条语句执行之前,此时形参都被赋值了
- 函数返回,调用者代码执行之前
- 初始化表达式结尾
- …
当前程序实例执行任何语句的副作用都会在下个序列点之后对gang中其他程序实例可见。
Support For SOA Layout
避免了gather,scatter等某些CPU不支持的操作,转变为Vector loads
struct Foo { float x, y, z; };
uniform Foo a[...] = { ... };
int index = ...;
float x = a[index].x;
struct Foo4 { float x[4], y[4], z[4]; };
uniform Foo4 a[...] = { ... };
int index = ...;
float x = a[index/4].x[index & 3]
struct Foo { float x, y, z; };
soa<4> struct Foo a[...] = { ... };
int index = ...;
float x = a[index].x;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EfmCEea2-1632655362964)(SOA.png)]
Tasking Model
launch
用launch调用的方法跟调用者异步执行,它可能会立即执行或者它会同步在其他核上执行
一个方法调用的多个函数执行没有先后顺序,且它们可能会同步执行。
sync关键词会让调用者等待所有它launch的方法返回后才执行下面的语句